refactor(ui): remaster settings and navigation layout
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
- Update navigation labels from "My Account" to "Dashboard" and "Login" to "Sign in", updating tests accordingly. - Redesign settings forms into structured sections with improved spacing and layout. - Add CSS styles for tabs, small buttons, and responsive settings sections to enhance the user experience.
This commit is contained in:
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
**Go Executable:**
|
||||||
|
```bash
|
||||||
|
/home/linuxbrew/.linuxbrew/bin/go
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run dev server:**
|
||||||
|
```bash
|
||||||
|
# First-time setup: copy the env template
|
||||||
|
cp scripts/env/dev.env.example scripts/env/dev.env
|
||||||
|
# Edit scripts/env/dev.env to set WARPBOX_ADMIN_TOKEN and other values, then:
|
||||||
|
./scripts/run/dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run directly (one-off):**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go run ./cmd/warpbox
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run all tests:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run a single test or package:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go test ./libs/services/... -run TestDeleteTokenVerification
|
||||||
|
go test ./libs/handlers/... -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go build ./cmd/warpbox
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Warpbox is a self-hosted file-sharing app. All code lives under `backend/`. There is no frontend build step — the server renders Go templates and serves static assets directly.
|
||||||
|
|
||||||
|
### Startup flow
|
||||||
|
|
||||||
|
`cmd/warpbox/main.go` → `config.Load()` → `httpserver.New()` → `server.ListenAndServe()`
|
||||||
|
|
||||||
|
`httpserver.New()` wires everything together:
|
||||||
|
1. Creates `web.Renderer` (template engine)
|
||||||
|
2. Creates `UploadService` (opens bbolt DB, creates `files/` and `db/` dirs)
|
||||||
|
3. Creates `AuthService` (reuses same `*bbolt.DB`)
|
||||||
|
4. Creates `SettingsService` (reuses same `*bbolt.DB`)
|
||||||
|
5. Starts background jobs via `jobs.StartAll`
|
||||||
|
6. Creates `handlers.App` with all services
|
||||||
|
7. Registers all routes on a `http.ServeMux`
|
||||||
|
8. Wraps the mux in middleware chain: `Recoverer → RequestID → SecurityHeaders → Gzip → Logger`
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
The three services share a single bbolt database. Each owns distinct buckets:
|
||||||
|
|
||||||
|
| Service | Buckets |
|
||||||
|
|---|---|
|
||||||
|
| `UploadService` | `boxes` |
|
||||||
|
| `AuthService` | `users`, `user_emails`, `sessions`, `invites`, `collections` |
|
||||||
|
| `SettingsService` | `settings`, `usage` |
|
||||||
|
|
||||||
|
`UploadService` owns the DB handle. `AuthService` and `SettingsService` receive `uploadService.DB()`.
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
|
||||||
|
- **Box** — one upload session. Has expiry, optional download limit, optional password (SHA-256 salted hash), optional owner (`OwnerID`), optional collection. Stored as JSON in the `boxes` bucket.
|
||||||
|
- **File** — belongs to a Box. Stored on disk as `data/files/{boxID}/@each@{fileID}.ext`. Thumbnails at `@thumb@{fileID}.jpg`.
|
||||||
|
- Box metadata is also written to `data/files/{boxID}/.warpbox.box.json` on every save.
|
||||||
|
- **User** passwords are hashed with argon2id. Session tokens are SHA-256 hashed before storage.
|
||||||
|
- **Delete tokens** for anonymous boxes are one-time random IDs, stored only as a SHA-256 hash.
|
||||||
|
|
||||||
|
### Handlers
|
||||||
|
|
||||||
|
`handlers.App` holds all three services plus config, logger, and renderer. `RegisterRoutes` maps every URL pattern. Handler files are split by concern: `upload.go`, `download.go`, `dashboard.go` (user `/app`), `admin.go`, `auth.go`, `manage.go`, `pages.go`.
|
||||||
|
|
||||||
|
### Upload policy enforcement
|
||||||
|
|
||||||
|
`SettingsService` stores per-day usage records keyed by `ip:{ip}:{date}` or `user:{userID}:{date}`. The upload handler (`handlers/upload.go`) checks these against `UploadPolicySettings` before accepting a multipart form. Admins bypass all per-upload and daily limits.
|
||||||
|
|
||||||
|
### Background jobs
|
||||||
|
|
||||||
|
`jobs.StartAll` launches goroutines for:
|
||||||
|
- **Cleanup** (`WARPBOX_CLEANUP_ENABLED`): deletes expired boxes and boxes that hit their download limit.
|
||||||
|
- **Thumbnails** (`WARPBOX_THUMBNAIL_ENABLED`): generates JPEG thumbnails for image/video files that don't have one yet.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
All config comes from env vars via `config.Load()`. The dev script sources `scripts/env/dev.env`. `WARPBOX_BASE_URL` is required and must not be empty. Size values accept an optional `MB`/`Mb` suffix and support fractions (e.g. `0.5` = 512 KiB).
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Structured JSONL logs go to `data/logs/{YYYY-MM-DD}.log` via `log/slog`. Every log entry includes `source` (e.g. `"user-upload"`, `"admin"`) and `severity` fields. User-activity events include a numeric `code` field (e.g. `2001` = upload complete, `2101` = box deleted).
|
||||||
|
|
||||||
|
### Template rendering
|
||||||
|
|
||||||
|
`web.Renderer` parses all templates from `backend/templates/` at startup using `html/template`. Page data is passed as `web.PageData`. The base layout is `templates/layouts/base.html`. The current logged-in user is injected into every page render via `a.currentPublicUser(r)`.
|
||||||
|
|
||||||
|
## First-run bootstrap
|
||||||
|
|
||||||
|
On a fresh `data/` directory, visit `/register` to create the first admin account. After bootstrap, normal registration is closed. Admins create invite links from `/admin/users`. The `WARPBOX_ADMIN_TOKEN` env var provides emergency fallback access at `/admin/login`.
|
||||||
@@ -387,7 +387,7 @@ func TestAPIDocsHeaderReflectsLoggedInUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
body := response.Body.String()
|
body := response.Body.String()
|
||||||
header := body[:strings.Index(body, "<main")]
|
header := body[:strings.Index(body, "<main")]
|
||||||
if !strings.Contains(header, "My Account") || strings.Contains(header, ">Login<") || strings.Contains(header, "Health") {
|
if !strings.Contains(header, "Dashboard") || strings.Contains(header, "Sign in") || strings.Contains(header, "Health") {
|
||||||
t.Fatalf("api header did not reflect logged-in state: %s", body)
|
t.Fatalf("api header did not reflect logged-in state: %s", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +404,7 @@ func TestAPIDocsHeaderReflectsLoggedOutUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
body := response.Body.String()
|
body := response.Body.String()
|
||||||
header := body[:strings.Index(body, "<main")]
|
header := body[:strings.Index(body, "<main")]
|
||||||
if !strings.Contains(header, ">Login<") || !strings.Contains(header, ">API<") || strings.Contains(header, "Health") || strings.Contains(header, "My Account") {
|
if !strings.Contains(header, "Sign in") || !strings.Contains(header, ">API<") || strings.Contains(header, "Health") || strings.Contains(header, "Dashboard") {
|
||||||
t.Fatalf("api header did not reflect logged-out state: %s", body)
|
t.Fatalf("api header did not reflect logged-out state: %s", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,12 +337,12 @@ h1 {
|
|||||||
|
|
||||||
.settings-form {
|
.settings-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 1.5rem;
|
||||||
gap: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-form-narrow {
|
.settings-form-narrow {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-form label {
|
.settings-form label {
|
||||||
@@ -1292,4 +1292,239 @@ pre code {
|
|||||||
.metric-grid {
|
.metric-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-collection-body {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── UX remaster ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.button-sm {
|
||||||
|
min-height: 1.85rem;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab navigation */
|
||||||
|
.tabs-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 120ms, color 120ms, border-color 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background: var(--muted);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.is-active {
|
||||||
|
border-color: var(--border);
|
||||||
|
background: var(--muted);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar structure */
|
||||||
|
.sidebar-sep {
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline row edit (details/summary in table cells) */
|
||||||
|
.row-edit {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-edit > summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-edit > summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
.row-edit[open] > summary {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-edit-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-edit-form input,
|
||||||
|
.row-edit-form select {
|
||||||
|
width: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 8rem;
|
||||||
|
min-height: 1.9rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge variants */
|
||||||
|
.badge-active {
|
||||||
|
background: rgba(134, 239, 172, 0.12);
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-disabled {
|
||||||
|
background: rgba(252, 165, 165, 0.1);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-expired {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collection create dropdown */
|
||||||
|
.new-collection-drop {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-collection-drop > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-collection-drop > summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
.new-collection-body {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
z-index: 10;
|
||||||
|
width: 15rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: color-mix(in srgb, var(--card) 97%, #000);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-collection-body label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copyable URL field */
|
||||||
|
.copy-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-field input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings sections */
|
||||||
|
.settings-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-title {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section .checkbox-field {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quota form in admin users table */
|
||||||
|
.quota-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-form input {
|
||||||
|
width: 6.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav username indicator in header */
|
||||||
|
.nav-username {
|
||||||
|
max-width: 8rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,13 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
{{if .CurrentUser}}
|
{{if .CurrentUser}}
|
||||||
|
<a class="button button-ghost" href="/app">Dashboard</a>
|
||||||
|
{{if eq .CurrentUser.Role "admin"}}<a class="button button-ghost" href="/admin">Admin</a>{{end}}
|
||||||
<a class="button button-ghost" href="/api">API</a>
|
<a class="button button-ghost" href="/api">API</a>
|
||||||
<a class="button button-outline" href="/account/settings">My Account</a>
|
<a class="button button-outline" href="/account/settings"><span class="nav-username">{{.CurrentUser.Username}}</span></a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a class="button button-ghost" href="/login">Login</a>
|
|
||||||
<a class="button button-ghost" href="/api">API</a>
|
<a class="button button-ghost" href="/api">API</a>
|
||||||
|
<a class="button button-outline" href="/login">Sign in</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<span>{{.AppName}} · {{.CurrentYear}} · self-hosted</span>
|
<span>{{.AppName}} · {{.CurrentYear}} · self-hosted</span>
|
||||||
<span class="footer-links">{{if .CurrentUser}}<a href="/api">API</a><a href="/account/settings">My Account</a>{{else}}<a href="/login">Login</a><a href="/api">API</a>{{end}}</span>
|
<span class="footer-links">{{if .CurrentUser}}<a href="/app">Dashboard</a><a href="/api">API</a><a href="/account/settings">Account</a>{{else}}<a href="/login">Sign in</a><a href="/api">API</a>{{end}}</span>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="app-shell" aria-labelledby="account-title">
|
<section class="app-shell" aria-labelledby="account-title">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<a class="sidebar-link" href="/app">My files</a>
|
<nav class="sidebar-nav">
|
||||||
<a class="sidebar-link is-active" href="/account/settings">Account settings</a>
|
<a class="sidebar-link" href="/app">My Files</a>
|
||||||
{{if eq .Data.Role "admin"}}<a class="sidebar-link" href="/admin">Admin</a>{{end}}
|
<a class="sidebar-link is-active" href="/account/settings">Account</a>
|
||||||
|
{{if eq .Data.Role "admin"}}<a class="sidebar-link" href="/admin">Admin panel</a>{{end}}
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
<form class="sidebar-logout" action="/logout" method="post">
|
<form class="sidebar-logout" action="/logout" method="post">
|
||||||
<button class="button button-outline" type="submit">Logout</button>
|
<button class="button button-outline" type="submit">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,19 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="app-shell admin-shell" aria-labelledby="admin-title">
|
<section class="app-shell admin-shell" aria-labelledby="admin-title">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<a class="sidebar-link {{if eq .Data.Section "overview"}}is-active{{end}}" href="/admin">Overview</a>
|
<nav class="sidebar-nav">
|
||||||
<a class="sidebar-link {{if eq .Data.Section "files"}}is-active{{end}}" href="/admin/files">Files</a>
|
<a class="sidebar-link {{if eq .Data.Section "overview"}}is-active{{end}}" href="/admin">Overview</a>
|
||||||
<a class="sidebar-link" href="/admin/users">Users</a>
|
<a class="sidebar-link {{if eq .Data.Section "files"}}is-active{{end}}" href="/admin/files">Files</a>
|
||||||
<a class="sidebar-link" href="/admin/settings">Settings</a>
|
<a class="sidebar-link" href="/admin/users">Users</a>
|
||||||
<a class="sidebar-link" href="/app">My files</a>
|
<a class="sidebar-link" href="/admin/settings">Settings</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a class="sidebar-link" href="/app">My Files</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
<form class="sidebar-logout" action="/admin/logout" method="post">
|
<form class="sidebar-logout" action="/admin/logout" method="post">
|
||||||
<button class="button button-outline" type="submit">Logout</button>
|
<button class="button button-outline" type="submit">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -3,62 +3,76 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="app-shell admin-shell" aria-labelledby="admin-settings-title">
|
<section class="app-shell admin-shell" aria-labelledby="admin-settings-title">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<a class="sidebar-link" href="/admin">Overview</a>
|
<nav class="sidebar-nav">
|
||||||
<a class="sidebar-link" href="/admin/files">Files</a>
|
<a class="sidebar-link" href="/admin">Overview</a>
|
||||||
<a class="sidebar-link" href="/admin/users">Users</a>
|
<a class="sidebar-link" href="/admin/files">Files</a>
|
||||||
<a class="sidebar-link is-active" href="/admin/settings">Settings</a>
|
<a class="sidebar-link" href="/admin/users">Users</a>
|
||||||
<a class="sidebar-link" href="/app">My files</a>
|
<a class="sidebar-link is-active" href="/admin/settings">Settings</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a class="sidebar-link" href="/app">My Files</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
<form class="sidebar-logout" action="/admin/logout" method="post">
|
<form class="sidebar-logout" action="/admin/logout" method="post">
|
||||||
<button class="button button-outline" type="submit">Logout</button>
|
<button class="button button-outline" type="submit">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="kicker">Operator console</p>
|
<p class="kicker">Operator console</p>
|
||||||
<h1 id="admin-settings-title">{{.Data.PageTitle}}</h1>
|
<h1 id="admin-settings-title">{{.Data.PageTitle}}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card admin-table-card">
|
<div class="card admin-table-card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Upload policy</h2>
|
<h2>Upload policy</h2>
|
||||||
<p>Values are stored in megabytes. Admin users bypass these upload caps.</p>
|
<p>Admin users bypass all upload caps. Values are in megabytes.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="settings-form" action="/admin/settings" method="post">
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">Anonymous uploads</h3>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input type="checkbox" name="anonymous_uploads_enabled" {{if .Data.Settings.AnonymousUploadsEnabled}}checked{{end}}>
|
||||||
|
<span>Allow anonymous uploads</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Max upload size (MB)</span>
|
||||||
|
<input name="anonymous_max_upload_mb" value="{{.Data.Settings.AnonymousMaxUploadMB}}" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Daily cap per IP (MB)</span>
|
||||||
|
<input name="anonymous_daily_upload_mb" value="{{.Data.Settings.AnonymousDailyUploadMB}}" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">User limits</h3>
|
||||||
|
<label>
|
||||||
|
<span>Daily upload cap (MB)</span>
|
||||||
|
<input name="user_daily_upload_mb" value="{{.Data.Settings.UserDailyUploadMB}}" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Default storage quota (MB)</span>
|
||||||
|
<input name="default_user_storage_mb" value="{{.Data.Settings.DefaultUserStorageMB}}" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Usage retention (days)</span>
|
||||||
|
<input type="number" name="usage_retention_days" min="1" value="{{.Data.Settings.UsageRetentionDays}}" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button button-primary" type="submit">Save settings</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="settings-form" action="/admin/settings" method="post">
|
|
||||||
<label class="checkbox-field">
|
|
||||||
<input type="checkbox" name="anonymous_uploads_enabled" {{if .Data.Settings.AnonymousUploadsEnabled}}checked{{end}}>
|
|
||||||
<span>Allow anonymous uploads</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Anonymous max upload MB</span>
|
|
||||||
<input name="anonymous_max_upload_mb" value="{{.Data.Settings.AnonymousMaxUploadMB}}" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Anonymous daily upload MB per IP</span>
|
|
||||||
<input name="anonymous_daily_upload_mb" value="{{.Data.Settings.AnonymousDailyUploadMB}}" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>User daily upload MB</span>
|
|
||||||
<input name="user_daily_upload_mb" value="{{.Data.Settings.UserDailyUploadMB}}" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Default user storage MB</span>
|
|
||||||
<input name="default_user_storage_mb" value="{{.Data.Settings.DefaultUserStorageMB}}" required>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Usage retention days</span>
|
|
||||||
<input type="number" name="usage_retention_days" min="1" value="{{.Data.Settings.UsageRetentionDays}}" required>
|
|
||||||
</label>
|
|
||||||
<button class="button button-primary" type="submit">Save settings</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -3,80 +3,110 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="app-shell admin-shell" aria-labelledby="admin-users-title">
|
<section class="app-shell admin-shell" aria-labelledby="admin-users-title">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<a class="sidebar-link" href="/admin">Overview</a>
|
<nav class="sidebar-nav">
|
||||||
<a class="sidebar-link" href="/admin/files">Files</a>
|
<a class="sidebar-link" href="/admin">Overview</a>
|
||||||
<a class="sidebar-link is-active" href="/admin/users">Users</a>
|
<a class="sidebar-link" href="/admin/files">Files</a>
|
||||||
<a class="sidebar-link" href="/admin/settings">Settings</a>
|
<a class="sidebar-link is-active" href="/admin/users">Users</a>
|
||||||
<a class="sidebar-link" href="/app">My files</a>
|
<a class="sidebar-link" href="/admin/settings">Settings</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a class="sidebar-link" href="/app">My Files</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
<form class="sidebar-logout" action="/admin/logout" method="post">
|
<form class="sidebar-logout" action="/admin/logout" method="post">
|
||||||
<button class="button button-outline" type="submit">Logout</button>
|
<button class="button button-outline" type="submit">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="kicker">Operator console</p>
|
<p class="kicker">Operator console</p>
|
||||||
<h1 id="admin-users-title">{{.Data.PageTitle}}</h1>
|
<h1 id="admin-users-title">{{.Data.PageTitle}}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card admin-table-card">
|
<div class="card admin-table-card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Create invite</h2>
|
<h2>Create invite</h2>
|
||||||
<p>Copy the generated link and send it manually. SMTP delivery comes later.</p>
|
<p>Copy the generated link and send it manually. SMTP delivery comes later.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Data.LastInviteURL}}
|
||||||
|
<div class="copy-field">
|
||||||
|
<input type="text" value="{{.Data.LastInviteURL}}" readonly id="invite-url-field" aria-label="Invite link">
|
||||||
|
<button class="button button-outline button-sm" type="button"
|
||||||
|
onclick="navigator.clipboard.writeText(document.getElementById('invite-url-field').value).then(()=>{this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',2000)})">Copy</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form class="inline-controls" action="/admin/invites" method="post">
|
||||||
|
<label><span>Email</span><input type="email" name="email" required></label>
|
||||||
|
<label><span>Role</span><select name="role"><option value="user">User</option><option value="admin">Admin</option></select></label>
|
||||||
|
<button class="button button-primary" type="submit">Create invite</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card admin-table-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="table-header">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<p>Disable accounts or generate reset links.</p>
|
||||||
|
</div>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
<th>Today</th>
|
||||||
|
<th>Joined</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Data.Users}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Username}}</td>
|
||||||
|
<td>{{.Email}}</td>
|
||||||
|
<td>{{.Role}}</td>
|
||||||
|
<td><span class="badge {{if eq .Status "active"}}badge-active{{else}}badge-disabled{{end}}">{{.Status}}</span></td>
|
||||||
|
<td>{{.StorageUsed}} / {{.StorageQuota}}</td>
|
||||||
|
<td>{{.DailyUsed}}</td>
|
||||||
|
<td>{{.CreatedAt}}</td>
|
||||||
|
<td class="table-actions">
|
||||||
|
{{if eq .Status "disabled"}}
|
||||||
|
<form action="/admin/users/{{.ID}}/disable?disabled=false" method="post">
|
||||||
|
<button class="button button-outline button-sm" type="submit">Reactivate</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<form action="/admin/users/{{.ID}}/disable" method="post">
|
||||||
|
<button class="button button-danger button-sm" type="submit">Disable</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
<form action="/admin/users/{{.ID}}/reset" method="post">
|
||||||
|
<button class="button button-outline button-sm" type="submit">Reset link</button>
|
||||||
|
</form>
|
||||||
|
<form class="quota-form" action="/admin/users/{{.ID}}/quota" method="post">
|
||||||
|
<input name="storage_quota_mb" placeholder="Quota MB" title="Override storage quota in MB (leave blank to clear override)">
|
||||||
|
<button class="button button-outline button-sm" type="submit">Set</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="8" class="muted-copy">No users yet.</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .Data.LastInviteURL}}
|
|
||||||
<p class="manage-link"><span>Invite link:</span> <a href="{{.Data.LastInviteURL}}">{{.Data.LastInviteURL}}</a></p>
|
|
||||||
{{end}}
|
|
||||||
<form class="inline-controls" action="/admin/invites" method="post">
|
|
||||||
<label><span>Email</span><input type="email" name="email" required></label>
|
|
||||||
<label><span>Role</span><select name="role"><option value="user">User</option><option value="admin">Admin</option></select></label>
|
|
||||||
<button class="button button-primary" type="submit">Create invite</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card admin-table-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="table-header"><h2>Users</h2><p>Disable accounts or create reset links.</p></div>
|
|
||||||
<div class="admin-table-wrap">
|
|
||||||
<table class="admin-table">
|
|
||||||
<thead><tr><th>User</th><th>Email</th><th>Role</th><th>Status</th><th>Storage</th><th>Today</th><th>Joined</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .Data.Users}}
|
|
||||||
<tr>
|
|
||||||
<td>{{.Username}}</td>
|
|
||||||
<td>{{.Email}}</td>
|
|
||||||
<td>{{.Role}}</td>
|
|
||||||
<td><span class="badge">{{.Status}}</span></td>
|
|
||||||
<td>{{.StorageUsed}} / {{.StorageQuota}}</td>
|
|
||||||
<td>{{.DailyUsed}}</td>
|
|
||||||
<td>{{.CreatedAt}}</td>
|
|
||||||
<td class="table-actions">
|
|
||||||
{{if eq .Status "disabled"}}
|
|
||||||
<form action="/admin/users/{{.ID}}/disable?disabled=false" method="post"><button class="button button-outline" type="submit">Reactivate</button></form>
|
|
||||||
{{else}}
|
|
||||||
<form action="/admin/users/{{.ID}}/disable" method="post"><button class="button button-danger" type="submit">Disable</button></form>
|
|
||||||
{{end}}
|
|
||||||
<form action="/admin/users/{{.ID}}/reset" method="post"><button class="button button-outline" type="submit">Reset link</button></form>
|
|
||||||
<form action="/admin/users/{{.ID}}/quota" method="post">
|
|
||||||
<input class="compact-input" name="storage_quota_mb" placeholder="Quota MB">
|
|
||||||
<button class="button button-outline" type="submit">Quota</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{else}}
|
|
||||||
<tr><td colspan="8">No users yet.</td></tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -3,18 +3,14 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="app-shell" aria-labelledby="dashboard-title">
|
<section class="app-shell" aria-labelledby="dashboard-title">
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<a class="sidebar-link is-active" href="/app">Dashboard</a>
|
<nav class="sidebar-nav">
|
||||||
<a class="sidebar-link" href="/account/settings">Settings</a>
|
<a class="sidebar-link is-active" href="/app">My Files</a>
|
||||||
{{if eq .Data.User.Role "admin"}}<a class="sidebar-link" href="/admin">Admin</a>{{end}}
|
<a class="sidebar-link" href="/account/settings">Account</a>
|
||||||
|
{{if eq .Data.User.Role "admin"}}<a class="sidebar-link" href="/admin">Admin panel</a>{{end}}
|
||||||
|
</nav>
|
||||||
|
<hr class="sidebar-sep">
|
||||||
<form class="sidebar-logout" action="/logout" method="post">
|
<form class="sidebar-logout" action="/logout" method="post">
|
||||||
<button class="button button-outline" type="submit">Logout</button>
|
<button class="button button-outline" type="submit">Sign out</button>
|
||||||
</form>
|
|
||||||
<form class="collection-create" action="/app/collections" method="post">
|
|
||||||
<label>
|
|
||||||
<span>New collection</span>
|
|
||||||
<input name="name" placeholder="Projects">
|
|
||||||
</label>
|
|
||||||
<button class="button button-outline" type="submit">Create</button>
|
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -22,46 +18,91 @@
|
|||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="kicker">Personal space</p>
|
<p class="kicker">Personal space</p>
|
||||||
<h1 id="dashboard-title">My files</h1>
|
<h1 id="dashboard-title">My Files</h1>
|
||||||
<p class="muted-copy">{{.Data.StorageUsed}} used · max file size {{.Data.MaxUploadSize}}</p>
|
<p class="muted-copy">{{.Data.StorageUsed}} used · max file size {{.Data.MaxUploadSize}}</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-primary" href="/">Upload files</a>
|
<a class="button button-primary" href="/">Upload files</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collection-tabs">
|
<div class="tabs-bar">
|
||||||
<a class="button {{if not .Data.Selected}}button-primary{{else}}button-outline{{end}}" href="/app">All</a>
|
<div class="tab-list" role="tablist">
|
||||||
{{range .Data.Collections}}
|
<a class="tab {{if not .Data.Selected}}is-active{{end}}" href="/app">All</a>
|
||||||
<a class="button {{if eq $.Data.Selected .ID}}button-primary{{else}}button-outline{{end}}" href="/app?collection={{.ID}}">{{.Name}}</a>
|
{{range .Data.Collections}}
|
||||||
{{end}}
|
<a class="tab {{if eq $.Data.Selected .ID}}is-active{{end}}" href="/app?collection={{.ID}}">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<details class="new-collection-drop">
|
||||||
|
<summary class="button button-outline button-sm">+ Collection</summary>
|
||||||
|
<div class="new-collection-body">
|
||||||
|
<form action="/app/collections" method="post">
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input name="name" placeholder="e.g. Projects" required>
|
||||||
|
</label>
|
||||||
|
<button class="button button-primary button-sm" type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card admin-table-card">
|
<div class="card admin-table-card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="table-header"><h2>Owned boxes</h2><p>Collections organize boxes. Shared links remain unlisted.</p></div>
|
<div class="table-header">
|
||||||
|
<div>
|
||||||
|
<h2>Boxes</h2>
|
||||||
|
<p>Collections organise boxes. Shared links remain unlisted.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="admin-table-wrap">
|
<div class="admin-table-wrap">
|
||||||
<table class="admin-table">
|
<table class="admin-table">
|
||||||
<thead><tr><th>Title</th><th>Collection</th><th>Files</th><th>Size</th><th>Created</th><th>Expires</th><th>Actions</th></tr></thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Collection</th>
|
||||||
|
<th>Files</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Data.Boxes}}
|
{{range .Data.Boxes}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="file-name">{{.Title}}</td>
|
<td>
|
||||||
<td>{{if .CollectionName}}{{.CollectionName}}{{else}}Unsorted{{end}}</td>
|
<div class="file-name">{{.Title}}</div>
|
||||||
|
<details class="row-edit">
|
||||||
|
<summary>Rename</summary>
|
||||||
|
<form action="/app/boxes/{{.ID}}/rename" method="post" class="row-edit-form">
|
||||||
|
<input name="title" placeholder="New title">
|
||||||
|
<button class="button button-outline button-sm" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>{{if .CollectionName}}{{.CollectionName}}{{else}}<span class="muted-copy">—</span>{{end}}</div>
|
||||||
|
<details class="row-edit">
|
||||||
|
<summary>Move</summary>
|
||||||
|
<form action="/app/boxes/{{.ID}}/move" method="post" class="row-edit-form">
|
||||||
|
<select name="collection_id">
|
||||||
|
<option value="">Unsorted</option>
|
||||||
|
{{range $.Data.Collections}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
<button class="button button-outline button-sm" type="submit">Move</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</td>
|
||||||
<td>{{.FileCount}}</td>
|
<td>{{.FileCount}}</td>
|
||||||
<td>{{.Size}}</td>
|
<td>{{.Size}}</td>
|
||||||
<td>{{.CreatedAt}}</td>
|
|
||||||
<td>{{.ExpiresAt}}</td>
|
<td>{{.ExpiresAt}}</td>
|
||||||
<td class="table-actions">
|
<td class="table-actions">
|
||||||
<a class="button button-outline" href="{{.URL}}" target="_blank" rel="noopener noreferrer">Open</a>
|
<a class="button button-outline button-sm" href="{{.URL}}" target="_blank" rel="noopener noreferrer">Open</a>
|
||||||
<form action="/app/boxes/{{.ID}}/rename" method="post"><input class="compact-input" name="title" placeholder="Rename"><button class="button button-outline" type="submit">Save</button></form>
|
<form action="/app/boxes/{{.ID}}/delete" method="post">
|
||||||
<form action="/app/boxes/{{.ID}}/move" method="post">
|
<button class="button button-danger button-sm" type="submit">Delete</button>
|
||||||
<select name="collection_id"><option value="">Unsorted</option>{{range $.Data.Collections}}<option value="{{.ID}}">{{.Name}}</option>{{end}}</select>
|
|
||||||
<button class="button button-outline" type="submit">Move</button>
|
|
||||||
</form>
|
</form>
|
||||||
<form action="/app/boxes/{{.ID}}/delete" method="post"><button class="button button-danger" type="submit">Delete</button></form>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="7">You have no boxes yet.</td></tr>
|
<tr><td colspan="6" class="muted-copy">No boxes yet. Upload some files to get started.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="upload-view" aria-labelledby="upload-title">
|
<section class="upload-view" aria-labelledby="upload-title">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
|
{{if .CurrentUser}}
|
||||||
|
<h1 id="upload-title">Upload files.</h1>
|
||||||
|
<p>{{.Data.LimitSummary}}</p>
|
||||||
|
{{else}}
|
||||||
<h1 id="upload-title">Send a file. Get a link.</h1>
|
<h1 id="upload-title">Send a file. Get a link.</h1>
|
||||||
<p>Anonymous, self-hosted transfers. No account required.</p>
|
<p>Anonymous, self-hosted transfers. No account required.</p>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="upload-panel card" id="upload-form" action="/api/v1/upload" method="post" enctype="multipart/form-data">
|
<form class="upload-panel card" id="upload-form" action="/api/v1/upload" method="post" enctype="multipart/form-data">
|
||||||
|
|||||||
BIN
backend/warpbox
Executable file
BIN
backend/warpbox
Executable file
Binary file not shown.
Reference in New Issue
Block a user