Restore the app files (prior commit mis-scoped its tar)
build-and-publish / build (push) Failing after 8s

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
2026-07-22 20:50:58 -05:00
co-authored by Claude Opus 4.8
parent eaad7b916e
commit 2a0dd1ae6b
85 changed files with 6012 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import { getMe } from '../api';
import type { MeInfo } from '../types';
interface AuthState {
me: MeInfo | null;
loading: boolean;
refresh: () => void;
}
const AuthContext = createContext<AuthState>({ me: null, loading: true, refresh: () => {} });
/** This is a public site — /api/me is open and simply reports whether an admin is signed in. */
export function AuthProvider({ children }: { children: ReactNode }) {
const [me, setMe] = useState<MeInfo | null>(null);
const [loading, setLoading] = useState(true);
const refresh = useCallback(() => {
setLoading(true);
getMe().then(setMe).catch(() => setMe(null)).finally(() => setLoading(false));
}, []);
useEffect(() => { refresh(); }, [refresh]);
return <AuthContext.Provider value={{ me, loading, refresh }}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthState {
return useContext(AuthContext);
}
+55
View File
@@ -0,0 +1,55 @@
// Shared HTTP core (canonical copy: platform/frontend-template/src/lib/http.ts). Same-origin fetch to
// /api, CSRF from the XSRF-TOKEN cookie, and 401 -> Authentik login redirect (only admin routes are gated).
export interface HttpOptions {
loginUrl?: string;
base?: string;
}
function csrfHeader(): Record<string, string> {
const m = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
return m ? { 'X-XSRF-TOKEN': decodeURIComponent(m[1]) } : {};
}
export function createApi(opts: HttpOptions = {}) {
const loginUrl = opts.loginUrl ?? '/oauth2/authorization/authentik';
const base = opts.base ?? '/api';
async function req<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(base + path, {
...init,
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', ...csrfHeader(), ...(init?.headers ?? {}) },
});
if (res.status === 401) {
window.location.href = loginUrl;
throw new Error('unauthenticated');
}
if (!res.ok) throw new Error(`${init?.method ?? 'GET'} ${path} failed: ${res.status}`);
if (res.status === 204) return undefined as T;
const text = await res.text();
return (text ? JSON.parse(text) : null) as T;
}
return {
get: <T>(path: string) => req<T>(path),
post: <T>(path: string, body?: unknown) =>
req<T>(path, { method: 'POST', body: body != null ? JSON.stringify(body) : undefined }),
put: <T>(path: string, body?: unknown) =>
req<T>(path, { method: 'PUT', body: body != null ? JSON.stringify(body) : undefined }),
del: <T>(path: string) => req<T>(path, { method: 'DELETE' }),
login(): void {
window.location.href = loginUrl;
},
logout(): void {
// Full-page form POST so the browser follows the RP-initiated logout redirect chain.
const form = document.createElement('form');
form.method = 'POST';
form.action = '/logout';
document.body.appendChild(form);
form.submit();
},
};
}
export type Api = ReturnType<typeof createApi>;
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
/** Minimal data-loading helper: runs `fn` on mount / when `deps` change. */
export function useAsync<T>(fn: () => Promise<T>, deps: unknown[] = []) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setLoading(true);
setError(null);
fn()
.then((d) => { if (alive) { setData(d); setLoading(false); } })
.catch((e) => { if (alive) { setError(String(e)); setLoading(false); } });
return () => { alive = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return { data, loading, error };
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}