feat: add authentication and book upload
- Session-based login (username/password, 30-day cookie) - SQLite user store with bcrypt password hashing (modernc.org/sqlite) - Per-user upload permission (can_upload flag) - Admin CLI (cmd/admin) for user management: user add/list/delete/set-upload - Upload handler for EPUB/PDF with path-traversal protection - All routes protected by requireAuth middleware; /upload additionally requires requireUpload - Login/logout UI, upload form, logout button in header - New env var: USERS_DB (default: users.db, gitignored) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
# eBook Library (Go + templ)
|
||||||
|
|
||||||
|
Minimalistic, no-JS eBook library web app for reading files (EPUB/PDF) on a
|
||||||
|
Tolino e-reader browser. UI text/comments are in German.
|
||||||
|
|
||||||
|
## Build & run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy # install deps
|
||||||
|
templ generate # regenerate views/*_templ.go from views/*.templ (required after editing .templ files)
|
||||||
|
go build ./... # build check
|
||||||
|
go run ./cmd/server # run dev server on :8080
|
||||||
|
```
|
||||||
|
|
||||||
|
- `templ` CLI is required whenever `views/pages.templ` changes — generated
|
||||||
|
Go code lives in `views/pages_templ.go` and is checked into the repo, so it
|
||||||
|
must be regenerated and committed together with template edits.
|
||||||
|
- No test suite exists yet.
|
||||||
|
- Config via env vars: `ADDR` (default `:8080`), `BOOKS_DIR` (default `books`),
|
||||||
|
`USERS_DB` (default `users.db`).
|
||||||
|
|
||||||
|
## Admin CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/admin user add <name> [--upload] # Benutzer anlegen
|
||||||
|
go run ./cmd/admin user list # alle Benutzer
|
||||||
|
go run ./cmd/admin user delete <name> # löschen
|
||||||
|
go run ./cmd/admin user set-upload <name> <true|false>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `cmd/server/main.go` — entrypoint; wires `library.Service`, `users.Store` and
|
||||||
|
`web.Handler` together and starts the HTTP server.
|
||||||
|
- `internal/library` — core domain logic, no HTTP dependency (unchanged).
|
||||||
|
- `internal/users/store.go` — SQLite-backed user store (`modernc.org/sqlite`,
|
||||||
|
pure Go driver). Tables: `users` (id, username, password_hash bcrypt,
|
||||||
|
can_upload), `sessions` (token, user_id FK, expires_at unix timestamp).
|
||||||
|
Sessions TTL = 30 days. Key methods: `Authenticate`, `CreateSession`,
|
||||||
|
`LookupSession`, `DeleteSession`, `CreateUser`, `SetUpload`, `DeleteUser`, `ListUsers`.
|
||||||
|
- `cmd/admin/main.go` — CLI binary for user management; reads `USERS_DB` env var.
|
||||||
|
- `internal/web/handlers.go` — HTTP handlers including `loginPage`,
|
||||||
|
`loginSubmit`, `logout`, `uploadPage`, `uploadSubmit`.
|
||||||
|
- `internal/web/middleware.go` — `requireAuth` and `requireUpload` middleware;
|
||||||
|
session resolved from `"session"` cookie; stored in request context via `sessionKey`.
|
||||||
|
- `views/pages.templ` — `LoginPage(errMsg)`, `UploadPage(errMsg)` added;
|
||||||
|
`IndexPage` now takes `canUpload bool`; `Layout` includes logout button.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Path traversal guards matter: `downloadBook` validates the resolved book
|
||||||
|
path stays within `BooksDir()` before serving; `cleanEPUBPath` rejects
|
||||||
|
`../`-escaping hrefs inside EPUB zips; `uploadSubmit` validates the
|
||||||
|
destination path stays within `BooksDir()`. Preserve these checks in any
|
||||||
|
related changes.
|
||||||
|
- Cover image reads are capped (`maxCoverBytes = 10 MiB`) to avoid decompression
|
||||||
|
abuse — keep similar limits when reading zip entries.
|
||||||
|
- Handlers return `404` for missing/empty `id` and `500` on internal errors;
|
||||||
|
keep that pattern for new routes.
|
||||||
|
- Auth: `/login` and `/static/` are the only public routes — everything else
|
||||||
|
goes through `requireAuth`. Routes needing upload permission use `requireUpload`
|
||||||
|
(which wraps `requireAuth`).
|
||||||
|
- Session cookie: `HttpOnly`, `SameSite=Lax`, 30-day expiry; no `Secure` flag
|
||||||
|
set (intended for LAN use without TLS).
|
||||||
|
- `modernc.org/sqlite` is a pure-Go SQLite driver (no CGo). `MaxOpenConns(1)`
|
||||||
|
is set because SQLite doesn't support concurrent writers.
|
||||||
|
- `users.db` is gitignored; create it at runtime with the admin CLI.
|
||||||
@@ -24,3 +24,4 @@ dist/
|
|||||||
# Local env
|
# Local env
|
||||||
.env
|
.env
|
||||||
books/
|
books/
|
||||||
|
users.db
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Minimalistische eBook-Bibliothek für den Tolino-Webbrowser.
|
|||||||
- Detailseite pro Buch
|
- Detailseite pro Buch
|
||||||
- Download-Link pro Buch (für Tolino)
|
- Download-Link pro Buch (für Tolino)
|
||||||
- Schlichtes, kontrastreiches UI ohne JavaScript-Abhängigkeit
|
- Schlichtes, kontrastreiches UI ohne JavaScript-Abhängigkeit
|
||||||
|
- Login-Pflicht (Session-Cookie, 30 Tage)
|
||||||
|
- Optionaler Buch-Upload für berechtigte Benutzer
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
@@ -34,13 +36,21 @@ go mod tidy
|
|||||||
templ generate
|
templ generate
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Server starten:
|
3. Ersten Benutzer anlegen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/admin user add <benutzername>
|
||||||
|
# mit Upload-Recht:
|
||||||
|
go run ./cmd/admin user add <benutzername> --upload
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Server starten:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run ./cmd/server
|
go run ./cmd/server
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Browser öffnen:
|
5. Browser öffnen:
|
||||||
|
|
||||||
- `http://localhost:8080`
|
- `http://localhost:8080`
|
||||||
|
|
||||||
@@ -51,9 +61,23 @@ Lege deine Dateien in den Ordner `books/`:
|
|||||||
- `.epub` (bevorzugt)
|
- `.epub` (bevorzugt)
|
||||||
- `.pdf` (optional)
|
- `.pdf` (optional)
|
||||||
|
|
||||||
|
Alternativ können Benutzer mit Upload-Recht Bücher direkt im Browser hochladen.
|
||||||
|
|
||||||
|
## Benutzerverwaltung (CLI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/admin user list
|
||||||
|
go run ./cmd/admin user add <name> [--upload]
|
||||||
|
go run ./cmd/admin user delete <name>
|
||||||
|
go run ./cmd/admin user set-upload <name> <true|false>
|
||||||
|
```
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
Umgebungsvariablen:
|
Umgebungsvariablen:
|
||||||
|
|
||||||
- `ADDR` (Standard `:8080`)
|
| Variable | Standard | Beschreibung |
|
||||||
- `BOOKS_DIR` (Standard `books`)
|
|------------|------------|---------------------------------|
|
||||||
|
| `ADDR` | `:8080` | Listen-Adresse des Servers |
|
||||||
|
| `BOOKS_DIR`| `books` | Verzeichnis mit Büchern |
|
||||||
|
| `USERS_DB` | `users.db` | Pfad zur SQLite-Benutzerdatenbank |
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/arnef/ebooks/internal/users"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := getenv("USERS_DB", "users.db")
|
||||||
|
store, err := users.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("open db: %v\n", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
resource := os.Args[1]
|
||||||
|
action := os.Args[2]
|
||||||
|
|
||||||
|
switch resource {
|
||||||
|
case "user":
|
||||||
|
handleUser(store, action, os.Args[3:])
|
||||||
|
default:
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUser(store *users.Store, action string, args []string) {
|
||||||
|
switch action {
|
||||||
|
case "add":
|
||||||
|
if len(args) < 1 {
|
||||||
|
fatalf("usage: user add <username> [--upload]\n")
|
||||||
|
}
|
||||||
|
username := args[0]
|
||||||
|
canUpload := len(args) > 1 && args[1] == "--upload"
|
||||||
|
|
||||||
|
password, err := readPassword(fmt.Sprintf("Passwort für '%s': ", username))
|
||||||
|
if err != nil {
|
||||||
|
fatalf("passwort lesen: %v\n", err)
|
||||||
|
}
|
||||||
|
confirm, err := readPassword("Passwort bestätigen: ")
|
||||||
|
if err != nil {
|
||||||
|
fatalf("passwort lesen: %v\n", err)
|
||||||
|
}
|
||||||
|
if password != confirm {
|
||||||
|
fatalf("Passwörter stimmen nicht überein.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := store.CreateUser(username, password, canUpload)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("user anlegen: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Benutzer '%s' angelegt (upload: %s)\n", u.Username, yesNo(u.CanUpload))
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
list, err := store.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
fatalf("user auflisten: %v\n", err)
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
fmt.Println("Keine Benutzer vorhanden.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("%-20s %s\n", "BENUTZERNAME", "UPLOAD")
|
||||||
|
fmt.Println(strings.Repeat("-", 32))
|
||||||
|
for _, u := range list {
|
||||||
|
fmt.Printf("%-20s %s\n", u.Username, yesNo(u.CanUpload))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
if len(args) < 1 {
|
||||||
|
fatalf("usage: user delete <username>\n")
|
||||||
|
}
|
||||||
|
if err := store.DeleteUser(args[0]); err != nil {
|
||||||
|
fatalf("user löschen: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Benutzer '%s' gelöscht.\n", args[0])
|
||||||
|
|
||||||
|
case "set-upload":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fatalf("usage: user set-upload <username> <true|false>\n")
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseBool(args[1])
|
||||||
|
if err != nil {
|
||||||
|
fatalf("ungültiger Wert '%s', erwartet true oder false\n", args[1])
|
||||||
|
}
|
||||||
|
if err := store.SetUpload(args[0], val); err != nil {
|
||||||
|
fatalf("upload-recht setzen: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Upload-Recht für '%s' auf %s gesetzt.\n", args[0], yesNo(val))
|
||||||
|
|
||||||
|
default:
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(prompt string) (string, error) {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
b, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func yesNo(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "ja"
|
||||||
|
}
|
||||||
|
return "nein"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, `ebooks-admin – Benutzerverwaltung
|
||||||
|
|
||||||
|
Befehle:
|
||||||
|
user add <name> [--upload] Benutzer anlegen (optional: Upload-Recht)
|
||||||
|
user list Alle Benutzer auflisten
|
||||||
|
user delete <name> Benutzer löschen
|
||||||
|
user set-upload <name> <true|false> Upload-Recht ändern
|
||||||
|
|
||||||
|
Umgebungsvariablen:
|
||||||
|
USERS_DB Pfad zur SQLite-Datenbank (Standard: users.db)`)
|
||||||
|
}
|
||||||
+9
-1
@@ -6,15 +6,23 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/arnef/ebooks/internal/library"
|
"github.com/arnef/ebooks/internal/library"
|
||||||
|
"github.com/arnef/ebooks/internal/users"
|
||||||
"github.com/arnef/ebooks/internal/web"
|
"github.com/arnef/ebooks/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
booksDir := getenv("BOOKS_DIR", "books")
|
booksDir := getenv("BOOKS_DIR", "books")
|
||||||
addr := getenv("ADDR", ":8080")
|
addr := getenv("ADDR", ":8080")
|
||||||
|
usersDB := getenv("USERS_DB", "users.db")
|
||||||
|
|
||||||
|
store, err := users.Open(usersDB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("users db: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
lib := library.New(booksDir)
|
lib := library.New(booksDir)
|
||||||
h := web.NewHandler(lib)
|
h := web.NewHandler(lib, store)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
h.RegisterRoutes(mux)
|
h.RegisterRoutes(mux)
|
||||||
|
|||||||
@@ -3,3 +3,18 @@ module github.com/arnef/ebooks
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require github.com/a-h/templ v0.3.1020
|
require github.com/a-h/templ v0.3.1020
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/term v0.45.0 // indirect
|
||||||
|
modernc.org/libc v1.74.4 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.56.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,28 @@
|
|||||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
|
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||||
|
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
|
||||||
|
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("user not found")
|
||||||
|
var ErrUserExists = errors.New("user already exists")
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64
|
||||||
|
Username string
|
||||||
|
CanUpload bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Token string
|
||||||
|
UserID int64
|
||||||
|
Username string
|
||||||
|
CanUpload bool
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(path string) (*Store, error) {
|
||||||
|
db, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db.SetMaxOpenConns(1) // SQLite doesn't support concurrent writers
|
||||||
|
s := &Store{db: db}
|
||||||
|
if err := s.migrate(); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Close() error { return s.db.Close() }
|
||||||
|
|
||||||
|
func (s *Store) migrate() error {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
can_upload INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser adds a new user. Returns ErrUserExists if the username is taken.
|
||||||
|
func (s *Store) CreateUser(username, password string, canUpload bool) (*User, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
`INSERT INTO users (username, password_hash, can_upload) VALUES (?, ?, ?)`,
|
||||||
|
username, string(hash), boolToInt(canUpload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if isUnique(err) {
|
||||||
|
return nil, ErrUserExists
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id, _ := res.LastInsertId()
|
||||||
|
return &User{ID: id, Username: username, CanUpload: canUpload}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpload changes the upload permission of an existing user.
|
||||||
|
func (s *Store) SetUpload(username string, canUpload bool) error {
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
`UPDATE users SET can_upload = ? WHERE username = ?`,
|
||||||
|
boolToInt(canUpload), username,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n, _ := res.RowsAffected(); n == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser removes a user by username.
|
||||||
|
func (s *Store) DeleteUser(username string) error {
|
||||||
|
res, err := s.db.Exec(`DELETE FROM users WHERE username = ?`, username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n, _ := res.RowsAffected(); n == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers returns all users ordered by username.
|
||||||
|
func (s *Store) ListUsers() ([]User, error) {
|
||||||
|
rows, err := s.db.Query(`SELECT id, username, can_upload FROM users ORDER BY username`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var users []User
|
||||||
|
for rows.Next() {
|
||||||
|
var u User
|
||||||
|
var up int
|
||||||
|
if err := rows.Scan(&u.ID, &u.Username, &up); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.CanUpload = up != 0
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
return users, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate verifies credentials and returns the matching user.
|
||||||
|
func (s *Store) Authenticate(username, password string) (*User, error) {
|
||||||
|
var u User
|
||||||
|
var hash string
|
||||||
|
var up int
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`SELECT id, username, password_hash, can_upload FROM users WHERE username = ?`,
|
||||||
|
username,
|
||||||
|
).Scan(&u.ID, &u.Username, &hash, &up)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
u.CanUpload = up != 0
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession issues a new session token for a user (TTL: 30 days).
|
||||||
|
func (s *Store) CreateSession(u *User) (*Session, error) {
|
||||||
|
token, err := randomToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exp := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
_, err = s.db.Exec(
|
||||||
|
`INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)`,
|
||||||
|
token, u.ID, exp.Unix(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Session{
|
||||||
|
Token: token,
|
||||||
|
UserID: u.ID,
|
||||||
|
Username: u.Username,
|
||||||
|
CanUpload: u.CanUpload,
|
||||||
|
ExpiresAt: exp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupSession returns the session if valid, nil if not found/expired.
|
||||||
|
func (s *Store) LookupSession(token string) (*Session, error) {
|
||||||
|
var sess Session
|
||||||
|
var up int
|
||||||
|
var expUnix int64
|
||||||
|
err := s.db.QueryRow(`
|
||||||
|
SELECT s.token, s.user_id, u.username, u.can_upload, s.expires_at
|
||||||
|
FROM sessions s JOIN users u ON u.id = s.user_id
|
||||||
|
WHERE s.token = ?
|
||||||
|
`, token).Scan(&sess.Token, &sess.UserID, &sess.Username, &up, &expUnix)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sess.ExpiresAt = time.Unix(expUnix, 0)
|
||||||
|
if time.Now().After(sess.ExpiresAt) {
|
||||||
|
_, _ = s.db.Exec(`DELETE FROM sessions WHERE token = ?`, token)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
sess.CanUpload = up != 0
|
||||||
|
return &sess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession invalidates a session token (logout).
|
||||||
|
func (s *Store) DeleteSession(token string) error {
|
||||||
|
_, err := s.db.Exec(`DELETE FROM sessions WHERE token = ?`, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomToken() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", fmt.Errorf("random token: %w", err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnique(err error) bool {
|
||||||
|
return err != nil && (contains(err.Error(), "UNIQUE") || contains(err.Error(), "unique"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, sub string) bool {
|
||||||
|
return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsStr(s, sub string) bool {
|
||||||
|
for i := 0; i+len(sub) <= len(s); i++ {
|
||||||
|
if s[i:i+len(sub)] == sub {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+137
-8
@@ -3,29 +3,46 @@ package web
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/arnef/ebooks/internal/library"
|
"github.com/arnef/ebooks/internal/library"
|
||||||
|
"github.com/arnef/ebooks/internal/users"
|
||||||
"github.com/arnef/ebooks/views"
|
"github.com/arnef/ebooks/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxUploadBytes = 512 << 20 // 512 MiB
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
lib *library.Service
|
lib *library.Service
|
||||||
|
store *users.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(lib *library.Service) *Handler {
|
func NewHandler(lib *library.Service, store *users.Store) *Handler {
|
||||||
return &Handler{lib: lib}
|
return &Handler{lib: lib, store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /", h.listBooks)
|
// Public
|
||||||
mux.HandleFunc("GET /book/{id}", h.bookDetails)
|
mux.HandleFunc("GET /login", h.loginPage)
|
||||||
mux.HandleFunc("GET /download/{id}", h.downloadBook)
|
mux.HandleFunc("POST /login", h.loginSubmit)
|
||||||
mux.HandleFunc("GET /cover/{id}", h.bookCover)
|
mux.HandleFunc("POST /logout", h.logout)
|
||||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
|
|
||||||
|
// Authenticated
|
||||||
|
mux.HandleFunc("GET /", h.requireAuth(h.listBooks))
|
||||||
|
mux.HandleFunc("GET /book/{id}", h.requireAuth(h.bookDetails))
|
||||||
|
mux.HandleFunc("GET /download/{id}", h.requireAuth(h.downloadBook))
|
||||||
|
mux.HandleFunc("GET /cover/{id}", h.requireAuth(h.bookCover))
|
||||||
|
|
||||||
|
// Upload (authenticated + can_upload)
|
||||||
|
mux.HandleFunc("GET /upload", h.requireUpload(h.uploadPage))
|
||||||
|
mux.HandleFunc("POST /upload", h.requireUpload(h.uploadSubmit))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) listBooks(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) listBooks(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -34,7 +51,9 @@ func (h *Handler) listBooks(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "failed to load books", http.StatusInternalServerError)
|
http.Error(w, "failed to load books", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, r.Context(), views.IndexPage(books))
|
sess := sessionFrom(r)
|
||||||
|
canUpload := sess != nil && sess.CanUpload
|
||||||
|
render(w, r.Context(), views.IndexPage(books, canUpload))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) bookDetails(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) bookDetails(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -116,3 +135,113 @@ func render(w http.ResponseWriter, ctx context.Context, c templ.Component) {
|
|||||||
http.Error(w, "render failed", http.StatusInternalServerError)
|
http.Error(w, "render failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Auth handlers ---
|
||||||
|
|
||||||
|
func (h *Handler) loginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render(w, r.Context(), views.LoginPage(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) loginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
|
||||||
|
user, err := h.store.Authenticate(username, password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
render(w, r.Context(), views.LoginPage("Benutzername oder Passwort falsch."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := h.store.CreateSession(user)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Session konnte nicht erstellt werden", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: sess.Token,
|
||||||
|
Path: "/",
|
||||||
|
Expires: sess.ExpiresAt,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) logout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("session")
|
||||||
|
if err == nil {
|
||||||
|
_ = h.store.DeleteSession(cookie.Value)
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
HttpOnly: true,
|
||||||
|
})
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Upload handlers ---
|
||||||
|
|
||||||
|
func (h *Handler) uploadPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render(w, r.Context(), views.UploadPage(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) uploadSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)
|
||||||
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||||
|
render(w, r.Context(), views.UploadPage("Datei zu groß oder ungültige Anfrage."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, header, err := r.FormFile("book")
|
||||||
|
if err != nil {
|
||||||
|
render(w, r.Context(), views.UploadPage("Keine Datei ausgewählt."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||||
|
if ext != ".epub" && ext != ".pdf" {
|
||||||
|
render(w, r.Context(), views.UploadPage("Nur EPUB- und PDF-Dateien erlaubt."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize filename: keep only base name, no path components.
|
||||||
|
safeFilename := filepath.Base(header.Filename)
|
||||||
|
destPath := filepath.Join(h.lib.BooksDir(), safeFilename)
|
||||||
|
|
||||||
|
// Check it still resolves inside BooksDir.
|
||||||
|
cleanBase := filepath.Clean(h.lib.BooksDir())
|
||||||
|
cleanDest := filepath.Clean(destPath)
|
||||||
|
if !strings.HasPrefix(cleanDest, cleanBase+string(filepath.Separator)) {
|
||||||
|
render(w, r.Context(), views.UploadPage("Ungültiger Dateiname."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
render(w, r.Context(), views.UploadPage(fmt.Sprintf("Datei '%s' existiert bereits.", safeFilename)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Datei konnte nicht gespeichert werden", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, file); err != nil {
|
||||||
|
_ = os.Remove(destPath)
|
||||||
|
http.Error(w, "Upload fehlgeschlagen", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/arnef/ebooks/internal/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const sessionKey contextKey = iota
|
||||||
|
|
||||||
|
// sessionFrom retrieves the session stored in request context.
|
||||||
|
func sessionFrom(r *http.Request) *users.Session {
|
||||||
|
s, _ := r.Context().Value(sessionKey).(*users.Session)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireAuth is middleware that redirects unauthenticated requests to /login.
|
||||||
|
func (h *Handler) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sess, err := h.sessionFromRequest(r)
|
||||||
|
if err != nil || sess == nil {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), sessionKey, sess)
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireUpload is middleware that returns 403 if the user lacks upload permission.
|
||||||
|
func (h *Handler) requireUpload(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return h.requireAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sess := sessionFrom(r)
|
||||||
|
if !sess.CanUpload {
|
||||||
|
http.Error(w, "Keine Berechtigung", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) sessionFromRequest(r *http.Request) (*users.Session, error) {
|
||||||
|
cookie, err := r.Cookie("session")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return h.store.LookupSession(cookie.Value)
|
||||||
|
}
|
||||||
@@ -208,3 +208,39 @@ code {
|
|||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Auth & Upload --- */
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #000;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
max-width: 22rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form,
|
||||||
|
.upload-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input,
|
||||||
|
.upload-form input[type="file"] {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
+58
-1
@@ -21,6 +21,9 @@ templ Layout(title string, backHref string) {
|
|||||||
<a class="header-back" href={ backHref } aria-label="Zurück">←</a>
|
<a class="header-back" href={ backHref } aria-label="Zurück">←</a>
|
||||||
}
|
}
|
||||||
<h1><a href="/">eBook Library</a></h1>
|
<h1><a href="/">eBook Library</a></h1>
|
||||||
|
<form method="post" action="/logout" style="margin-left:auto">
|
||||||
|
<button type="submit" class="btn-logout">Abmelden</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
@@ -30,6 +33,57 @@ templ Layout(title string, backHref string) {
|
|||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ LoginLayout(title string) {
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{ title }</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
{ children... }
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ LoginPage(errMsg string) {
|
||||||
|
@LoginLayout("Anmelden") {
|
||||||
|
<section class="login-box">
|
||||||
|
<h2>Anmelden</h2>
|
||||||
|
if errMsg != "" {
|
||||||
|
<p class="error">{ errMsg }</p>
|
||||||
|
}
|
||||||
|
<form method="post" action="/login" class="login-form">
|
||||||
|
<label for="username">Benutzername</label>
|
||||||
|
<input id="username" type="text" name="username" required autofocus />
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
<input id="password" type="password" name="password" required />
|
||||||
|
<button type="submit" class="btn">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ UploadPage(errMsg string) {
|
||||||
|
@Layout("Buch hochladen", "/") {
|
||||||
|
<section class="page-head">
|
||||||
|
<h2>Buch hochladen</h2>
|
||||||
|
</section>
|
||||||
|
if errMsg != "" {
|
||||||
|
<p class="error">{ errMsg }</p>
|
||||||
|
}
|
||||||
|
<form method="post" action="/upload" enctype="multipart/form-data" class="upload-form">
|
||||||
|
<label for="book">EPUB oder PDF auswählen</label>
|
||||||
|
<input id="book" type="file" name="book" accept=".epub,.pdf" required />
|
||||||
|
<button type="submit" class="btn">Hochladen</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
templ BookCover(book library.Book, detail bool) {
|
templ BookCover(book library.Book, detail bool) {
|
||||||
if book.HasCover {
|
if book.HasCover {
|
||||||
<img class={ coverClass(detail) } src={ "/cover/" + book.ID } alt={ "Cover von " + book.Title } loading="lazy" />
|
<img class={ coverClass(detail) } src={ "/cover/" + book.ID } alt={ "Cover von " + book.Title } loading="lazy" />
|
||||||
@@ -40,11 +94,14 @@ templ BookCover(book library.Book, detail bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ IndexPage(books []library.Book) {
|
templ IndexPage(books []library.Book, canUpload bool) {
|
||||||
@Layout("Bibliothek", "") {
|
@Layout("Bibliothek", "") {
|
||||||
<section class="page-head">
|
<section class="page-head">
|
||||||
<h2>Meine Bücher</h2>
|
<h2>Meine Bücher</h2>
|
||||||
<p class="page-subtitle">Titel, Autor und Cover auf einen Blick.</p>
|
<p class="page-subtitle">Titel, Autor und Cover auf einen Blick.</p>
|
||||||
|
if canUpload {
|
||||||
|
<a class="btn" href="/upload">Buch hochladen</a>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
if len(books) == 0 {
|
if len(books) == 0 {
|
||||||
<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>
|
<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>
|
||||||
|
|||||||
+297
-97
@@ -70,7 +70,7 @@ func Layout(title string, backHref string) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h1><a href=\"/\">eBook Library</a></h1></div></header><main class=\"container\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h1><a href=\"/\">eBook Library</a></h1><form method=\"post\" action=\"/logout\" style=\"margin-left:auto\"><button type=\"submit\" class=\"btn-logout\">Abmelden</button></form></div></header><main class=\"container\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,196 @@ func Layout(title string, backHref string) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoginLayout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var4 == nil {
|
||||||
|
templ_7745c5c3_Var4 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<!doctype html><html lang=\"de\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 42, Col: 18}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</title><link rel=\"stylesheet\" href=\"/static/styles.css\"></head><body><main class=\"container\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var4.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</main></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginPage(errMsg string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var6 == nil {
|
||||||
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<section class=\"login-box\"><h2>Anmelden</h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errMsg != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"error\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 58, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<form method=\"post\" action=\"/login\" class=\"login-form\"><label for=\"username\">Benutzername</label> <input id=\"username\" type=\"text\" name=\"username\" required autofocus> <label for=\"password\">Passwort</label> <input id=\"password\" type=\"password\" name=\"password\" required> <button type=\"submit\" class=\"btn\">Anmelden</button></form></section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = LoginLayout("Anmelden").Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadPage(errMsg string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var9 == nil {
|
||||||
|
templ_7745c5c3_Var9 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section class=\"page-head\"><h2>Buch hochladen</h2></section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errMsg != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p class=\"error\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 77, Col: 31}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " <form method=\"post\" action=\"/upload\" enctype=\"multipart/form-data\" class=\"upload-form\"><label for=\"book\">EPUB oder PDF auswählen</label> <input id=\"book\" type=\"file\" name=\"book\" accept=\".epub,.pdf\" required> <button type=\"submit\" class=\"btn\">Hochladen</button></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Buch hochladen", "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func BookCover(book library.Book, detail bool) templ.Component {
|
func BookCover(book library.Book, detail bool) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
@@ -102,93 +292,93 @@ func BookCover(book library.Book, detail bool) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var4 == nil {
|
if templ_7745c5c3_Var12 == nil {
|
||||||
templ_7745c5c3_Var4 = templ.NopComponent
|
templ_7745c5c3_Var12 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
if book.HasCover {
|
if book.HasCover {
|
||||||
var templ_7745c5c3_Var5 = []any{coverClass(detail)}
|
var templ_7745c5c3_Var13 = []any{coverClass(detail)}
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<img class=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<img class=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var13).String())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" src=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" src=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var15 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/cover/" + book.ID)
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/cover/" + book.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 35, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 89, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" alt=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var16 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("Cover von " + book.Title)
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("Cover von " + book.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 35, Col: 97}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 89, Col: 97}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" loading=\"lazy\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" loading=\"lazy\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var templ_7745c5c3_Var9 = []any{placeholderClass(detail)}
|
var templ_7745c5c3_Var17 = []any{placeholderClass(detail)}
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var18 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 1, Col: 0}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" aria-hidden=\"true\"><span>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" aria-hidden=\"true\"><span>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var19 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ToUpper(book.Format))
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ToUpper(book.Format))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 38, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 92, Col: 42}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -197,7 +387,7 @@ func BookCover(book library.Book, detail bool) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func IndexPage(books []library.Book) templ.Component {
|
func IndexPage(books []library.Book, canUpload bool) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@@ -213,12 +403,12 @@ func IndexPage(books []library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var12 == nil {
|
if templ_7745c5c3_Var20 == nil {
|
||||||
templ_7745c5c3_Var12 = templ.NopComponent
|
templ_7745c5c3_Var20 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var13 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
@@ -230,35 +420,45 @@ func IndexPage(books []library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<section class=\"page-head\"><h2>Meine Bücher</h2><p class=\"page-subtitle\">Titel, Autor und Cover auf einen Blick.</p></section>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<section class=\"page-head\"><h2>Meine Bücher</h2><p class=\"page-subtitle\">Titel, Autor und Cover auf einen Blick.</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if canUpload {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a class=\"btn\" href=\"/upload\">Buch hochladen</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</section>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if len(books) == 0 {
|
if len(books) == 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<ul class=\"book-list\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<ul class=\"book-list\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
for _, b := range books {
|
for _, b := range books {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<li class=\"book-item\"><a class=\"book-card\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<li class=\"book-item\"><a class=\"book-card\" href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var14 templ.SafeURL
|
var templ_7745c5c3_Var22 templ.SafeURL
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/book/" + b.ID)
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs("/book/" + b.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 55, Col: 55}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 112, Col: 55}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"><div class=\"book-card-cover\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><div class=\"book-card-cover\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -266,58 +466,58 @@ func IndexPage(books []library.Book) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"book-card-body\"><strong class=\"book-title\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><div class=\"book-card-body\"><strong class=\"book-title\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var15 string
|
var templ_7745c5c3_Var23 string
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(b.Title)
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 60, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 117, Col: 52}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</strong> <span class=\"book-author\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</strong> <span class=\"book-author\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var16 string
|
var templ_7745c5c3_Var24 string
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(b.Author)
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(b.Author)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 61, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 118, Col: 52}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> <span class=\"format\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span> <span class=\"format\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var17 string
|
var templ_7745c5c3_Var25 string
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 62, Col: 47}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 119, Col: 47}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span></div></a></li>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div></a></li>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</ul>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ul>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
templ_7745c5c3_Err = Layout("Bibliothek", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = Layout("Bibliothek", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -341,12 +541,12 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var18 == nil {
|
if templ_7745c5c3_Var26 == nil {
|
||||||
templ_7745c5c3_Var18 = templ.NopComponent
|
templ_7745c5c3_Var26 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var19 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
@@ -358,7 +558,7 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<article class=\"book-detail\"><div class=\"book-detail-cover\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<article class=\"book-detail\"><div class=\"book-detail-cover\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -366,78 +566,78 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><div class=\"book-detail-body\"><h2>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div><div class=\"book-detail-body\"><h2>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var20 string
|
var templ_7745c5c3_Var28 string
|
||||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 79, Col: 24}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 136, Col: 24}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</h2><p class=\"book-author-detail\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h2><p class=\"book-author-detail\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var21 string
|
var templ_7745c5c3_Var29 string
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 80, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 137, Col: 51}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p><dl class=\"book-meta\"><div><dt>Datei</dt><dd>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</p><dl class=\"book-meta\"><div><dt>Datei</dt><dd>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var22 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 84, Col: 31}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 141, Col: 31}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</dd></div><div><dt>Format</dt><dd>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</dd></div><div><dt>Format</dt><dd>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var23 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 88, Col: 29}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 145, Col: 29}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</dd></div></dl><p><a class=\"btn\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</dd></div></dl><p><a class=\"btn\" href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var24 templ.SafeURL
|
var templ_7745c5c3_Var32 templ.SafeURL
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + book.ID)
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + book.ID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 91, Col: 55}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 148, Col: 55}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">Auf Tolino herunterladen</a></p></div></article>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">Auf Tolino herunterladen</a></p></div></article>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var19), templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user