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:
@@ -3,6 +3,15 @@ import { tokenStore } from "./token";
|
|||||||
import { createApiClient } from "./axios";
|
import { createApiClient } from "./axios";
|
||||||
import { AuthUser } from "./models";
|
import { AuthUser } from "./models";
|
||||||
|
|
||||||
|
export interface AuthServerConfig {
|
||||||
|
serverUrl: string;
|
||||||
|
loginPath: string;
|
||||||
|
registerPath: string;
|
||||||
|
logoutPath: string;
|
||||||
|
mePath: string;
|
||||||
|
introspectPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthContextModel {
|
interface AuthContextModel {
|
||||||
currentUser: AuthUser | null;
|
currentUser: AuthUser | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
@@ -17,24 +26,24 @@ const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
|||||||
|
|
||||||
export function AuthProvider({
|
export function AuthProvider({
|
||||||
children,
|
children,
|
||||||
authBaseUrl,
|
authConfig,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
authBaseUrl: string;
|
authConfig: AuthServerConfig;
|
||||||
}) {
|
}) {
|
||||||
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(tokenStore.get());
|
const [token, setToken] = useState<string | null>(tokenStore.get());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const auth = createApiClient(authBaseUrl);
|
const auth = createApiClient(authConfig.serverUrl);
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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;
|
const { access_token, user } = res.data;
|
||||||
|
|
||||||
tokenStore.set(access_token);
|
tokenStore.set(access_token);
|
||||||
@@ -52,7 +61,7 @@ export function AuthProvider({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
await auth.post("/register", { username, password });
|
await auth.post(authConfig.registerPath, { username, password });
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.response?.data?.detail ?? "Registration failed");
|
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();
|
tokenStore.clear();
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
@@ -70,10 +84,12 @@ export function AuthProvider({
|
|||||||
const fetchCurrentUser = async () => {
|
const fetchCurrentUser = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const me = await auth.get("/me");
|
const me = await auth.get(authConfig.mePath);
|
||||||
setCurrentUser({ ...me.data });
|
setCurrentUser({ ...me.data });
|
||||||
} catch {
|
} catch {
|
||||||
logout();
|
tokenStore.clear();
|
||||||
|
setToken(null);
|
||||||
|
setCurrentUser(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { AuthProvider, useAuth } from "./contexts";
|
export { AuthProvider, useAuth } from "./contexts";
|
||||||
|
export type { AuthServerConfig } from "./contexts";
|
||||||
export { createApiClient } from "./axios";
|
export { createApiClient } from "./axios";
|
||||||
export { AuthPage } from "./AuthPage";
|
export { AuthPage } from "./AuthPage";
|
||||||
export { ProfileCreate } from "./ProfileCreate";
|
export { ProfileCreate } from "./ProfileCreate";
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export { useItemSse } from "./src/hooks/useItemSse";
|
|||||||
export { sanitizePayload } from "./src/utils/sanitize-payload";
|
export { sanitizePayload } from "./src/utils/sanitize-payload";
|
||||||
export type { FkResolver } from "./src/utils/sanitize-payload";
|
export type { FkResolver } from "./src/utils/sanitize-payload";
|
||||||
export type { FilterComponentProps } from "./src/context/useResource";
|
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";
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { createContext, useContext } from "react";
|
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 {
|
export interface AppContextValue {
|
||||||
config: SpecConfiguration;
|
config: SpecConfiguration;
|
||||||
resources: ResourceConfig[];
|
resources: ResourceConfig[];
|
||||||
profileOperations: ProfileOperation[];
|
profileOperations: ProfileOperation[];
|
||||||
profileComponents: ProfileComponents;
|
profileComponents: ProfileComponents;
|
||||||
|
authConfig: AuthConfig;
|
||||||
schemas: Record<string, any>;
|
schemas: Record<string, any>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
errors: ValidationMessage[];
|
errors: ValidationMessage[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState, useMemo } from "react";
|
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 { AppContext } from "./AppContext";
|
||||||
import { loadSpec } from "../spec-loader";
|
import { loadSpec } from "../spec-loader";
|
||||||
import { validateSpec } from "../spec-validator";
|
import { validateSpec } from "../spec-validator";
|
||||||
@@ -11,6 +11,19 @@ interface AppProviderProps {
|
|||||||
children: React.ReactNode;
|
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[] {
|
function extractProfileOperations(spec: any): ProfileOperation[] {
|
||||||
const ops: ProfileOperation[] = [];
|
const ops: ProfileOperation[] = [];
|
||||||
const paths = spec.paths ?? {};
|
const paths = spec.paths ?? {};
|
||||||
@@ -30,10 +43,20 @@ function extractProfileOperations(spec: any): ProfileOperation[] {
|
|||||||
return ops;
|
return ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
||||||
|
serverUrl: "",
|
||||||
|
loginPath: "/login",
|
||||||
|
registerPath: "/register",
|
||||||
|
logoutPath: "/logout",
|
||||||
|
mePath: "/me",
|
||||||
|
introspectPath: "/introspect",
|
||||||
|
};
|
||||||
|
|
||||||
export function AppProvider({ specConfiguration, children }: AppProviderProps) {
|
export function AppProvider({ specConfiguration, children }: AppProviderProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [resources, setResources] = useState<ResourceConfig[]>([]);
|
const [resources, setResources] = useState<ResourceConfig[]>([]);
|
||||||
const [profileOperations, setProfileOperations] = useState<ProfileOperation[]>([]);
|
const [profileOperations, setProfileOperations] = useState<ProfileOperation[]>([]);
|
||||||
|
const [authConfig, setAuthConfig] = useState<AuthConfig>(DEFAULT_AUTH_CONFIG);
|
||||||
const [schemas, setSchemas] = useState<Record<string, any>>({});
|
const [schemas, setSchemas] = useState<Record<string, any>>({});
|
||||||
const [errors, setErrors] = useState<ValidationMessage[]>([]);
|
const [errors, setErrors] = useState<ValidationMessage[]>([]);
|
||||||
const [warnings, setWarnings] = useState<ValidationMessage[]>([]);
|
const [warnings, setWarnings] = useState<ValidationMessage[]>([]);
|
||||||
@@ -59,6 +82,7 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
|
|||||||
setWarnings(warns);
|
setWarnings(warns);
|
||||||
setSchemas(spec.components?.schemas ?? {});
|
setSchemas(spec.components?.schemas ?? {});
|
||||||
setProfileOperations(extractProfileOperations(spec));
|
setProfileOperations(extractProfileOperations(spec));
|
||||||
|
setAuthConfig(extractAuthConfig(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errs.length === 0) {
|
if (errs.length === 0) {
|
||||||
@@ -97,12 +121,13 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
|
|||||||
resources,
|
resources,
|
||||||
profileOperations,
|
profileOperations,
|
||||||
profileComponents,
|
profileComponents,
|
||||||
|
authConfig,
|
||||||
schemas,
|
schemas,
|
||||||
loading,
|
loading,
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
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);
|
return React.createElement(AppContext.Provider, { value }, children);
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export interface SpecConfiguration {
|
|||||||
profileComponents?: ProfileComponents;
|
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. */
|
/** Represents a single operation marked with `x-profile: true` in the spec. */
|
||||||
export interface ProfileOperation {
|
export interface ProfileOperation {
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
26
src/main.jsx
26
src/main.jsx
@@ -15,7 +15,7 @@ import {
|
|||||||
import Home from './Home';
|
import Home from './Home';
|
||||||
import FetchRequests from './FetchRequest/FetchRequestCreate';
|
import FetchRequests from './FetchRequest/FetchRequestCreate';
|
||||||
import FetchRequestDetail from './FetchRequest/FetchRequestDetail';
|
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 { AuthProvider, AuthPage, useAuth, ProfileCreate, ProfileEdit, ProfileView } from "../react-auth";
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
@@ -27,8 +27,6 @@ const queryClient = new QueryClient();
|
|||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
const root = createRoot(rootElement);
|
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
|
// Wire profile components from react-auth into the spec-driven admin
|
||||||
specConfiguration.profileComponents = {
|
specConfiguration.profileComponents = {
|
||||||
create: ProfileCreate,
|
create: ProfileCreate,
|
||||||
@@ -81,11 +79,12 @@ const routerMapping = [
|
|||||||
{ path: "/profile/*", component: ProfileRoutes, headerTitle: "Profile" },
|
{ path: "/profile/*", component: ProfileRoutes, headerTitle: "Profile" },
|
||||||
];
|
];
|
||||||
|
|
||||||
root.render(
|
/** Reads authConfig from AppProvider context and passes it to AuthProvider. */
|
||||||
<QueryClientProvider client={queryClient}>
|
function AppContent() {
|
||||||
<AppProvider specConfiguration={specConfiguration}>
|
const { authConfig } = useAppContext();
|
||||||
<BrowserRouter>
|
return (
|
||||||
<AuthProvider authBaseUrl={AUTH_BASE}>
|
<AuthProvider authConfig={authConfig}>
|
||||||
|
<BrowserRouter>
|
||||||
<AppTheme>
|
<AppTheme>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
<Header routerMapping={routerMapping} />
|
<Header routerMapping={routerMapping} />
|
||||||
@@ -106,8 +105,15 @@ root.render(
|
|||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</AppTheme>
|
</AppTheme>
|
||||||
</AuthProvider>
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AppProvider specConfiguration={specConfiguration}>
|
||||||
|
<AppContent />
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user