Merge pull request 'furqan' (#16) from furqan into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/saas_frontend/pulls/16
This commit is contained in:
furqan
2026-02-17 06:28:13 +00:00
11 changed files with 111 additions and 165 deletions
+4 -6
View File
@@ -31,7 +31,7 @@ export const authApi = {
payload,
{ ...withTenantHeader(options?.tenantId), successMessage: "Account created", errorMessage: "Failed to create account" }
),
logout: () => apiClient.post<LogoutResponse>("/api/auth/logout", { successMessage: "Signed out", errorMessage: "Failed to sign out" }),
logout: () => apiClient.post<LogoutResponse>("/api/auth/logout", null, { successMessage: "Signed out", errorMessage: "Failed to sign out" }),
me: () => apiClient.get<AuthUser>("/api/auth/me"),
resetPassword: (oldPassword: string, newPassword: string) =>
apiClient.post<{ message: string }>("/api/auth/reset-password", {
@@ -69,8 +69,6 @@ export const authApi = {
silent: true,
}),
refresh: (refreshToken: string) =>
apiClient.post<TokenResponse>("/api/auth/refresh", {
refresh_token: refreshToken,
}),
};
refresh: () =>
apiClient.post<TokenResponse>("/api/auth/refresh", {}, { silent: true }),
};
@@ -30,6 +30,7 @@ export type AuthUser = {
export type SigninRequest = {
email: string;
password: string;
remember_me?: boolean;
};
export type SignupRequest = {
@@ -14,7 +14,7 @@ export default function SignInForm() {
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const { login } = useAuth(); // Retrieve login function from context
const { login } = useAuth();
const handleSignIn = async (event: React.FormEvent) => {
event.preventDefault();
@@ -27,10 +27,8 @@ export default function SignInForm() {
};
try {
// Use the context login function to ensure state is updated
await login(payload, isChecked);
// Redirect to the dashboard after successful signin.
navigate("/dashboard");
} catch (error) {
const message =
+16 -6
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, useMemo } from 'react';
import axios from 'axios';
import { ExternalLink, Box, Search, Rocket } from 'lucide-react';
import { moduleApi, type Module } from '../modules/user/UserModuleApi';
import { useAuth } from '../../context/AuthContext';
@@ -31,14 +30,25 @@ const Dashboard = () => {
const handleModuleClick = async (moduleId: string) => {
try {
const response = await moduleApi.launchModule(moduleId);
await axios.post(response.target_url, response.payload, {
headers: response.headers,
withCredentials: true
await fetch(response.target_url, {
method: "POST",
headers: { "Content-Type": "application/json", ...response.headers },
body: JSON.stringify(response.payload),
credentials: "include",
});
if (response.redirect_url) {
window.location.href = response.redirect_url;
try {
const url = new URL(response.redirect_url);
if (url.protocol !== "https:" && url.protocol !== "http:") {
throw new Error("Unsafe protocol");
}
window.location.href = response.redirect_url;
} catch {
console.error("Invalid redirect URL from SSO");
alert("Failed to launch module. Please try again.");
}
}
} catch (error) {
alert("Failed to launch module. Please try again.");
+33 -67
View File
@@ -1,5 +1,4 @@
import axios from 'axios';
import { API_BASE_URL } from '../../../constant';
import { apiClient } from '../../../lib/apiClient';
import type {
Module,
ModuleCreate,
@@ -13,85 +12,52 @@ import type {
TenantModuleUpdate
} from './AdminModuleTypes';
const api = axios.create({
baseURL: API_BASE_URL,
withCredentials: true,
});
export const adminModuleApi = {
listModules: async (): Promise<Module[]> => {
const response = await api.get('/api/admin/modules/');
return response.data;
},
listModules: (): Promise<Module[]> =>
apiClient.get<Module[]>('/api/admin/modules/', { toast: false }),
createModule: async (data: ModuleCreate): Promise<Module> => {
const response = await api.post('/api/admin/modules/', data);
return response.data;
},
createModule: (data: ModuleCreate): Promise<Module> =>
apiClient.post<Module>('/api/admin/modules/', data, { toast: false }),
getModule: async (moduleId: string): Promise<Module> => {
const response = await api.get(`/api/admin/modules/${moduleId}`);
return response.data;
},
getModule: (moduleId: string): Promise<Module> =>
apiClient.get<Module>(`/api/admin/modules/${moduleId}`, { toast: false }),
updateModule: async (moduleId: string, data: ModuleUpdate): Promise<Module> => {
const response = await api.put(`/api/admin/modules/${moduleId}`, data);
return response.data;
},
updateModule: (moduleId: string, data: ModuleUpdate): Promise<Module> =>
apiClient.put<Module>(`/api/admin/modules/${moduleId}`, data, { toast: false }),
deleteModule: async (moduleId: string): Promise<void> => {
await api.delete(`/api/admin/modules/${moduleId}`);
},
deleteModule: (moduleId: string): Promise<void> =>
apiClient.delete<void>(`/api/admin/modules/${moduleId}`, { toast: false }),
listEnvironments: async (moduleId: string): Promise<ModuleEnvironment[]> => {
const response = await api.get(`/api/admin/modules/${moduleId}/environments`);
return response.data;
},
listEnvironments: (moduleId: string): Promise<ModuleEnvironment[]> =>
apiClient.get<ModuleEnvironment[]>(`/api/admin/modules/${moduleId}/environments`, { toast: false }),
createEnvironment: async (moduleId: string, data: EnvironmentCreate): Promise<ModuleEnvironment> => {
const response = await api.post(`/api/admin/modules/${moduleId}/environments`, data);
return response.data;
},
createEnvironment: (moduleId: string, data: EnvironmentCreate): Promise<ModuleEnvironment> =>
apiClient.post<ModuleEnvironment>(`/api/admin/modules/${moduleId}/environments`, data, { toast: false }),
updateEnvironment: async (moduleId: string, envId: string, data: EnvironmentUpdate): Promise<ModuleEnvironment> => {
const response = await api.put(`/api/admin/modules/${moduleId}/environments/${envId}`, data);
return response.data;
},
updateEnvironment: (moduleId: string, envId: string, data: EnvironmentUpdate): Promise<ModuleEnvironment> =>
apiClient.put<ModuleEnvironment>(`/api/admin/modules/${moduleId}/environments/${envId}`, data, { toast: false }),
setDefaultEnvironment: async (moduleId: string, envId: string): Promise<void> => {
await api.patch(`/api/admin/modules/${moduleId}/environments/${envId}/default`);
},
setDefaultEnvironment: (moduleId: string, envId: string): Promise<void> =>
apiClient.patch<void>(`/api/admin/modules/${moduleId}/environments/${envId}/default`, undefined, { toast: false }),
deleteEnvironment: async (moduleId: string, envId: string): Promise<void> => {
await api.delete(`/api/admin/modules/${moduleId}/environments/${envId}`);
},
deleteEnvironment: (moduleId: string, envId: string): Promise<void> =>
apiClient.delete<void>(`/api/admin/modules/${moduleId}/environments/${envId}`, { toast: false }),
getModulePermissions: async (moduleId: string): Promise<ModulePermission[]> => {
const response = await api.get(`/api/admin/modules/${moduleId}/permissions`);
return response.data;
},
getModulePermissions: (moduleId: string): Promise<ModulePermission[]> =>
apiClient.get<ModulePermission[]>(`/api/admin/modules/${moduleId}/permissions`, { toast: false }),
syncModulePermissions: async (moduleId: string): Promise<{ message: string; synced_count: number }> => {
const response = await api.post(`/api/admin/modules/${moduleId}/permissions/sync`);
return response.data;
},
syncModulePermissions: (moduleId: string): Promise<{ message: string; synced_count: number }> =>
apiClient.post<{ message: string; synced_count: number }>(`/api/admin/modules/${moduleId}/permissions/sync`, undefined, { toast: false }),
listTenantModules: async (tenantId: string): Promise<TenantModule[]> => {
const response = await api.get(`/api/admin/tenants/${tenantId}/modules`);
return response.data;
},
listTenantModules: (tenantId: string): Promise<TenantModule[]> =>
apiClient.get<TenantModule[]>(`/api/admin/tenants/${tenantId}/modules`, { toast: false }),
assignModuleToTenant: async (tenantId: string, data: TenantModuleCreate): Promise<TenantModule> => {
const response = await api.post(`/api/admin/tenants/${tenantId}/modules`, data);
return response.data;
},
assignModuleToTenant: (tenantId: string, data: TenantModuleCreate): Promise<TenantModule> =>
apiClient.post<TenantModule>(`/api/admin/tenants/${tenantId}/modules`, data, { toast: false }),
updateTenantModule: async (tenantId: string, tenantModuleId: string, data: TenantModuleUpdate): Promise<TenantModule> => {
const response = await api.put(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, data);
return response.data;
},
updateTenantModule: (tenantId: string, tenantModuleId: string, data: TenantModuleUpdate): Promise<TenantModule> =>
apiClient.put<TenantModule>(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, data, { toast: false }),
removeTenantModule: async (tenantId: string, tenantModuleId: string): Promise<void> => {
await api.delete(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`);
},
removeTenantModule: (tenantId: string, tenantModuleId: string): Promise<void> =>
apiClient.delete<void>(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, { toast: false }),
};
@@ -40,9 +40,9 @@ interface UseModuleApiResult {
export const useModuleApi = (): UseModuleApiResult => {
const [loading, setLoading] = useState(false);
const handleError = (error: any, action: string) => {
const handleError = (error: unknown, action: string) => {
console.error(`Failed to ${action}`, error);
const message = error.response?.data?.detail || `Failed to ${action}`;
const message = error instanceof Error ? error.message : `Failed to ${action}`;
toast.error(message);
};
+5 -16
View File
@@ -1,5 +1,4 @@
import axios from "axios";
import { API_BASE_URL } from "../../../constant";
import { apiClient } from "../../../lib/apiClient";
export interface Module {
module_id: string;
@@ -19,19 +18,9 @@ export interface LaunchResponse {
}
export const moduleApi = {
getAvailableModules: async (): Promise<Module[]> => {
const response = await axios.get(`${API_BASE_URL}/api/modules/available`, {
withCredentials: true,
});
return response.data;
},
getAvailableModules: (): Promise<Module[]> =>
apiClient.get<Module[]>("/api/modules/available", { toast: false }),
launchModule: async (moduleId: string): Promise<LaunchResponse> => {
const response = await axios.post(
`${API_BASE_URL}/api/sso/initiate`,
{ module_id: moduleId },
{ withCredentials: true }
);
return response.data;
},
launchModule: (moduleId: string): Promise<LaunchResponse> =>
apiClient.post<LaunchResponse>("/api/sso/initiate", { module_id: moduleId }, { toast: false }),
};
+5 -1
View File
@@ -61,7 +61,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({
const login = async (credentials: SigninRequest, rememberMe = false) => {
setIsLoading(true);
try {
const response = await authApi.signin(credentials);
const response = await authApi.signin({ ...credentials, remember_me: rememberMe });
setAuthCookies(response, rememberMe);
setUser(response.user);
} finally {
@@ -70,6 +70,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({
};
const logout = async () => {
try {
await authApi.logout();
} catch {
}
clearAuthCookies();
setUser(null);
window.location.href = "/signin";
+33 -35
View File
@@ -1,16 +1,12 @@
import { toast } from "react-toastify";
import { API_BASE_URL } from "../constant";
import { AUTH_COOKIE_KEYS, clearAuthCookies, getCookie } from "./authCookies";
import { isTokenExpired } from "./jwt";
import { clearAuthCookies, refreshLoggedInFlag } from "./authCookies";
type ApiRequestOptions = Omit<RequestInit, "body"> & {
body?: unknown;
toast?: boolean;
/** Optional custom success message to show in a toast */
successMessage?: string;
/** Optional custom error message to show in a toast */
errorMessage?: string;
/** If true, silences all toast notifications for this request */
silent?: boolean;
};
@@ -28,23 +24,39 @@ const hardLogout = () => {
};
const refreshAccessToken = async () => {
const refreshToken = getCookie(AUTH_COOKIE_KEYS.refresh);
if (!refreshToken) throw new Error("No refresh token");
const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
body: JSON.stringify({}),
});
if (!res.ok) throw new Error("Refresh failed");
return res.json();
};
const waitForRefresh = (): Promise<void> =>
new Promise<void>((resolve) => refreshQueue.push(resolve));
const doRefresh = async () => {
if (isRefreshing) {
await waitForRefresh();
return;
}
isRefreshing = true;
try {
await refreshAccessToken();
refreshLoggedInFlag();
} finally {
isRefreshing = false;
refreshQueue.forEach((cb) => cb());
refreshQueue = [];
}
};
const request = async <T>(
path: string,
options: ApiRequestOptions = {}
options: ApiRequestOptions = {},
_isRetry = false
): Promise<T> => {
const {
body,
@@ -58,25 +70,6 @@ const request = async <T>(
const showToast = shouldToast && !silent;
const accessToken = getCookie(AUTH_COOKIE_KEYS.access);
if (accessToken && isTokenExpired(accessToken)) {
try {
if (!isRefreshing) {
isRefreshing = true;
await refreshAccessToken();
isRefreshing = false;
refreshQueue.forEach((cb) => cb());
refreshQueue = [];
} else {
await new Promise<void>((resolve) => refreshQueue.push(resolve));
}
} catch {
hardLogout();
throw new Error("Session expired");
}
}
const requestHeaders = new Headers(headers);
let requestBody: BodyInit | undefined;
@@ -101,8 +94,16 @@ const request = async <T>(
body: requestBody,
});
// Global 401 handler - ignore for signin requests as they just mean invalid credentials
if (response.status === 401 && !url.includes("/api/auth/signin")) {
if (!_isRetry) {
try {
await doRefresh();
return request<T>(path, options, true);
} catch {
hardLogout();
throw new Error("Session expired");
}
}
hardLogout();
throw new Error("Unauthorized");
}
@@ -116,20 +117,17 @@ const request = async <T>(
: await response.text();
if (!response.ok) {
// Extract the specific error message from the backend response
const backendError =
typeof data === "object" && data !== null
? (data as any).detail || (data as any).message
: undefined;
// Prioritize backend error, then the custom errorMessage option, then a default
const finalErrorMessage = backendError || errorMessage || "Request failed";
if (showToast) {
toast.error(finalErrorMessage, API_TOAST_OPTIONS);
}
// Throw the actual message so the caller (e.g., SignInForm) can display it
throw new Error(finalErrorMessage);
}
@@ -168,4 +166,4 @@ export const apiClient = {
delete: <T>(path: string, options?: ApiRequestOptions) =>
request<T>(path, { ...options, method: "DELETE" }),
};
};
+11 -20
View File
@@ -1,12 +1,5 @@
import { type TokenResponse } from "../application/authentication/AuthTypes";
export const AUTH_COOKIE_KEYS = {
access: "access_token",
refresh: "refresh_token",
};
const ACCESS_MAX_AGE_SECONDS = 60 * 60; // 1 hour
const REFRESH_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7 days
const LOGGED_IN_KEY = "logged_in";
const LOGGED_IN_MAX_AGE_SECONDS = 60 * 15;
const buildCookieOptions = (maxAgeSeconds?: number) => {
const options = ["path=/", "samesite=lax"];
@@ -41,19 +34,17 @@ export const getCookie = (name: string) => {
return decodeURIComponent(rawValue);
};
export const setAuthCookies = (tokens: TokenResponse, rememberMe = false) => {
// Store tokens in cookies so they can be reused across page refreshes.
const accessMaxAge = rememberMe ? ACCESS_MAX_AGE_SECONDS : undefined;
const refreshMaxAge = rememberMe ? REFRESH_MAX_AGE_SECONDS : undefined;
setCookie(AUTH_COOKIE_KEYS.access, tokens.access_token, accessMaxAge);
setCookie(AUTH_COOKIE_KEYS.refresh, tokens.refresh_token, refreshMaxAge);
export const setAuthCookies = (_tokens: unknown, rememberMe = false) => {
const maxAge = rememberMe ? LOGGED_IN_MAX_AGE_SECONDS : undefined;
setCookie(LOGGED_IN_KEY, "1", maxAge);
};
export const clearAuthCookies = () => {
// Clear both cookies on logout or auth failure.
deleteCookie(AUTH_COOKIE_KEYS.access);
deleteCookie(AUTH_COOKIE_KEYS.refresh);
deleteCookie(LOGGED_IN_KEY);
};
export const getAccessToken = () => getCookie(AUTH_COOKIE_KEYS.access);
export const refreshLoggedInFlag = () => {
setCookie(LOGGED_IN_KEY, "1", LOGGED_IN_MAX_AGE_SECONDS);
};
export const getAccessToken = () => getCookie(LOGGED_IN_KEY);
-9
View File
@@ -1,9 +0,0 @@
export const isTokenExpired = (token: string): boolean => {
try {
const payload = JSON.parse(atob(token.split(".")[1]));
const now = Math.floor(Date.now() / 1000);
return payload.exp < now;
} catch {
return true;
}
};