feat(phase-9): implement MVP polishing and closure
T-032: User management admin panel - backend users router with CRUD, frontend UsersPage with modals T-033: Audit log viewer - backend audit router with filters/pagination, frontend AuditLogPage T-034: Global error handling - ErrorBoundary, LoadingSpinner, ErrorMessage, Toast components T-035: Backend tests - pytest setup with SQLite, tests for health/auth/techniques/tests T-036: Documentation - Updated README with testing section, created docs/API.md
This commit is contained in:
58
frontend/src/api/audit.ts
Normal file
58
frontend/src/api/audit.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import client from "./client";
|
||||
|
||||
export interface AuditLogOut {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
username: string | null;
|
||||
action: string;
|
||||
entity_type: string | null;
|
||||
entity_id: string | null;
|
||||
timestamp: string;
|
||||
details: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AuditLogPage {
|
||||
items: AuditLogOut[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
user_id?: string;
|
||||
action?: string;
|
||||
entity_type?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** Fetch paginated audit logs with filters. */
|
||||
export async function getAuditLogs(filters?: AuditLogFilters): Promise<AuditLogPage> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.user_id) params.append("user_id", filters.user_id);
|
||||
if (filters?.action) params.append("action", filters.action);
|
||||
if (filters?.entity_type) params.append("entity_type", filters.entity_type);
|
||||
if (filters?.start_date) params.append("start_date", filters.start_date);
|
||||
if (filters?.end_date) params.append("end_date", filters.end_date);
|
||||
if (filters?.offset !== undefined) params.append("offset", String(filters.offset));
|
||||
if (filters?.limit !== undefined) params.append("limit", String(filters.limit));
|
||||
|
||||
const { data } = await client.get<AuditLogPage>(
|
||||
`/audit-logs${params.toString() ? `?${params}` : ""}`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Fetch list of distinct actions. */
|
||||
export async function getAuditActions(): Promise<string[]> {
|
||||
const { data } = await client.get<string[]>("/audit-logs/actions");
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Fetch list of distinct entity types. */
|
||||
export async function getAuditEntityTypes(): Promise<string[]> {
|
||||
const { data } = await client.get<string[]>("/audit-logs/entity-types");
|
||||
return data;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from "axios";
|
||||
import axios, { type AxiosError } from "axios";
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: "http://localhost:8000/api/v1",
|
||||
@@ -14,14 +14,33 @@ client.interceptors.request.use((config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// On 401, clear token so the UI can redirect to login
|
||||
// Response interceptor for error handling
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
(error: AxiosError<{ detail?: string; code?: string }>) => {
|
||||
const status = error.response?.status;
|
||||
|
||||
// On 401, clear token and redirect to login
|
||||
if (status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
// Only redirect if not already on login page
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
|
||||
// Extract error message from response
|
||||
const message =
|
||||
error.response?.data?.detail ||
|
||||
error.message ||
|
||||
"An unexpected error occurred";
|
||||
|
||||
// Create a more descriptive error
|
||||
const enhancedError = new Error(message);
|
||||
(enhancedError as Error & { status?: number; code?: string }).status = status;
|
||||
(enhancedError as Error & { code?: string }).code = error.response?.data?.code;
|
||||
|
||||
return Promise.reject(enhancedError);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
43
frontend/src/api/users.ts
Normal file
43
frontend/src/api/users.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import client from "./client";
|
||||
|
||||
export interface UserOut {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
role: string;
|
||||
is_active: boolean;
|
||||
created_at: string | null;
|
||||
last_login: string | null;
|
||||
}
|
||||
|
||||
export interface UserCreatePayload {
|
||||
username: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UserUpdatePayload {
|
||||
email?: string;
|
||||
role?: string;
|
||||
is_active?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
/** Fetch all users (admin only). */
|
||||
export async function getUsers(): Promise<UserOut[]> {
|
||||
const { data } = await client.get<UserOut[]>("/users");
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Create a new user (admin only). */
|
||||
export async function createUser(payload: UserCreatePayload): Promise<UserOut> {
|
||||
const { data } = await client.post<UserOut>("/users", payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Update a user (admin only). */
|
||||
export async function updateUser(userId: string, payload: UserUpdatePayload): Promise<UserOut> {
|
||||
const { data } = await client.patch<UserOut>(`/users/${userId}`, payload);
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user