feat(dev): add local auth bypass mode
Some checks failed
continuous-integration/drone/pr Build is failing

This commit is contained in:
2026-05-02 15:32:48 -04:00
parent 00faf9cea8
commit a950d50440
10 changed files with 140 additions and 5 deletions

View File

@@ -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=<username>` 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.

View File

@@ -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

View File

@@ -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{

View File

@@ -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", "")),

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.31.1
package database

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.31.1
package database

View File

@@ -27,6 +27,7 @@
bun
nodejs
tailwindcss
typescript-language-server
];
shellHook = ''
export PATH=$PATH:~/go/bin

View File

@@ -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.

View File

@@ -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",

View File

@@ -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',