From a950d50440d8e8407bdb77bebc0d131bb13747ac Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Sat, 2 May 2026 15:32:48 -0400 Subject: [PATCH] feat(dev): add local auth bypass mode --- AGENTS.md | 21 ++++++++++++++- api/auth.go | 58 +++++++++++++++++++++++++++++++++++++++++ api/v1/server.go | 41 +++++++++++++++++++++++++++++ config/config.go | 4 +++ database/db.go | 2 +- database/models.go | 2 +- flake.nix | 1 + frontend/AGENTS.md | 13 ++++++++- frontend/package.json | 2 +- frontend/vite.config.ts | 1 + 10 files changed, 140 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 329ca39..d71d904 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,7 @@ Regenerate: ### Common commands - Dev server: `make dev` - Direct dev run: `CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve` +- No-auth dev run: `CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true DISABLE_AUTH=true DISABLE_AUTH_USER=evan go run main.go serve` - Tests: `make tests` - Tailwind asset build: `make build_tailwind` @@ -51,6 +52,7 @@ Regenerate: - Root Tailwind output is built to `assets/style.css`. - Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both. - SQLite timestamps are stored as RFC3339 strings (usually with a trailing `Z`); prefer `parseTime` / `parseTimePtr` instead of ad-hoc `time.Parse` layouts. +- `DISABLE_AUTH=true` bypasses authentication on **all** routes (v1 API, legacy web app, KOSync, OPDS). Set `DISABLE_AUTH_USER=` to control which database user the session impersonates (defaults to the first user in the DB). The user must already exist. ## 5) Frontend @@ -63,7 +65,24 @@ For frontend-specific implementation notes and commands, also read: - Frontend API client: `cd frontend && bun run generate:api` - SQLC: `sqlc generate` -## 7) Updating This File +## 7) Live Dev Server Debugging + +- The Vite dev server runs on `localhost:5173` and proxies `/api` to the Go backend on `localhost:8585`. +- Use `glimpse` to interact with the running frontend for visual debugging: + ```bash + # Snapshot rendered page state (text, links, forms, buttons) + glimpse snapshot http://localhost:5173/some-page --wait-until=complete --timeout=15000 + + # Screenshot for visual inspection + glimpse screenshot http://localhost:5173/some-page --wait-until=complete --output=_scratch/page.png + + # Execute JS in the browser context (e.g. fill forms, click buttons, read state) + glimpse exec http://localhost:5173/some-page --wait-until=complete --timeout=20000 --js='return document.title' + ``` +- Use `curl` for direct API testing (both `localhost:5173` via Vite proxy and `localhost:8585` directly work). +- **Caveat:** Monkey-patching `window.fetch` inside `glimpse exec` breaks in Firefox with `TypeError: 'fetch' called on an object that does not implement interface Window.`. Avoid fetch interception; instead test API calls separately with `curl`. + +## 8) Updating This File After completing a task, update this `AGENTS.md` if you learned something general that would help future agents. diff --git a/api/auth.go b/api/auth.go index e3fa854..3a10845 100644 --- a/api/auth.go +++ b/api/auth.go @@ -49,7 +49,45 @@ func (api *API) authorizeCredentials(ctx context.Context, username string, passw } } +// resolveDevAuth returns an authData for the dev user when DISABLE_AUTH is +// set. If DISABLE_AUTH_USER names a specific user, that user is looked up; +// otherwise the first user in the database is used. +func (api *API) resolveDevAuth(c *gin.Context) (authData, bool) { + if api.cfg.DisableAuthUser != "" { + user, err := api.db.Queries.GetUser(c, api.cfg.DisableAuthUser) + if err != nil { + log.Errorf("DISABLE_AUTH_USER=%q not found in database: %v", api.cfg.DisableAuthUser, err) + return authData{}, false + } + return authData{ + UserName: user.ID, + IsAdmin: user.Admin, + AuthHash: *user.AuthHash, + }, true + } + + users, err := api.db.Queries.GetUsers(c) + if err != nil || len(users) == 0 { + return authData{}, false + } + return authData{ + UserName: users[0].ID, + IsAdmin: users[0].Admin, + AuthHash: *users[0].AuthHash, + }, true +} + func (api *API) authKOMiddleware(c *gin.Context) { + // Dev Auth Bypass + if api.cfg.DisableAuth { + if auth, ok := api.resolveDevAuth(c); ok { + c.Set("Authorization", auth) + c.Header("Cache-Control", "private") + c.Next() + return + } + } + session := sessions.Default(c) // Check Session First @@ -89,6 +127,16 @@ func (api *API) authKOMiddleware(c *gin.Context) { } func (api *API) authOPDSMiddleware(c *gin.Context) { + // Dev Auth Bypass + if api.cfg.DisableAuth { + if auth, ok := api.resolveDevAuth(c); ok { + c.Set("Authorization", auth) + c.Header("Cache-Control", "private") + c.Next() + return + } + } + c.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) user, rawPassword, hasAuth := c.Request.BasicAuth() @@ -113,6 +161,16 @@ func (api *API) authOPDSMiddleware(c *gin.Context) { } func (api *API) authWebAppMiddleware(c *gin.Context) { + // Dev Auth Bypass + if api.cfg.DisableAuth { + if auth, ok := api.resolveDevAuth(c); ok { + c.Set("Authorization", auth) + c.Header("Cache-Control", "private") + c.Next() + return + } + } + session := sessions.Default(c) // Check Session diff --git a/api/v1/server.go b/api/v1/server.go index 89dc59a..0ca3708 100644 --- a/api/v1/server.go +++ b/api/v1/server.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" + log "github.com/sirupsen/logrus" "reichard.io/antholume/config" "reichard.io/antholume/database" ) @@ -28,6 +29,10 @@ func NewServer(db *database.DBManager, cfg *config.Config, assets fs.FS) *Server assets: assets, } + if cfg.DisableAuth { + log.Warn("DISABLE_AUTH is set — all API requests will bypass authentication") + } + // Create strict handler with authentication middleware strictHandler := NewStrictHandler(s, []StrictMiddlewareFunc{s.authMiddleware}) @@ -51,6 +56,22 @@ func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) S return handler(ctx, w, r, request) } + // Dev Auth Bypass - Inject an admin session when DISABLE_AUTH is set. + // This avoids repeated logins during local development. Uses the + // first user in the database so that DB queries using the user ID + // return real data. + if s.cfg.DisableAuth { + devAuth, ok := s.resolveDevAuth(ctx) + if !ok { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + json.NewEncoder(w).Encode(ErrorResponse{Code: 500, Message: "DISABLE_AUTH: no users in database; register one first"}) + return nil, nil + } + ctx = context.WithValue(ctx, "auth", devAuth) + return handler(ctx, w, r, request) + } + auth, ok := s.getSession(r) if !ok { // Write 401 response directly @@ -89,6 +110,26 @@ func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) S } } +// resolveDevAuth determines the dev user identity when DISABLE_AUTH is set. +// If DISABLE_AUTH_USER is specified, that user is looked up; otherwise the +// first user in the database is used. +func (s *Server) resolveDevAuth(ctx context.Context) (authData, bool) { + if s.cfg.DisableAuthUser != "" { + user, err := s.db.Queries.GetUser(ctx, s.cfg.DisableAuthUser) + if err != nil { + log.Errorf("DISABLE_AUTH_USER=%q not found in database: %v", s.cfg.DisableAuthUser, err) + return authData{}, false + } + return authData{UserName: user.ID, IsAdmin: user.Admin}, true + } + + users, err := s.db.Queries.GetUsers(ctx) + if err != nil || len(users) == 0 { + return authData{}, false + } + return authData{UserName: users[0].ID, IsAdmin: users[0].Admin}, true +} + // GetInfo returns server information func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) { return GetInfo200JSONResponse{ diff --git a/config/config.go b/config/config.go index 30a6355..a2a0906 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,8 @@ type Config struct { RegistrationEnabled bool SearchEnabled bool DemoMode bool + DisableAuth bool + DisableAuthUser string LogLevel string // Cookie Settings @@ -63,6 +65,8 @@ func Load() *Config { DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")), RegistrationEnabled: trimLowerString(getEnv("REGISTRATION_ENABLED", "false")) == "true", DemoMode: trimLowerString(getEnv("DEMO_MODE", "false")) == "true", + DisableAuth: trimLowerString(getEnv("DISABLE_AUTH", "false")) == "true", + DisableAuthUser: strings.TrimSpace(getEnv("DISABLE_AUTH_USER", "")), SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true", CookieAuthKey: trimLowerString(getEnv("COOKIE_AUTH_KEY", "")), CookieEncKey: trimLowerString(getEnv("COOKIE_ENC_KEY", "")), diff --git a/database/db.go b/database/db.go index ef3e100..9f7a5a6 100644 --- a/database/db.go +++ b/database/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package database diff --git a/database/models.go b/database/models.go index 070db00..a0e0680 100644 --- a/database/models.go +++ b/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package database diff --git a/flake.nix b/flake.nix index c521d51..104858b 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,7 @@ bun nodejs tailwindcss + typescript-language-server ]; shellHook = '' export PATH=$PATH:~/go/bin diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 397c290..4d9fec9 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -64,7 +64,18 @@ Also follow the repository root guide at `../AGENTS.md`. - `bun run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build. - When possible, validate changed files directly before escalating to full-project fixes. -## 7) Updating This File +## 7) Live Dev Server Debugging + +- Use `glimpse` to inspect the running Vite dev server at `localhost:5173`: + ```bash + glimpse snapshot http://localhost:5173/some-page --wait-until=complete --timeout=15000 + glimpse screenshot http://localhost:5173/some-page --wait-until=complete --output=_scratch/page.png + glimpse exec http://localhost:5173/some-page --wait-until=complete --timeout=20000 --js='return document.title' + ``` +- Use `curl` for API endpoint testing (both `localhost:5173` via proxy and `localhost:8585` directly). +- Do not monkey-patch `window.fetch` in `glimpse exec`; Firefox rejects it. Test API calls with `curl` instead. + +## 8) Updating This File After completing a frontend task, update this file if you learned something general that would help future frontend agents. diff --git a/frontend/package.json b/frontend/package.json index ceb1634..28ec7e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "typecheck": "tsc --noEmit", "build": "tsc && vite build", "preview": "vite preview", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8e1c140..24f592c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { + allowedHosts: ['lin-va-terminal'], proxy: { '/api': { target: 'http://localhost:8585',