feat(dev): add local auth bypass mode
Some checks failed
continuous-integration/drone/pr Build is failing
Some checks failed
continuous-integration/drone/pr Build is failing
This commit is contained in:
21
AGENTS.md
21
AGENTS.md
@@ -43,6 +43,7 @@ Regenerate:
|
|||||||
### Common commands
|
### Common commands
|
||||||
- Dev server: `make dev`
|
- Dev server: `make dev`
|
||||||
- Direct dev run: `CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve`
|
- 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`
|
- Tests: `make tests`
|
||||||
- Tailwind asset build: `make build_tailwind`
|
- Tailwind asset build: `make build_tailwind`
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ Regenerate:
|
|||||||
- Root Tailwind output is built to `assets/style.css`.
|
- 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.
|
- 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.
|
- 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
|
## 5) Frontend
|
||||||
|
|
||||||
@@ -63,7 +65,24 @@ For frontend-specific implementation notes and commands, also read:
|
|||||||
- Frontend API client: `cd frontend && bun run generate:api`
|
- Frontend API client: `cd frontend && bun run generate:api`
|
||||||
- SQLC: `sqlc generate`
|
- 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.
|
After completing a task, update this `AGENTS.md` if you learned something general that would help future agents.
|
||||||
|
|
||||||
|
|||||||
58
api/auth.go
58
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) {
|
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)
|
session := sessions.Default(c)
|
||||||
|
|
||||||
// Check Session First
|
// Check Session First
|
||||||
@@ -89,6 +127,16 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) authOPDSMiddleware(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"`)
|
c.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||||
|
|
||||||
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
||||||
@@ -113,6 +161,16 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) authWebAppMiddleware(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)
|
session := sessions.Default(c)
|
||||||
|
|
||||||
// Check Session
|
// Check Session
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"reichard.io/antholume/config"
|
"reichard.io/antholume/config"
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
)
|
)
|
||||||
@@ -28,6 +29,10 @@ func NewServer(db *database.DBManager, cfg *config.Config, assets fs.FS) *Server
|
|||||||
assets: assets,
|
assets: assets,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.DisableAuth {
|
||||||
|
log.Warn("DISABLE_AUTH is set — all API requests will bypass authentication")
|
||||||
|
}
|
||||||
|
|
||||||
// Create strict handler with authentication middleware
|
// Create strict handler with authentication middleware
|
||||||
strictHandler := NewStrictHandler(s, []StrictMiddlewareFunc{s.authMiddleware})
|
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)
|
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)
|
auth, ok := s.getSession(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Write 401 response directly
|
// 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
|
// GetInfo returns server information
|
||||||
func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) {
|
func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) {
|
||||||
return GetInfo200JSONResponse{
|
return GetInfo200JSONResponse{
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ type Config struct {
|
|||||||
RegistrationEnabled bool
|
RegistrationEnabled bool
|
||||||
SearchEnabled bool
|
SearchEnabled bool
|
||||||
DemoMode bool
|
DemoMode bool
|
||||||
|
DisableAuth bool
|
||||||
|
DisableAuthUser string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
|
||||||
// Cookie Settings
|
// Cookie Settings
|
||||||
@@ -63,6 +65,8 @@ func Load() *Config {
|
|||||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
||||||
RegistrationEnabled: trimLowerString(getEnv("REGISTRATION_ENABLED", "false")) == "true",
|
RegistrationEnabled: trimLowerString(getEnv("REGISTRATION_ENABLED", "false")) == "true",
|
||||||
DemoMode: trimLowerString(getEnv("DEMO_MODE", "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",
|
SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true",
|
||||||
CookieAuthKey: trimLowerString(getEnv("COOKIE_AUTH_KEY", "")),
|
CookieAuthKey: trimLowerString(getEnv("COOKIE_AUTH_KEY", "")),
|
||||||
CookieEncKey: trimLowerString(getEnv("COOKIE_ENC_KEY", "")),
|
CookieEncKey: trimLowerString(getEnv("COOKIE_ENC_KEY", "")),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.31.1
|
||||||
|
|
||||||
package database
|
package database
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.31.1
|
||||||
|
|
||||||
package database
|
package database
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
bun
|
bun
|
||||||
nodejs
|
nodejs
|
||||||
tailwindcss
|
tailwindcss
|
||||||
|
typescript-language-server
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PATH=$PATH:~/go/bin
|
export PATH=$PATH:~/go/bin
|
||||||
|
|||||||
@@ -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.
|
- `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.
|
- 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.
|
After completing a frontend task, update this file if you learned something general that would help future frontend agents.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
|
allowedHosts: ['lin-va-terminal'],
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8585',
|
target: 'http://localhost:8585',
|
||||||
|
|||||||
Reference in New Issue
Block a user