Files
bookhoard/internal/router/auth.go
T
John O'Keefe 431fec05ad feat(security): library visibility enforced at the source; per-library series; annotation ownership; admin user creation
The family-lens hardening: library_visibility was only enforced where
file bytes flowed (download/reader/OPDS) — metadata surfaces leaked
hidden libraries to any authenticated account, and registration was
open by default with no admin-side user creation.

- Series are grouped PER LIBRARY now (same name in two libraries =
  two entries: manga adaptations / LN splits no longer merge);
  /api/series aggregates only the requester's VISIBLE libraries and
  /api/series/books REQUIRES a visible library_id (breaking contract;
  web + app clients updated in the same change).
- /api/media-items, item detail, and the reader content endpoints
  (chapters/panels/outline/THUMBNAILS — page images leaked unguarded
  next to a gated GetPage) all enforce the visible set; resolveLibrary
  no longer honors a hand-typed library_id outside the visible set.
- Annotation/note/bookmark mutations (update/delete/tombstone, sync
  LWW paths incl. conflict resolution) carry ownership predicates:
  another account's bare row id is a 404, not a silent delete.
- Admin user creation: POST /api/auth/users (AdminMiddleware) + a
  Create User card in the admin Users UI — accounts stay creatable
  with self-service registration OFF, which is now the schema default
  (fresh installs; the first-admin bootstrap exception is unchanged).
- Tests: series contract re-pinned (per-library, visibility 403s),
  new ownership + admin-create integration suites, OPDS all=1 test
  now passes an explicit page size (shared-DB growth made the fresh
  books fall off page 1), device-cap helper switched to the admin
  endpoint.
2026-10-13 12:03:01 -04:00

49 lines
1.7 KiB
Go

package router
import (
"bookhoard/internal/handlers"
"github.com/labstack/echo/v5"
)
func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) {
e := cfg.Echo
// Auth routes (no auth required, but rate limited)
e.POST("/api/auth/register", rateLimitMiddleware(cfg.AuthHandler.Register))
e.POST("/api/auth/login", rateLimitMiddleware(cfg.AuthHandler.Login))
// JWT middleware for protected routes
jwtMiddleware := createJWTMiddleware(cfg)
// Create protected route group
protected := e.Group("/api", jwtMiddleware)
// Protected auth routes
protected.GET("/auth/profile", cfg.AuthHandler.GetProfile)
// Refresh token endpoint (no authentication required - uses refresh token from body)
e.POST("/api/auth/refresh", cfg.AuthHandler.RefreshAccessToken)
// Logout endpoint (optional authentication - can revoke tokens if provided)
e.POST("/api/auth/logout", cfg.AuthHandler.Logout)
// Auth update routes
authGroup := e.Group("/api/auth", createJWTMiddleware(cfg))
authGroup.PUT("/password", cfg.AuthHandler.UpdatePassword)
authGroup.PUT("/theme", cfg.AuthHandler.UpdateTheme)
// Profile management (combined handlers - self-edit)
authGroup.PUT("/profile", cfg.AuthHandler.UpdateProfile)
authGroup.DELETE("/profile", cfg.AuthHandler.DeleteUser)
// Admin-only routes (same handlers with URL param)
admin := protected.Group("/auth", handlers.AdminMiddleware)
admin.GET("/users", cfg.AuthHandler.ListUsers)
admin.POST("/users", cfg.AuthHandler.AdminCreateUser)
admin.PUT("/users/:id/max-devices", cfg.AuthHandler.UpdateUserMaxDevices)
admin.PUT("/profile/:id", cfg.AuthHandler.UpdateProfile)
admin.PUT("/password/:id", cfg.AuthHandler.UpdatePassword)
admin.DELETE("/profile/:id", cfg.AuthHandler.DeleteUser)
}