4 Commits

Author SHA1 Message Date
1a8f98d4a2 move BrowserRouter above AppProvider, wire onUnauthorized redirect to /login 2026-07-19 01:04:42 +05:30
804617ff27 update .run configs to point at khata-ui package.json 2026-07-18 20:20:22 +05:30
00dd507fc4 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
2026-07-18 19:48:47 +05:30
e87a2d55ab fixes 2026-07-18 13:52:52 +05:30
10 changed files with 116 additions and 46 deletions

View File

@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Install Deps" type="js.build_tools.npm"> <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" /> <command value="install" />
<node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" /> <node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" />
<envs /> <envs />

View File

@@ -1,12 +1,15 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev" type="js.build_tools.npm"> <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" /> <command value="run" />
<scripts> <scripts>
<script value="dev" /> <script value="dev" />
</scripts> </scripts>
<node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" /> <node-interpreter value="$APPLICATION_CONFIG_DIR$/node/versions/20.19.5/node" />
<envs /> <envs />
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
<browser name="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" />
</EXTENSION>
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>

View File

@@ -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);
} }
}; };

View File

@@ -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";

View File

@@ -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";

View File

@@ -135,7 +135,7 @@ function ProfileComponentWrapper({
getOperation?: { path: string }; getOperation?: { path: string };
}) { }) {
const [data, setData] = useState<any>(null); 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 [error, setError] = useState<string | null>(null);
const fetchPath = getOperation?.path ?? operation.path; const fetchPath = getOperation?.path ?? operation.path;

View File

@@ -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[];

View File

@@ -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);

View File

@@ -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;

View File

@@ -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> const navigate = useNavigate();
<AuthProvider authBaseUrl={AUTH_BASE}> return (
<AuthProvider authConfig={authConfig}>
<AppTheme> <AppTheme>
<CssBaseline enableColorScheme /> <CssBaseline enableColorScheme />
<Header routerMapping={routerMapping} /> <Header routerMapping={routerMapping} />
@@ -107,7 +106,22 @@ root.render(
<Footer /> <Footer />
</AppTheme> </AppTheme>
</AuthProvider> </AuthProvider>
</BrowserRouter> );
}
function AppWithAuth() {
const navigate = useNavigate();
return (
<AppProvider specConfiguration={specConfiguration} onUnauthorized={() => navigate("/login")}>
<AppContent />
</AppProvider> </AppProvider>
);
}
root.render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppWithAuth />
</BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
); );