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:
2026-08-11 19:11:40 +02:00
co-authored by Copilot
parent a8b31dae8a
commit a66c370de4
13 changed files with 1115 additions and 111 deletions
+51
View File
@@ -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)
}