Files
PocketVeto/frontend/lib/api.ts
Jack Levy 5b73b60d9e Add multi-user auth system and admin panel
- User model with email/hashed_password/is_admin/notification_prefs
- JWT auth: POST /api/auth/register, /login, /me
- First registered user auto-promoted to admin
- Migration 0005: users table + user_id FK on follows (clears global follows)
- Follows, dashboard, settings, admin endpoints all require authentication
- Admin endpoints (settings writes, celery triggers) require is_admin
- Frontend: login/register pages, Zustand auth store (localStorage persist)
- AuthGuard component gates all app routes, shows app shell only when authed
- Sidebar shows user email + logout; Admin nav link visible to admins only
- Admin panel (/settings): user list with delete + promote/demote, LLM config,
  data source settings, and manual celery controls

Authored-By: Jack Levy
2026-02-28 21:44:34 -05:00

145 lines
4.2 KiB
TypeScript

import axios from "axios";
import type {
Bill,
BillAction,
BillDetail,
DashboardData,
Follow,
Member,
NewsArticle,
PaginatedResponse,
SettingsData,
TrendScore,
} from "./types";
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || "",
timeout: 30000,
});
// Attach JWT from localStorage on every request
apiClient.interceptors.request.use((config) => {
if (typeof window !== "undefined") {
try {
const stored = localStorage.getItem("pocketveto-auth");
if (stored) {
const { state } = JSON.parse(stored);
if (state?.token) {
config.headers.Authorization = `Bearer ${state.token}`;
}
}
} catch {
// ignore parse errors
}
}
return config;
});
interface AuthUser {
id: number;
email: string;
is_admin: boolean;
notification_prefs: Record<string, unknown>;
created_at: string;
}
interface TokenResponse {
access_token: string;
token_type: string;
user: AuthUser;
}
// Auth
export const authAPI = {
register: (email: string, password: string) =>
apiClient.post<TokenResponse>("/api/auth/register", { email, password }).then((r) => r.data),
login: (email: string, password: string) =>
apiClient.post<TokenResponse>("/api/auth/login", { email, password }).then((r) => r.data),
me: () =>
apiClient.get<AuthUser>("/api/auth/me").then((r) => r.data),
};
// Bills
export const billsAPI = {
list: (params?: Record<string, unknown>) =>
apiClient.get<PaginatedResponse<Bill>>("/api/bills", { params }).then((r) => r.data),
get: (id: string) =>
apiClient.get<BillDetail>(`/api/bills/${id}`).then((r) => r.data),
getActions: (id: string) =>
apiClient.get<BillAction[]>(`/api/bills/${id}/actions`).then((r) => r.data),
getNews: (id: string) =>
apiClient.get<NewsArticle[]>(`/api/bills/${id}/news`).then((r) => r.data),
getTrend: (id: string, days?: number) =>
apiClient.get<TrendScore[]>(`/api/bills/${id}/trend`, { params: { days } }).then((r) => r.data),
};
// Members
export const membersAPI = {
list: (params?: Record<string, unknown>) =>
apiClient.get<PaginatedResponse<Member>>("/api/members", { params }).then((r) => r.data),
get: (id: string) =>
apiClient.get<Member>(`/api/members/${id}`).then((r) => r.data),
getBills: (id: string, params?: Record<string, unknown>) =>
apiClient.get<PaginatedResponse<Bill>>(`/api/members/${id}/bills`, { params }).then((r) => r.data),
};
// Follows
export const followsAPI = {
list: () =>
apiClient.get<Follow[]>("/api/follows").then((r) => r.data),
add: (follow_type: string, follow_value: string) =>
apiClient.post<Follow>("/api/follows", { follow_type, follow_value }).then((r) => r.data),
remove: (id: number) =>
apiClient.delete(`/api/follows/${id}`),
};
// Dashboard
export const dashboardAPI = {
get: () =>
apiClient.get<DashboardData>("/api/dashboard").then((r) => r.data),
};
// Search
export const searchAPI = {
search: (q: string) =>
apiClient.get<{ bills: Bill[]; members: Member[] }>("/api/search", { params: { q } }).then((r) => r.data),
};
// Settings
export const settingsAPI = {
get: () =>
apiClient.get<SettingsData>("/api/settings").then((r) => r.data),
update: (key: string, value: string) =>
apiClient.put("/api/settings", { key, value }).then((r) => r.data),
testLLM: () =>
apiClient.post("/api/settings/test-llm").then((r) => r.data),
};
export interface AdminUser {
id: number;
email: string;
is_admin: boolean;
follow_count: number;
created_at: string;
}
// Admin
export const adminAPI = {
// Users
listUsers: () =>
apiClient.get<AdminUser[]>("/api/admin/users").then((r) => r.data),
deleteUser: (id: number) =>
apiClient.delete(`/api/admin/users/${id}`),
toggleAdmin: (id: number) =>
apiClient.patch<AdminUser>(`/api/admin/users/${id}/toggle-admin`).then((r) => r.data),
// Tasks
triggerPoll: () =>
apiClient.post("/api/admin/trigger-poll").then((r) => r.data),
triggerMemberSync: () =>
apiClient.post("/api/admin/trigger-member-sync").then((r) => r.data),
triggerTrendScores: () =>
apiClient.post("/api/admin/trigger-trend-scores").then((r) => r.data),
getTaskStatus: (taskId: string) =>
apiClient.get(`/api/admin/task-status/${taskId}`).then((r) => r.data),
};