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:
@@ -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 =
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user