- Introduced a centralized `WEB_URL` constant for managing application routes, improving maintainability and readability across components. - Updated various components, including login, auth, and main app modules, to utilize the new `WEB_URL` constants for navigation, ensuring consistency in route management. - Added a new `AppHomeRedirect` component to streamline user redirection based on privileges, enhancing user experience. - Implemented client-side navigation functions to prevent full page reloads, improving performance and user interaction. - Added tests for new functionalities, ensuring reliability in navigation and URL handling. These changes significantly enhance the application's routing structure and navigation efficiency, providing a more cohesive user experience.
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
import { appDatabase, AppDatabaseKey, appStorage, AppStorageKey } from '../storage/local';
|
|
import { API_URL } from '../constants/api-url';
|
|
import { WEB_URL } from '../constants/web-url';
|
|
import { clientReplace, notifySessionEnd, notifySessionStart } from './client-navigation';
|
|
import { mapUserPrivileges } from './map-user-privileges';
|
|
import { safeInternalPath } from './safe-internal-path';
|
|
import type { AuthUser, TokenPair } from './auth.types';
|
|
|
|
interface TerminateOptions {
|
|
/** When true, appends ?redirect= so the user returns to their page after re-login.
|
|
* Defaults to false (intended for 401 / expired token).
|
|
* Set to false for manual logout so the user lands on a clean login page. */
|
|
preserveRedirect?: boolean;
|
|
}
|
|
|
|
export async function persistTokenPair(tokens: TokenPair): Promise<void> {
|
|
await appDatabase.setItem(AppDatabaseKey.ACCESS_TOKEN, tokens.accessToken);
|
|
await appDatabase.setItem(AppDatabaseKey.REFRESH_TOKEN, tokens.refreshToken);
|
|
}
|
|
|
|
export async function persistUserSession(me: AuthUser): Promise<void> {
|
|
const label = me.isSuperadmin ? 'Superadmin' : (me.privilege?.name ?? me.username);
|
|
|
|
await appStorage.setItem(AppStorageKey.USER_ID, me.id);
|
|
await appDatabase.setItem(AppDatabaseKey.USER_PROFILE, {
|
|
id: me.id,
|
|
username: me.username,
|
|
isSuperadmin: me.isSuperadmin,
|
|
privilege: me.privilege,
|
|
name: me.username,
|
|
label,
|
|
});
|
|
await appDatabase.setItem(AppDatabaseKey.USER_PRIVILEGE, mapUserPrivileges(me.permissions, me.isSuperadmin));
|
|
}
|
|
|
|
export async function terminateAuthSession(options: TerminateOptions = {}): Promise<void> {
|
|
const { preserveRedirect = false } = options;
|
|
|
|
await appDatabase.removeItem(AppDatabaseKey.USER_PRIVILEGE);
|
|
await appDatabase.removeItem(AppDatabaseKey.USER_PROFILE);
|
|
await appDatabase.removeItem(AppDatabaseKey.ACCESS_TOKEN);
|
|
await appDatabase.removeItem(AppDatabaseKey.REFRESH_TOKEN);
|
|
await appStorage.removeItem(AppStorageKey.USER_ID);
|
|
|
|
let loginUrl = WEB_URL.LOGIN;
|
|
const isLoginPage =
|
|
window.location.pathname === WEB_URL.LOGIN || window.location.pathname.startsWith(`${WEB_URL.LOGIN}/`);
|
|
|
|
if (!isLoginPage) {
|
|
if (preserveRedirect) {
|
|
loginUrl += `?redirect=${encodeURIComponent(window.location.pathname)}`;
|
|
}
|
|
|
|
notifySessionEnd();
|
|
clientReplace(loginUrl);
|
|
}
|
|
}
|
|
|
|
export async function initiateAuthSession(tokens: TokenPair, me: AuthUser): Promise<void> {
|
|
if (!tokens.accessToken) {
|
|
throw new Error('Login response missing access token');
|
|
}
|
|
|
|
await persistTokenPair(tokens);
|
|
await persistUserSession(me);
|
|
notifySessionStart();
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
clientReplace(safeInternalPath(params.get('redirect'), WEB_URL.APP));
|
|
}
|
|
|
|
let refreshInFlight: Promise<TokenPair> | null = null;
|
|
|
|
export async function refreshAuthSession(): Promise<TokenPair> {
|
|
if (!refreshInFlight) {
|
|
refreshInFlight = rotateRefreshToken().finally(() => {
|
|
refreshInFlight = null;
|
|
});
|
|
}
|
|
|
|
return refreshInFlight;
|
|
}
|
|
|
|
async function rotateRefreshToken(): Promise<TokenPair> {
|
|
const refreshToken = await appDatabase.getItem<string>(AppDatabaseKey.REFRESH_TOKEN);
|
|
if (!refreshToken) {
|
|
throw new Error('Missing refresh token');
|
|
}
|
|
|
|
const { apiClient } = await import('./api-client');
|
|
const response = await apiClient.request<TokenPair>({
|
|
url: API_URL.AUTH_REFRESH,
|
|
method: 'POST',
|
|
data: { refreshToken },
|
|
skipAuthRefresh: true,
|
|
});
|
|
const tokens: TokenPair = {
|
|
accessToken: response.data.accessToken,
|
|
refreshToken: response.data.refreshToken,
|
|
};
|
|
|
|
await persistTokenPair(tokens);
|
|
return tokens;
|
|
}
|
|
|
|
export async function logoutAuthSession(): Promise<void> {
|
|
const refreshToken = await appDatabase.getItem<string>(AppDatabaseKey.REFRESH_TOKEN);
|
|
|
|
try {
|
|
if (refreshToken) {
|
|
const { apiClient } = await import('./api-client');
|
|
await apiClient.request({
|
|
url: API_URL.AUTH_REVOKE,
|
|
method: 'POST',
|
|
data: { refreshToken },
|
|
skipAuthRefresh: true,
|
|
});
|
|
}
|
|
} catch {
|
|
// Always clear the local session, even if the server revoke call fails.
|
|
}
|
|
|
|
await terminateAuthSession({ preserveRedirect: false });
|
|
}
|