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:
1 parent
b3f273942e
commit
c571e28725
10 files changed
+523
-18
No files matched your search
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user