Quasi finito

main
Antonio De Lucreziis 2 years ago
parent 0c2009b659
commit bd0a9b5c6e

@ -7,6 +7,8 @@ import { AdminPanel } from '../pages/Admin.jsx'
import { ApiKeys } from '../pages/ApiKeys.jsx' import { ApiKeys } from '../pages/ApiKeys.jsx'
import { Login } from '../pages/Login.jsx' import { Login } from '../pages/Login.jsx'
import { Dashboard } from '../pages/Dashboard.jsx' import { Dashboard } from '../pages/Dashboard.jsx'
import { Buckets } from '../pages/Buckets.jsx'
import { Bucket } from '../pages/Bucket.jsx'
export const MainContent = () => { export const MainContent = () => {
const { setSidebarVisible, route, user } = useStore() const { setSidebarVisible, route, user } = useStore()
@ -17,6 +19,8 @@ export const MainContent = () => {
login: Login, login: Login,
admin: AdminPanel, admin: AdminPanel,
apiKeys: ApiKeys, apiKeys: ApiKeys,
buckets: Buckets,
bucket: Bucket,
}[route.id] || (() => <>Still nothing here</>) }[route.id] || (() => <>Still nothing here</>)
return ( return (

@ -39,6 +39,10 @@ const App = () => {
}, },
}) })
if (id === 'unknown') {
location.href = '/'
}
const user = useUser(user => { const user = useUser(user => {
if (id !== 'login' && user !== 'admin') { if (id !== 'login' && user !== 'admin') {
location.href = '/login' location.href = '/login'

@ -0,0 +1,106 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import { Icon } from '../components/Icon.jsx'
import { useStore } from '../context.jsx'
import { useRemoteState } from '../util.jsx'
export const Bucket = () => {
const {
route: {
params: { bucket },
},
showToast,
} = useStore()
const [bucketObjects, updateBucketObjects] = useRemoteState(`/api/buckets/${bucket}`, [])
const fileInputRef = useRef(null)
const uploadFile = async () => {
const formData = new FormData()
formData.append('file', fileInputRef.current.files[0])
showToast(<>Sto caricando il file...</>)
const res = await fetch(`/api/buckets/${bucket}`, {
method: 'POST',
body: formData,
})
if (res.ok) {
const { id } = await res.json()
showToast(
<>
File caricato con id <code>{id}</code>
</>
)
await updateBucketObjects()
}
}
const deleteObject = async id => {
const res = await fetch(`/api/buckets/${bucket}/${id}`, { method: 'DELETE' })
if (res.ok) {
showToast('Oggetto rimosso')
} else {
showToast(`Errore: ${await res.text()}`)
}
}
return (
<>
<div class="panel">
<div class="row">
<div class="row-group">
<div class="title">
Bucket / <code>{bucket}</code>
</div>
</div>
<div class="row-group">
<button class="icon" onClick={() => fileInputRef.current.click()}>
<Icon name="file_upload" />
</button>
<input
type="file"
ref={fileInputRef}
onChange={() => uploadFile()}
style="display: none;"
/>
</div>
</div>
<div class="table">
<div class="cells">
{/* Header */}
<div class="header">Objects</div>
<div class="header">Actions</div>
{/* Rows */}
{bucketObjects.map(key => (
<>
<div class="name">
<code>{key}</code>
</div>
<div class="options">
<a
class="button icon flat"
href={`/api/buckets/${bucket}/${key}`}
>
<Icon name="file_download" />
</a>
<button
class="icon delete flat"
onClick={() => deleteObject(key)}
>
<Icon name="delete" />
</button>
</div>
</>
))}
</div>
</div>
</div>
</>
)
}

@ -0,0 +1,118 @@
import { useEffect, useState } from 'preact/hooks'
import { Icon } from '../components/Icon.jsx'
import { showToast } from '../components/Toasts.jsx'
import { useRemoteState } from '../util.jsx'
export const Buckets = () => {
const [buckets, updateBuckets] = useRemoteState('/api/buckets', [])
const [allBucketSettings, setAllBucketSettings] = useState({})
const updateBucketSettings = async bucket => {
const res = await fetch(`/api/buckets/${bucket}/settings`)
const value = await res.json()
setAllBucketSettings(s => ({ ...s, [bucket]: value }))
}
useEffect(() => {
for (const bucket of buckets) {
updateBucketSettings(bucket)
}
}, [buckets])
const [newBucketName, setNewBucketName] = useState('')
const [newBucketSettings, setNewBucketSettings] = useState('')
const createBucket = async () => {
const { path } = JSON.parse(newBucketSettings || '{"path":""}')
const res = await fetch(`/api/buckets/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
bucket: newBucketName,
path,
}),
})
if (res.ok) {
await updateBuckets()
} else {
showToast(`Errore: ${await res.text()}`)
}
}
return (
<>
<div class="panel">
<div class="row">
<div class="row-group">
<div class="title">Buckets</div>
</div>
</div>
<div class="table">
<div class="cells">
{/* Header */}
<div class="header">Bucket</div>
<div class="header">Settings</div>
<div class="header">Options</div>
{/* Rows */}
{buckets.map(bucket => (
<>
<div class="name">
<a href={`/buckets/${bucket}`}>
<code>{bucket}</code>
</a>
</div>
<div class="settings">
<input
readonly
class="mono"
type="text"
value={JSON.stringify(allBucketSettings[bucket])}
/>
</div>
<div class="options">
<button class="icon delete flat">
<Icon name="delete" />
</button>
</div>
</>
))}
</div>
</div>
</div>
<form action="/api/buckets" method="POST">
<div class="fill title">Nuovo Bucket</div>
<label for="new-bucket-name">Name</label>
<input
type="text"
name="bucket"
id="new-bucket-name"
placeholder="new-bucket"
value={newBucketName}
onInput={e => setNewBucketName(e.target.value)}
/>
<label for="new-bucket-settings">Settings</label>
<input
type="text"
name="settings"
id="new-bucket-settings"
placeholder={`{"path":"${newBucketName || 'new-bucket'}/"}`}
value={newBucketSettings}
onInput={e => setNewBucketSettings(e.target.value)}
/>
<div class="fill center">
<button type="button" onClick={() => createBucket()}>
Crea Bucket
</button>
</div>
</form>
</>
)
}

@ -33,8 +33,14 @@ export function useUser(onLoginStateChanged) {
return user return user
} }
//
// Hooks
//
// Router
function matchPattern(pattern, url) { function matchPattern(pattern, url) {
const r = `^${pattern.replace(/:([a-zA-Z0-9\_\-]+)/g, '(?<$1>.+?)')}$` const r = `^${pattern.replace(/:([a-zA-Z0-9\_\-]+)/g, '(?<$1>[^\\/\\?]+?)')}$`
return new RegExp(r).exec(url) return new RegExp(r).exec(url)
} }
@ -49,6 +55,27 @@ export const useRouter = routes => {
return ['unknown', { id: 'unknown' }, {}] return ['unknown', { id: 'unknown' }, {}]
} }
// useRemoteState
export function useRemoteState(url, initialValue = null) {
const [value, setValue] = useState(initialValue)
const updateValue = async () => {
const res = await fetch(url)
if (!res.ok) {
console.error(res.body)
}
setValue(await res.json())
}
useEffect(() => {
updateValue()
}, [])
return [value, updateValue]
}
// //
// Change Case Utility // Change Case Utility
// //

@ -13,6 +13,7 @@
--bg-dark-400: #55555f; --bg-dark-400: #55555f;
--bg-dark-500: #38383d; --bg-dark-500: #38383d;
--fg-light-300: #5d5d5d;
--fg-light-400: #3d3d3d; --fg-light-400: #3d3d3d;
--fg-light-500: #333; --fg-light-500: #333;
@ -215,6 +216,8 @@ main {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1rem;
flex-grow: 1; flex-grow: 1;
padding: 1rem 1rem 0 1rem; padding: 1rem 1rem 0 1rem;
@ -265,6 +268,43 @@ main {
// Components // Components
// //
.toasts {
position: absolute;
z-index: 10;
left: 50%;
transform: translateX(-50%);
bottom: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
transition: all 150ms linear;
font-size: 15px;
.toast {
background: #38383d;
color: var(--fg-dark-500);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
opacity: 0.95;
animation: toast-fade-in 250ms ease-out;
transition: opacity 250ms ease-in;
&.removed {
opacity: 0;
}
}
}
.panel { .panel {
background: var(--bg-light-100); background: var(--bg-light-100);
border: 1px solid var(--bg-light-600); border: 1px solid var(--bg-light-600);
@ -277,6 +317,10 @@ main {
& .title { & .title {
font-weight: var(--ft-sans-wt-bold); font-weight: var(--ft-sans-wt-bold);
code {
font-weight: var(--ft-sans-wt-normal);
}
} }
} }
@ -301,6 +345,9 @@ main {
& > div { & > div {
min-width: 4rem; min-width: 4rem;
display: flex;
align-items: center;
} }
} }
} }
@ -385,6 +432,15 @@ button,
background: var(--bg-light-700); background: var(--bg-light-700);
} }
} }
&.delete,
&.error {
color: #e62929;
&:hover {
background: #e629291d;
}
}
} }
input[type='text'], input[type='text'],
@ -408,6 +464,11 @@ textarea {
font-size: 90%; font-size: 90%;
font-family: var(--ft-mono); font-family: var(--ft-mono);
} }
&:read-only {
color: var(--fg-light-300);
background: var(--bg-light-500);
}
} }
textarea { textarea {
@ -540,51 +601,36 @@ $heading-scale: 1.33;
display: flex; display: flex;
justify-content: end; justify-content: end;
gap: 0.5rem; gap: 0.5rem;
.delete {
color: #e62929;
&:hover {
background: #e629291d;
}
}
} }
} }
} }
.toasts { .route-buckets {
position: absolute; .table {
z-index: 10; min-width: 30rem;
left: 50%;
transform: translateX(-50%);
bottom: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
transition: all 150ms linear;
font-size: 15px;
.toast { .cells {
background: #38383d; grid-template-columns: 1fr auto auto;
color: var(--fg-dark-500); gap: 0.25rem 1rem;
padding: 0.5rem 1rem; .settings {
border-radius: 0.5rem; min-width: 20rem;
opacity: 0.95; input[type='text'] {
width: 100%;
}
}
}
}
}
animation: toast-fade-in 250ms ease-out; .route-bucket {
transition: opacity 250ms ease-in; .table {
min-width: 30rem;
&.removed { .cells {
opacity: 0; grid-template-columns: 1fr auto;
gap: 0.25rem 1rem;
} }
} }
} }

@ -29,33 +29,40 @@ type Database interface {
// API Keys // API Keys
AllAPIKeys() ([]string, error)
CreateAPIKey() (string, error) CreateAPIKey() (string, error)
CheckAPIKey(key string) error CheckAPIKey(key string) error
RemoveAPIKey(key string) error RemoveAPIKey(key string) error
AllAPIKeys() ([]string, error)
// Bucket Operations // Bucket Operations
CreateBucket(bucket string, options ...any) error
DeleteBucket(bucket string) error
AllBuckets() ([]string, error) AllBuckets() ([]string, error)
CreateBucket(bucket string, settings any) error
GetBucketSettings(bucket string) (any, error)
SetBucketSettings(bucket string, settings any) error
DeleteBucket(bucket string) error
// Bucket Object Operations // Bucket Object Operations
CreateBucketObject(bucket string, r io.Reader) (string, error) AllBucketObjects(bucket string) ([]string, error)
DeleteBucketObject(bucket, id string) error
CreateBucketObject(bucket string, r io.Reader) (string, error)
GetBucketObject(bucket, id string, w io.Writer) error GetBucketObject(bucket, id string, w io.Writer) error
SetBucketObject(bucket, id string, r io.Reader) error SetBucketObject(bucket, id string, r io.Reader) error
DeleteBucketObject(bucket, id string) error
}
AllBucketObjects(bucket string) ([]string, error) type JsonBucketSettings struct {
Path string `json:"path"`
} }
type jsonBucket struct { type jsonBucket struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"`
Objects []string `json:"objects"` Objects []string `json:"objects"`
Settings *JsonBucketSettings `json:"settings"`
} }
type jsonDB struct { type jsonDB struct {
@ -207,7 +214,7 @@ func (db *jsonDB) CheckAPIKey(key string) error {
} }
if !slices.Contains(db.APIKeys, key) { if !slices.Contains(db.APIKeys, key) {
return fmt.Errorf("the given API key %q is invalid", key) return fmt.Errorf("the given api key %q is invalid", key)
} }
return nil return nil
@ -220,7 +227,7 @@ func (db *jsonDB) RemoveAPIKey(key string) error {
i := slices.Index(db.APIKeys, key) i := slices.Index(db.APIKeys, key)
if i == -1 { if i == -1 {
return fmt.Errorf("the given API key %q is invalid", key) return fmt.Errorf("the given api key %q is invalid", key)
} }
db.APIKeys = append(db.APIKeys[:i], db.APIKeys[i+1:]...) db.APIKeys = append(db.APIKeys[:i], db.APIKeys[i+1:]...)
@ -232,7 +239,11 @@ func (db *jsonDB) RemoveAPIKey(key string) error {
return nil return nil
} }
func (db *jsonDB) CreateBucket(bucket string, options ...any) error { //
// Bucket Methods
//
func (db *jsonDB) CreateBucket(bucket string, settings any) error {
if err := db.load(); err != nil { if err := db.load(); err != nil {
return err return err
} }
@ -242,12 +253,15 @@ func (db *jsonDB) CreateBucket(bucket string, options ...any) error {
} }
bi := &jsonBucket{ bi := &jsonBucket{
Name: bucket, Name: bucket,
Path: bucket + "/", Settings: &JsonBucketSettings{
Path: bucket + "/",
},
Objects: []string{}, Objects: []string{},
} }
if len(options) > 0 {
bi.Path = options[0].(string) if settings != nil {
bi.Settings = settings.(*JsonBucketSettings)
} }
db.Buckets[bucket] = bi db.Buckets[bucket] = bi
@ -259,6 +273,39 @@ func (db *jsonDB) CreateBucket(bucket string, options ...any) error {
return nil return nil
} }
func (db *jsonDB) GetBucketSettings(bucket string) (any, error) {
if err := db.load(); err != nil {
return nil, err
}
if _, found := db.Buckets[bucket]; !found {
return nil, fmt.Errorf("bucket named %q not found", bucket)
}
return db.Buckets[bucket].Settings, nil
}
func (db *jsonDB) SetBucketSettings(bucket string, settings any) error {
if err := db.load(); err != nil {
return err
}
b, found := db.Buckets[bucket]
if !found {
return fmt.Errorf("bucket named %q not found", bucket)
}
b.Settings = settings.(*JsonBucketSettings)
log.Printf("[Warning] Bucket %q settings changed but bucket migration is not yet implemented (move the folder yourself)", bucket)
if err := db.store(); err != nil {
return err
}
return nil
}
func (db *jsonDB) DeleteBucket(bucket string) error { func (db *jsonDB) DeleteBucket(bucket string) error {
if err := db.load(); err != nil { if err := db.load(); err != nil {
return err return err
@ -303,7 +350,7 @@ func (db *jsonDB) CreateBucketObject(bucket string, r io.Reader) (string, error)
return "", fmt.Errorf("bucket named %q not found", bucket) return "", fmt.Errorf("bucket named %q not found", bucket)
} }
id, err := store.Create(b.Path, db.PrefixSize, r) id, err := store.Create(b.Settings.Path, db.PrefixSize, r)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -331,7 +378,7 @@ func (db *jsonDB) SetBucketObject(bucket, id string, r io.Reader) error {
return err return err
} }
return store.Update(b.Path, db.PrefixSize, id, r) return store.Update(b.Settings.Path, db.PrefixSize, id, r)
} }
func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error { func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error {
@ -344,7 +391,7 @@ func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error {
return fmt.Errorf("bucket named %q not found", bucket) return fmt.Errorf("bucket named %q not found", bucket)
} }
return store.Read(b.Path, db.PrefixSize, id, w) return store.Read(b.Settings.Path, db.PrefixSize, id, w)
} }
func (db *jsonDB) DeleteBucketObject(bucket, id string) error { func (db *jsonDB) DeleteBucketObject(bucket, id string) error {
@ -357,7 +404,7 @@ func (db *jsonDB) DeleteBucketObject(bucket, id string) error {
return fmt.Errorf("bucket named %q not found", bucket) return fmt.Errorf("bucket named %q not found", bucket)
} }
if err := store.Delete(b.Path, db.PrefixSize, id); err != nil { if err := store.Delete(b.Settings.Path, db.PrefixSize, id); err != nil {
return err return err
} }
@ -381,7 +428,7 @@ func (db *jsonDB) AllBucketObjects(bucket string) ([]string, error) {
return nil, fmt.Errorf("bucket named %q not found", bucket) return nil, fmt.Errorf("bucket named %q not found", bucket)
} }
objects, err := store.All(b.Path) objects, err := store.All(b.Settings.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -22,9 +22,15 @@ func (r *Router) Api(api fiber.Router) {
return c.Next() return c.Next()
} }
isAPIKeyMiddleware := func(c *fiber.Ctx) error { isAPIKeyMiddleware := func(c *fiber.Ctx) error {
if _, isAdmin := adminSessions[c.Cookies("sid")]; !isAdmin { // if admin continue if _, isAdmin := adminSessions[c.Cookies("sid")]; !isAdmin { // if admin continue
if err := r.Database.CheckAPIKey(c.Cookies("token")); err != nil { // otherwise also check api token token := c.Cookies("token")
if token == "" {
return fmt.Errorf("no api token")
}
if err := r.Database.CheckAPIKey(token); err != nil { // otherwise also check api token
return err return err
} }
} }
@ -120,12 +126,12 @@ func (r *Router) Api(api fiber.Router) {
return err return err
} }
opts := []any{} settings := &database.JsonBucketSettings{}
if req.Path != "" { if req.Path != "" {
opts = append(opts, req.Path) settings.Path = req.Path
} }
if err := r.Database.CreateBucket(req.Bucket, opts...); err != nil { if err := r.Database.CreateBucket(req.Bucket, settings); err != nil {
return err return err
} }
@ -145,6 +151,37 @@ func (r *Router) Api(api fiber.Router) {
return c.JSON(objects) return c.JSON(objects)
}) })
api.Get("/buckets/:bucket/settings",
isAdminMiddleware,
func(c *fiber.Ctx) error {
bucket := c.Params("bucket")
settings, err := r.Database.GetBucketSettings(bucket)
if err != nil {
return err
}
return c.JSON(settings)
})
api.Post("/buckets/:bucket/settings",
isAdminMiddleware,
func(c *fiber.Ctx) error {
bucket := c.Params("bucket")
var settings *database.JsonBucketSettings
if err := c.BodyParser(&settings); err != nil {
return err
}
if err := r.Database.SetBucketSettings(bucket, settings); err != nil {
return err
}
return c.JSON("ok")
})
api.Post("/buckets/:bucket", api.Post("/buckets/:bucket",
isAPIKeyMiddleware, isAPIKeyMiddleware,
func(c *fiber.Ctx) error { func(c *fiber.Ctx) error {
@ -186,6 +223,19 @@ func (r *Router) Api(api fiber.Router) {
return c.SendStream(buf) return c.SendStream(buf)
}) })
api.Delete("/buckets/:bucket/:id",
isAPIKeyMiddleware,
func(c *fiber.Ctx) error {
bucket := c.Params("bucket")
id := c.Params("id")
if err := r.Database.DeleteBucketObject(bucket, id); err != nil {
return err
}
return c.JSON("ok")
})
// //
// API Keys // API Keys
// //

Loading…
Cancel
Save