Compare commits
4 Commits
5d34c51e98
...
1a8f98d4a2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8f98d4a2 | |||
| 804617ff27 | |||
| 00dd507fc4 | |||
| e87a2d55ab |
@@ -1,6 +1,6 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Install Deps" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<package-json value="$PROJECT_DIR$/../khata-ui/package.json" />
|
||||
<command value="install" />
|
||||
<node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" />
|
||||
<envs />
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<package-json value="$PROJECT_DIR$/../khata-ui/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" />
|
||||
<envs />
|
||||
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
|
||||
<browser name="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" />
|
||||
</EXTENSION>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -135,7 +135,7 @@ function ProfileComponentWrapper({
|
||||
getOperation?: { path: string };
|
||||
}) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(mode !== "create");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchPath = getOperation?.path ?? operation.path;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
74
src/main.jsx
74
src/main.jsx
@@ -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,33 +79,49 @@ const routerMapping = [
|
||||
{ path: "/profile/*", component: ProfileRoutes, headerTitle: "Profile" },
|
||||
];
|
||||
|
||||
/** Reads authConfig from AppProvider context and passes it to AuthProvider. */
|
||||
function AppContent() {
|
||||
const { authConfig } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<AuthProvider authConfig={authConfig}>
|
||||
<AppTheme>
|
||||
<CssBaseline enableColorScheme />
|
||||
<Header routerMapping={routerMapping} />
|
||||
|
||||
<Box sx={{ pb: 8 }}>
|
||||
<Toolbar />
|
||||
|
||||
<Routes>
|
||||
{routerMapping.map(({ path, component: Component }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Component basePath={path.replace(/\/\*$/, "")} />}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</Box>
|
||||
|
||||
<Footer />
|
||||
</AppTheme>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppWithAuth() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<AppProvider specConfiguration={specConfiguration} onUnauthorized={() => navigate("/login")}>
|
||||
<AppContent />
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppProvider specConfiguration={specConfiguration}>
|
||||
<BrowserRouter>
|
||||
<AuthProvider authBaseUrl={AUTH_BASE}>
|
||||
<AppTheme>
|
||||
<CssBaseline enableColorScheme />
|
||||
<Header routerMapping={routerMapping} />
|
||||
|
||||
<Box sx={{ pb: 8 }}>
|
||||
<Toolbar />
|
||||
|
||||
<Routes>
|
||||
{routerMapping.map(({ path, component: Component }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Component basePath={path.replace(/\/\*$/, "")} />}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</Box>
|
||||
|
||||
<Footer />
|
||||
</AppTheme>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
<BrowserRouter>
|
||||
<AppWithAuth />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
);
|
||||
Reference in New Issue
Block a user