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,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)`)
|
||||
}
|
||||
Reference in New Issue
Block a user