Add React UI with Execution Explorer, auth, and standalone deployment
- Scaffold Vite + React + TypeScript frontend in ui/ with full design system (dark/light themes) matching the HTML mockups - Implement Execution Explorer page: search filters, results table with expandable processor tree and exchange detail sidebar, pagination - Add UI authentication: UiAuthController (login/refresh endpoints), JWT filter handles ui: subject prefix, CORS configuration - Shared components: StatusPill, DurationBar, StatCard, AppBadge, FilterChip, Pagination — all using CSS Modules with design tokens - API client layer: openapi-fetch with auth middleware, TanStack Query hooks for search/detail/snapshot queries, Zustand for state - Standalone deployment: Nginx Dockerfile, K8s Deployment + ConfigMap + NodePort (30080), runtime config.js for API base URL - Embedded mode: maven-resources-plugin copies ui/dist into JAR static resources, SPA forward controller for client-side routing - CI/CD: UI build step, Docker build/push for server-ui image, K8s deploy step for UI, UI credential secrets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
109
ui/src/auth/auth-store.ts
Normal file
109
ui/src/auth/auth-store.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { create } from 'zustand';
|
||||
import { config } from '../config';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
username: string | null;
|
||||
isAuthenticated: boolean;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
refresh: () => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
function loadTokens() {
|
||||
return {
|
||||
accessToken: localStorage.getItem('cameleer-access-token'),
|
||||
refreshToken: localStorage.getItem('cameleer-refresh-token'),
|
||||
username: localStorage.getItem('cameleer-username'),
|
||||
};
|
||||
}
|
||||
|
||||
function persistTokens(access: string, refresh: string, username: string) {
|
||||
localStorage.setItem('cameleer-access-token', access);
|
||||
localStorage.setItem('cameleer-refresh-token', refresh);
|
||||
localStorage.setItem('cameleer-username', username);
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
localStorage.removeItem('cameleer-access-token');
|
||||
localStorage.removeItem('cameleer-refresh-token');
|
||||
localStorage.removeItem('cameleer-username');
|
||||
}
|
||||
|
||||
const initial = loadTokens();
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
accessToken: initial.accessToken,
|
||||
refreshToken: initial.refreshToken,
|
||||
username: initial.username,
|
||||
isAuthenticated: !!initial.accessToken,
|
||||
error: null,
|
||||
loading: false,
|
||||
|
||||
login: async (username, password) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || 'Invalid credentials');
|
||||
}
|
||||
const { accessToken, refreshToken } = await res.json();
|
||||
persistTokens(accessToken, refreshToken, username);
|
||||
set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
username,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
set({
|
||||
error: e instanceof Error ? e.message : 'Login failed',
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: async () => {
|
||||
const { refreshToken } = get();
|
||||
if (!refreshToken) return false;
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
const username = get().username ?? '';
|
||||
persistTokens(data.accessToken, data.refreshToken, username);
|
||||
set({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
isAuthenticated: true,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearTokens();
|
||||
set({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
username: null,
|
||||
isAuthenticated: false,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user