Pages used zinc and sky classes with every dark: variant paired by hand, and several pairs failed WCAG AA: white on the sky-600 buttons (4.1:1), zinc-500 secondary text on dark backgrounds (3.7-4.1:1), the 10px zinc-400 labels (2.6:1) and the dashboard's green "Connected" (3.3:1). index.css now defines theme tokens once, light and dark side by side (page, surface, subtle, line, line-strong, fg, muted, primary and its soft/line/ink shades, link, focus), exposed to Tailwind with @theme inline. Pages use those names, so a palette change is one file. Blue #2563eb from the logo for buttons and links, the logo's navy #0b1120 for the dark page. Every text pair is at least 4.5:1 in both themes, most 7:1. Cards and lists sit on the surface color with a light shadow on an off-white page, at rounded-xl; native controls follow the theme (color-scheme). Zinc and sky stay only as data colors: status dots and chart swatches.
42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import type { ReactNode } from 'react';
|
|
import { Navigate, useLocation } from 'react-router';
|
|
import type { AuthUser } from '@selfphoneng/shared';
|
|
import { ApiError, authApi } from '../api/client.ts';
|
|
|
|
export const meQuery = {
|
|
queryKey: ['me'],
|
|
queryFn: authApi.me,
|
|
retry: (n: number, e: Error) => !(e instanceof ApiError && e.status === 401) && n < 2,
|
|
staleTime: 60_000,
|
|
};
|
|
|
|
export function useMe() {
|
|
return useQuery(meQuery);
|
|
}
|
|
|
|
/** Gate for signed-in pages: login first, then a forced password change, then the role check. */
|
|
export function RequireAuth({
|
|
role,
|
|
children,
|
|
}: {
|
|
role?: AuthUser['role'];
|
|
children: (user: AuthUser) => ReactNode;
|
|
}) {
|
|
const me = useMe();
|
|
const loc = useLocation();
|
|
if (me.isPending) return <p className="p-6 text-sm text-muted">Loading…</p>;
|
|
if (me.isError) {
|
|
if (me.error instanceof ApiError && me.error.status === 401)
|
|
return <Navigate to={`/login?next=${encodeURIComponent(loc.pathname)}`} replace />;
|
|
return <p className="p-6 text-sm text-red-600">Could not reach the server.</p>;
|
|
}
|
|
const user = me.data;
|
|
if (user.mustChangePassword) return <Navigate to="/change-password" replace />;
|
|
if (role && user.role !== role) return <Navigate to="/" replace />;
|
|
return <>{children(user)}</>;
|
|
}
|
|
|
|
export const homeFor = (user: AuthUser) => (user.role === 'admin' ? '/admin' : '/me');
|