feat: Phase 1 frontend - auth UI with KITHARA_MODE switch

- Auth API client (src/api/auth.ts): login, register, me, api-key endpoints
- AuthContext: React context managing auth state, mode detection, JWT tokens
- LoginPage: clean dark-themed login form with error handling
- RegisterPage: invite code + registration with password validation
- App.tsx: BrowserRouter + AuthProvider wrapper, mode-conditional routing
  - Single mode: straight to app, no auth required
  - Multi mode: redirect to /login, auth-gated access
- main.tsx: BrowserRouter + AuthProvider initialization
- react-router-dom added as dependency
- docker-compose.yml: KITHARA_MODE, KITHARA_ADMIN_USER, KITHARA_ADMIN_PASS env vars
- requirements.txt: pinned bcrypt==4.0.1 for passlib compatibility
- Tested: multi-mode admin bootstrap, JWT login, auth/me round-trip all confirmed
This commit is contained in:
rin.kagamine committed 2026-05-31 12:15:13 -04:00
1 parent b3f273942e
commit c571e28725
10 files changed
+523 -18

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
const API_BASE = '/api/auth'
export interface AuthUser {
id: string
username: string
display_name: string | null
role: string
has_api_key: boolean
}
export interface AuthMeResponse {
mode: 'single' | 'multi'
user: AuthUser | null
}
export interface LoginResponse {
access_token: string
token_type: string
user: AuthUser
}
export interface RegisterResponse {
access_token: string
token_type: string
user: AuthUser
}
const TOKEN_KEY = 'kithara_token'
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token)
}
export function clearToken(): void {
localStorage.removeItem(TOKEN_KEY)
}
export async function fetchMe(): Promise<AuthMeResponse> {
const token = getToken()
const headers: Record<string, string> = {}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const res = await fetch(`${API_BASE}/me`, { headers })
if (!res.ok) throw new Error('Failed to fetch auth status')
return res.json()
}
export async function login(username: string, password: string): Promise<LoginResponse> {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.detail || 'Login failed')
}
return res.json()
}
export async function register(username: string, password: string, invite_code: string, display_name?: string): Promise<RegisterResponse> {
const res = await fetch(`${API_BASE}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, invite_code, display_name }),
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.detail || 'Registration failed')
}
return res.json()
}
export async function updateApiKey(apiKey: string, token: string): Promise<{ message: string }> {
const res = await fetch(`${API_BASE}/api-key`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ api_key: apiKey }),
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.detail || 'Failed to update API key')
}
return res.json()
}