refactor: replace can_upload boolean with role-based access control

Introduces a three-tier role hierarchy: reader < uploader < admin.

- internal/users/role.go: Role type, roleLevel map, AtLeast(min),
  ParseRole() — new roles added by inserting into roleLevel only
- internal/users/store.go: versioned migrations via _schema_version table;
  v2 migration adds 'role' column and migrates existing can_upload data;
  SetRole() replaces SetUpload(); Session carries Role instead of CanUpload
- internal/web/middleware.go: generic requireRole(minRole) middleware
  replaces the ad-hoc requireUpload
- internal/web/handlers.go: upload routes use requireRole(RoleUploader);
  listBooks derives canUpload from sess.Role.AtLeast(RoleUploader)
- cmd/admin/main.go: user add --role <reader|uploader|admin>,
  user set-role replaces user set-upload
- README, copilot-instructions updated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-11 19:43:39 +02:00
co-authored by Copilot
parent a66c370de4
commit 817763887a
7 changed files with 201 additions and 107 deletions
+32 -25
View File
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
"syscall"
@@ -40,10 +39,20 @@ func handleUser(store *users.Store, action string, args []string) {
switch action {
case "add":
if len(args) < 1 {
fatalf("usage: user add <username> [--upload]\n")
fatalf("usage: user add <username> [--role reader|uploader|admin]\n")
}
username := args[0]
canUpload := len(args) > 1 && args[1] == "--upload"
role := users.RoleReader
if len(args) > 1 && args[1] == "--role" {
if len(args) < 3 {
fatalf("--role erwartet einen Wert: reader, uploader oder admin\n")
}
r, err := users.ParseRole(args[2])
if err != nil {
fatalf("%v\n", err)
}
role = r
}
password, err := readPassword(fmt.Sprintf("Passwort für '%s': ", username))
if err != nil {
@@ -57,11 +66,11 @@ func handleUser(store *users.Store, action string, args []string) {
fatalf("Passwörter stimmen nicht überein.\n")
}
u, err := store.CreateUser(username, password, canUpload)
u, err := store.CreateUser(username, password, role)
if err != nil {
fatalf("user anlegen: %v\n", err)
}
fmt.Printf("✓ Benutzer '%s' angelegt (upload: %s)\n", u.Username, yesNo(u.CanUpload))
fmt.Printf("✓ Benutzer '%s' angelegt (rolle: %s)\n", u.Username, u.Role)
case "list":
list, err := store.ListUsers()
@@ -72,10 +81,10 @@ func handleUser(store *users.Store, action string, args []string) {
fmt.Println("Keine Benutzer vorhanden.")
return
}
fmt.Printf("%-20s %s\n", "BENUTZERNAME", "UPLOAD")
fmt.Printf("%-20s %s\n", "BENUTZERNAME", "ROLLE")
fmt.Println(strings.Repeat("-", 32))
for _, u := range list {
fmt.Printf("%-20s %s\n", u.Username, yesNo(u.CanUpload))
fmt.Printf("%-20s %s\n", u.Username, u.Role)
}
case "delete":
@@ -87,18 +96,18 @@ func handleUser(store *users.Store, action string, args []string) {
}
fmt.Printf("✓ Benutzer '%s' gelöscht.\n", args[0])
case "set-upload":
case "set-role":
if len(args) < 2 {
fatalf("usage: user set-upload <username> <true|false>\n")
fatalf("usage: user set-role <username> <reader|uploader|admin>\n")
}
val, err := strconv.ParseBool(args[1])
role, err := users.ParseRole(args[1])
if err != nil {
fatalf("ungültiger Wert '%s', erwartet true oder false\n", args[1])
fatalf("%v\n", err)
}
if err := store.SetUpload(args[0], val); err != nil {
fatalf("upload-recht setzen: %v\n", err)
if err := store.SetRole(args[0], role); err != nil {
fatalf("rolle setzen: %v\n", err)
}
fmt.Printf("✓ Upload-Recht für '%s' auf %s gesetzt.\n", args[0], yesNo(val))
fmt.Printf("✓ Rolle für '%s' auf '%s' gesetzt.\n", args[0], role)
default:
usage()
@@ -116,13 +125,6 @@ func readPassword(prompt string) (string, error) {
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
@@ -138,11 +140,16 @@ func fatalf(format string, args ...any) {
func usage() {
fmt.Fprintln(os.Stderr, `ebooks-admin Benutzerverwaltung
Rollen (aufsteigend):
reader Lesen und Herunterladen
uploader Zusätzlich: Bücher hochladen
admin Alle Rechte
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
user add <name> [--role reader|uploader|admin] Benutzer anlegen (Standard: reader)
user list Alle Benutzer auflisten
user delete <name> Benutzer löschen
user set-role <name> <reader|uploader|admin> Rolle ändern
Umgebungsvariablen:
USERS_DB Pfad zur SQLite-Datenbank (Standard: users.db)`)