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
+6 -4
View File
@@ -17,7 +17,7 @@ func sessionFrom(r *http.Request) *users.Session {
return s
}
// requireAuth is middleware that redirects unauthenticated requests to /login.
// requireAuth 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)
@@ -30,11 +30,13 @@ func (h *Handler) requireAuth(next http.HandlerFunc) http.HandlerFunc {
}
}
// requireUpload is middleware that returns 403 if the user lacks upload permission.
func (h *Handler) requireUpload(next http.HandlerFunc) http.HandlerFunc {
// requireRole returns middleware that allows access only to users whose role is
// at least minRole. Unknown sessions are redirected to /login; insufficient
// role yields 403.
func (h *Handler) requireRole(minRole users.Role, next http.HandlerFunc) http.HandlerFunc {
return h.requireAuth(func(w http.ResponseWriter, r *http.Request) {
sess := sessionFrom(r)
if !sess.CanUpload {
if !sess.Role.AtLeast(minRole) {
http.Error(w, "Keine Berechtigung", http.StatusForbidden)
return
}