Feature 1: Local accounts, replacing shared token
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
- Local accounts with bcrypt password hashing; first-run setup via POST /api/setup - Personal API tokens (dmv_<48hex>, SHA-256 hashed at rest) for scripted access - Server-side in-memory sessions with 32-byte secure cookie (dockmv_session, 7-day TTL, sliding renewal) - Login rate limiting (exponential backoff 1s–30s cap) per IP - Refuse to bind non-loopback while no account exists, unless DOCKMV_TRUST_ADDR=1 (for Docker's port mapping) - Every account can manage every other account (no roles in v1) - Auth middleware: public-path allowlist (/api/setup, /api/login, /api/logout, /api/me, /api/health) + session cookie check + API token (X-Auth-Token or Authorization: Bearer) check - Frontend AuthGate gates app on GET /api/me; shows setup screen or login form or app tree as needed - Account tab for personal token management; sign-out button in topbar - Break: removed --token flag, DOCKMV_TOKEN env var, ?token= query param, /api/health no longer auto-responds when unauthenticated Verified: go build/vet clean, frontend tsc+vite clean. Sandbox cannot execute binaries to test setup→login→session→token flow at runtime; recommend manual pass before merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import App from './App'
|
||||
import { api, setUnauthorizedHandler } from './api'
|
||||
import { Login } from './Login'
|
||||
import type { Me } from './types'
|
||||
|
||||
/**
|
||||
* AuthGate decides, on load and after every 401, whether to show the setup
|
||||
* screen, a login form, or the app itself. It owns the one GET /api/me call
|
||||
* the app needs before it can render anything real.
|
||||
*/
|
||||
export function AuthGate() {
|
||||
const [me, setMe] = useState<Me | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setMe(await api.me())
|
||||
setError('')
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
// A 401 elsewhere in the app (an expired or revoked session) means the
|
||||
// server no longer considers us logged in; drop straight to the login form
|
||||
// instead of waiting for the next unrelated re-render to notice.
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => setMe((prev) => (prev ? { ...prev, authenticated: false } : prev)))
|
||||
return () => setUnauthorizedHandler(null)
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await api.logout().catch(() => undefined)
|
||||
await load()
|
||||
}, [load])
|
||||
|
||||
if (!me) {
|
||||
return <div className="empty">{error || 'loading…'}</div>
|
||||
}
|
||||
if (!me.authenticated) {
|
||||
return <Login needsSetup={!!me.needsSetup} onDone={load} />
|
||||
}
|
||||
return <App me={me} logout={logout} />
|
||||
}
|
||||
Reference in New Issue
Block a user