fix: OIDC auth flow, Vite proxy, and PostgreSQL query compat

- Add auth redirect in root layout for unauthenticated users
- Proxy OIDC routes (/login, /callback, /logout) through Vite dev server
- Strip Secure flag from OIDC cookies in dev mode (HTTP localhost)
- Disable retry on auth query to prevent stale cookie loops
- Fix SQLite .get()/.all()/.run() calls in category and global-item
  services for PostgreSQL compatibility
- Add userId scoping to category service functions
- Add OIDC error logging in auth middleware
- Apply linter auto-formatting across affected files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 18:25:31 +02:00
parent f7588827b1
commit 574a12e6fa
32 changed files with 315 additions and 253 deletions

View File

@@ -11,6 +11,7 @@ export function useAuth() {
queryKey: ["auth"],
queryFn: () => apiGet<AuthState>("/api/auth/me"),
staleTime: 5 * 60 * 1000,
retry: false,
});
}

View File

@@ -12,9 +12,12 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
import { Route as GlobalItemsIndexRouteImport } from './routes/global-items/index'
import { Route as CollectionIndexRouteImport } from './routes/collection/index'
import { Route as UsersUserIdRouteImport } from './routes/users/$userId'
import { Route as ThreadsThreadIdRouteImport } from './routes/threads/$threadId'
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
@@ -31,11 +34,21 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const GlobalItemsIndexRoute = GlobalItemsIndexRouteImport.update({
id: '/global-items/',
path: '/global-items/',
getParentRoute: () => rootRouteImport,
} as any)
const CollectionIndexRoute = CollectionIndexRouteImport.update({
id: '/collection/',
path: '/collection/',
getParentRoute: () => rootRouteImport,
} as any)
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/users/$userId',
path: '/users/$userId',
getParentRoute: () => rootRouteImport,
} as any)
const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({
id: '/threads/$threadId',
path: '/threads/$threadId',
@@ -46,31 +59,45 @@ const SetupsSetupIdRoute = SetupsSetupIdRouteImport.update({
path: '/setups/$setupId',
getParentRoute: () => rootRouteImport,
} as any)
const GlobalItemsGlobalItemIdRoute = GlobalItemsGlobalItemIdRouteImport.update({
id: '/global-items/$globalItemId',
path: '/global-items/$globalItemId',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/settings': typeof SettingsRoute
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/settings': typeof SettingsRoute
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/users/$userId': typeof UsersUserIdRoute
'/collection': typeof CollectionIndexRoute
'/global-items': typeof GlobalItemsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/settings': typeof SettingsRoute
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -78,34 +105,46 @@ export interface FileRouteTypes {
| '/'
| '/login'
| '/settings'
| '/global-items/$globalItemId'
| '/setups/$setupId'
| '/threads/$threadId'
| '/users/$userId'
| '/collection/'
| '/global-items/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/login'
| '/settings'
| '/global-items/$globalItemId'
| '/setups/$setupId'
| '/threads/$threadId'
| '/users/$userId'
| '/collection'
| '/global-items'
id:
| '__root__'
| '/'
| '/login'
| '/settings'
| '/global-items/$globalItemId'
| '/setups/$setupId'
| '/threads/$threadId'
| '/users/$userId'
| '/collection/'
| '/global-items/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute
SettingsRoute: typeof SettingsRoute
GlobalItemsGlobalItemIdRoute: typeof GlobalItemsGlobalItemIdRoute
SetupsSetupIdRoute: typeof SetupsSetupIdRoute
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
UsersUserIdRoute: typeof UsersUserIdRoute
CollectionIndexRoute: typeof CollectionIndexRoute
GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute
}
declare module '@tanstack/react-router' {
@@ -131,6 +170,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/global-items/': {
id: '/global-items/'
path: '/global-items'
fullPath: '/global-items/'
preLoaderRoute: typeof GlobalItemsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/collection/': {
id: '/collection/'
path: '/collection'
@@ -138,6 +184,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CollectionIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/users/$userId': {
id: '/users/$userId'
path: '/users/$userId'
fullPath: '/users/$userId'
preLoaderRoute: typeof UsersUserIdRouteImport
parentRoute: typeof rootRouteImport
}
'/threads/$threadId': {
id: '/threads/$threadId'
path: '/threads/$threadId'
@@ -152,6 +205,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SetupsSetupIdRouteImport
parentRoute: typeof rootRouteImport
}
'/global-items/$globalItemId': {
id: '/global-items/$globalItemId'
path: '/global-items/$globalItemId'
fullPath: '/global-items/$globalItemId'
preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport
parentRoute: typeof rootRouteImport
}
}
}
@@ -159,9 +219,12 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LoginRoute: LoginRoute,
SettingsRoute: SettingsRoute,
GlobalItemsGlobalItemIdRoute: GlobalItemsGlobalItemIdRoute,
SetupsSetupIdRoute: SetupsSetupIdRoute,
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
UsersUserIdRoute: UsersUserIdRoute,
CollectionIndexRoute: CollectionIndexRoute,
GlobalItemsIndexRoute: GlobalItemsIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -2,6 +2,7 @@ import {
createRootRoute,
type ErrorComponentProps,
Outlet,
useLocation,
useMatchRoute,
useNavigate,
useRouter,
@@ -72,7 +73,8 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) {
function RootLayout() {
const navigate = useNavigate();
const { data: auth } = useAuth();
const location = useLocation();
const { data: auth, isLoading: authLoading } = useAuth();
const isAuthenticated = !!auth?.user;
// Item panel state
@@ -99,7 +101,7 @@ function RootLayout() {
const resolveCandidateId = useUIStore((s) => s.resolveCandidateId);
const closeResolveDialog = useUIStore((s) => s.closeResolveDialog);
// Onboarding
// Onboarding — only check when authenticated (endpoint requires auth)
const { data: onboardingComplete, isLoading: onboardingLoading } =
useOnboardingComplete();
const [wizardDismissed, setWizardDismissed] = useState(false);
@@ -152,7 +154,30 @@ function RootLayout() {
!(collectionSearch as Record<string, string>).tab ||
(collectionSearch as Record<string, string>).tab === "gear");
// Show a minimal loading state while checking onboarding status
// Show loading while checking auth
if (authLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-gray-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
// Redirect unauthenticated users to login (server-side OIDC route)
// Allow public routes through without auth
const isPublicRoute =
location.pathname.startsWith("/users/") || location.pathname === "/login";
if (!isAuthenticated && !isPublicRoute) {
window.location.href = "/login";
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-sm text-gray-500">Redirecting to login...</p>
</div>
);
}
// Show loading while checking onboarding status
if (onboardingLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">