wire spec-driven auth: remove VITE_AUTH_BASE_URL, reorder providers

- main.jsx: remove VITE_AUTH_BASE_URL env var; add AppContent that
  reads authConfig from useAppContext() and passes to AuthProvider;
  reorder: AppProvider -> AuthProvider -> BrowserRouter
- sync react-openapi: AuthConfig type, extractAuthConfig, loading fix
- sync react-auth: authConfig prop, config-driven paths, server logout
This commit is contained in:
2026-07-18 19:48:47 +05:30
parent e87a2d55ab
commit 00dd507fc4
7 changed files with 83 additions and 24 deletions

View File

@@ -3,6 +3,15 @@ import { tokenStore } from "./token";
import { createApiClient } from "./axios";
import { AuthUser } from "./models";
export interface AuthServerConfig {
serverUrl: string;
loginPath: string;
registerPath: string;
logoutPath: string;
mePath: string;
introspectPath: string;
}
interface AuthContextModel {
currentUser: AuthUser | null;
token: string | null;
@@ -17,24 +26,24 @@ const AuthContext = createContext<AuthContextModel | undefined>(undefined);
export function AuthProvider({
children,
authBaseUrl,
authConfig,
}: {
children: React.ReactNode;
authBaseUrl: string;
authConfig: AuthServerConfig;
}) {
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(tokenStore.get());
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const auth = createApiClient(authBaseUrl);
const auth = createApiClient(authConfig.serverUrl);
const login = async (username: string, password: string) => {
try {
setLoading(true);
setError(null);
const res = await auth.post("/login", { username, password });
const res = await auth.post(authConfig.loginPath, { username, password });
const { access_token, user } = res.data;
tokenStore.set(access_token);
@@ -52,7 +61,7 @@ export function AuthProvider({
setLoading(true);
setError(null);
await auth.post("/register", { username, password });
await auth.post(authConfig.registerPath, { username, password });
await login(username, password);
} catch (e: any) {
setError(e.response?.data?.detail ?? "Registration failed");
@@ -61,7 +70,12 @@ export function AuthProvider({
}
};
const logout = () => {
const logout = async () => {
try {
await auth.post(authConfig.logoutPath);
} catch {
// Server logout is best-effort; clear token locally regardless
}
tokenStore.clear();
setToken(null);
setCurrentUser(null);
@@ -70,10 +84,12 @@ export function AuthProvider({
const fetchCurrentUser = async () => {
if (!token) return;
try {
const me = await auth.get("/me");
const me = await auth.get(authConfig.mePath);
setCurrentUser({ ...me.data });
} catch {
logout();
tokenStore.clear();
setToken(null);
setCurrentUser(null);
}
};
@@ -94,4 +110,4 @@ export function useAuth(): AuthContextModel {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}
}

View File

@@ -1,4 +1,5 @@
export { AuthProvider, useAuth } from "./contexts";
export type { AuthServerConfig } from "./contexts";
export { createApiClient } from "./axios";
export { AuthPage } from "./AuthPage";
export { ProfileCreate } from "./ProfileCreate";

View File

@@ -12,4 +12,4 @@ export { useItemSse } from "./src/hooks/useItemSse";
export { sanitizePayload } from "./src/utils/sanitize-payload";
export type { FkResolver } from "./src/utils/sanitize-payload";
export type { FilterComponentProps } from "./src/context/useResource";
export type { SpecConfiguration, ResourceConfig, FieldConfig, FKFieldConfig, ResourceRelationship, ProfileOperation, ProfileComponents } from "./src/types";
export type { SpecConfiguration, ResourceConfig, FieldConfig, FKFieldConfig, ResourceRelationship, ProfileOperation, ProfileComponents, AuthConfig } from "./src/types";

View File

@@ -1,11 +1,12 @@
import { createContext, useContext } from "react";
import type { ProfileComponents, ProfileOperation, ResourceConfig, SpecConfiguration, ValidationMessage } from "../types";
import type { AuthConfig, ProfileComponents, ProfileOperation, ResourceConfig, SpecConfiguration, ValidationMessage } from "../types";
export interface AppContextValue {
config: SpecConfiguration;
resources: ResourceConfig[];
profileOperations: ProfileOperation[];
profileComponents: ProfileComponents;
authConfig: AuthConfig;
schemas: Record<string, any>;
loading: boolean;
errors: ValidationMessage[];

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from "react";
import type { ProfileOperation, SpecConfiguration, ResourceConfig, ValidationMessage } from "../types";
import type { AuthConfig, ProfileOperation, SpecConfiguration, ResourceConfig, ValidationMessage } from "../types";
import { AppContext } from "./AppContext";
import { loadSpec } from "../spec-loader";
import { validateSpec } from "../spec-validator";
@@ -11,6 +11,19 @@ interface AppProviderProps {
children: React.ReactNode;
}
function extractAuthConfig(spec: any): AuthConfig {
const bearer: Record<string, any> =
spec?.components?.securitySchemes?.bearerAuth ?? {};
return {
serverUrl: bearer["x-server-url"] ?? "",
loginPath: bearer["x-login-path"] ?? "/login",
registerPath: bearer["x-register-path"] ?? "/register",
logoutPath: bearer["x-logout-path"] ?? "/logout",
mePath: bearer["x-me-path"] ?? "/me",
introspectPath: bearer["x-introspect-path"] ?? "/introspect",
};
}
function extractProfileOperations(spec: any): ProfileOperation[] {
const ops: ProfileOperation[] = [];
const paths = spec.paths ?? {};
@@ -30,10 +43,20 @@ function extractProfileOperations(spec: any): ProfileOperation[] {
return ops;
}
const DEFAULT_AUTH_CONFIG: AuthConfig = {
serverUrl: "",
loginPath: "/login",
registerPath: "/register",
logoutPath: "/logout",
mePath: "/me",
introspectPath: "/introspect",
};
export function AppProvider({ specConfiguration, children }: AppProviderProps) {
const [loading, setLoading] = useState(true);
const [resources, setResources] = useState<ResourceConfig[]>([]);
const [profileOperations, setProfileOperations] = useState<ProfileOperation[]>([]);
const [authConfig, setAuthConfig] = useState<AuthConfig>(DEFAULT_AUTH_CONFIG);
const [schemas, setSchemas] = useState<Record<string, any>>({});
const [errors, setErrors] = useState<ValidationMessage[]>([]);
const [warnings, setWarnings] = useState<ValidationMessage[]>([]);
@@ -59,6 +82,7 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
setWarnings(warns);
setSchemas(spec.components?.schemas ?? {});
setProfileOperations(extractProfileOperations(spec));
setAuthConfig(extractAuthConfig(spec));
}
if (errs.length === 0) {
@@ -97,12 +121,13 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
resources,
profileOperations,
profileComponents,
authConfig,
schemas,
loading,
errors,
warnings,
}),
[specConfiguration, resources, profileOperations, profileComponents, schemas, loading, errors, warnings]
[specConfiguration, resources, profileOperations, profileComponents, authConfig, schemas, loading, errors, warnings]
);
return React.createElement(AppContext.Provider, { value }, children);

View File

@@ -21,6 +21,16 @@ export interface SpecConfiguration {
profileComponents?: ProfileComponents;
}
/** Auth server config extracted from securitySchemes extensions. */
export interface AuthConfig {
serverUrl: string;
loginPath: string;
registerPath: string;
logoutPath: string;
mePath: string;
introspectPath: string;
}
/** Represents a single operation marked with `x-profile: true` in the spec. */
export interface ProfileOperation {
path: string;

View File

@@ -15,7 +15,7 @@ import {
import Home from './Home';
import FetchRequests from './FetchRequest/FetchRequestCreate';
import FetchRequestDetail from './FetchRequest/FetchRequestDetail';
import { AppProvider, Admin, ProfileRoutes } from '../react-openapi';
import { AppProvider, useAppContext, Admin, ProfileRoutes } from '../react-openapi';
import { AuthProvider, AuthPage, useAuth, ProfileCreate, ProfileEdit, ProfileView } from "../react-auth";
import Header from './Header';
import Footer from './Footer';
@@ -27,8 +27,6 @@ const queryClient = new QueryClient();
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
// Wire profile components from react-auth into the spec-driven admin
specConfiguration.profileComponents = {
create: ProfileCreate,
@@ -81,11 +79,12 @@ const routerMapping = [
{ path: "/profile/*", component: ProfileRoutes, headerTitle: "Profile" },
];
root.render(
<QueryClientProvider client={queryClient}>
<AppProvider specConfiguration={specConfiguration}>
<BrowserRouter>
<AuthProvider authBaseUrl={AUTH_BASE}>
/** Reads authConfig from AppProvider context and passes it to AuthProvider. */
function AppContent() {
const { authConfig } = useAppContext();
return (
<AuthProvider authConfig={authConfig}>
<BrowserRouter>
<AppTheme>
<CssBaseline enableColorScheme />
<Header routerMapping={routerMapping} />
@@ -106,8 +105,15 @@ root.render(
<Footer />
</AppTheme>
</AuthProvider>
</BrowserRouter>
</BrowserRouter>
</AuthProvider>
);
}
root.render(
<QueryClientProvider client={queryClient}>
<AppProvider specConfiguration={specConfiguration}>
<AppContent />
</AppProvider>
</QueryClientProvider>
);
);