Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | 1x 1x 1x 1x 1x 1x 1x 1x 13x 13x 3x 3x 10x 13x 13x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 4x 4x 4x 4x 4x 4x 3x 2x 4x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 70x 70x 70x 70x 70x 1x 69x 69x 69x 69x 69x 69x 69x 69x 4x 69x 69x 1x 5x 5x 5x 3x 3x 2x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 3x 5x 5x 1x 51x 51x 51x 51x 51x 51x 3x 3x | // Constants
import { ENV, COOKIE_KEYS } from '@/constants';
import { ERROR_MESSAGES } from '@/constants/messages';
// Services
import { auth } from '@/services';
// Types
import { User } from 'firebase/auth';
// Utils
import { getCookieArray, setCookieArray, compareStringArrays } from '@/utils';
import { cookieStorage } from '@shared/utils';
// Types
import { PrivateRoute, UserProfile, UserRole } from '@/types/auth';
import { CookieAttributes, UserGroup } from '@shared/types';
const BEARER_PREFIX = 'Bearer ';
/**
* Extracts the Bearer token from an incoming API request's Authorization header.
*
* @returns The token string, or `null` when the header is missing or malformed.
*/
export const getBearerTokenFromRequest = (request: Request): string | null => {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith(BEARER_PREFIX)) {
return null;
}
const token = authHeader.slice(BEARER_PREFIX.length).trim();
return token || null;
};
/**
* Constructs a full API endpoint URL by combining the base URL with a given path.
* It ensures there are no duplicate slashes between the base and the path.
*
* @param path - The relative path to append to the base endpoint (e.g., 'rpc')
* @returns The full URL (e.g., 'https://api.example.com/rpc')
*/
export const getEndpointUrl = (path: string = ''): string => {
const base = ENV.API_ENDPOINT;
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
};
/**
* Returns the current Firebase ID token for authenticated API calls.
*
* @throws {Error} When no user is signed in or token retrieval fails.
*/
export const getAuthToken = async (): Promise<string> => {
const token = await auth.currentUser?.getIdToken();
if (!token) throw new Error(ERROR_MESSAGES.DEFAULT);
return token;
};
/**
* Check whether the Firebase auth state has changed compared to stored shared values.
*
* @param nextUser - The next Firebase `User` object from onAuthStateChanged, or `null` if signed out.
* @param tokenShared - The shared token stored in cookies/local storage (if any).
* @param emailShared - The shared user email stored in cookies/local storage (if any).
* @returns `true` if the auth state has changed and requires handling, otherwise `false`.
*/
export const hasUserStateChanged = (
nextUser: User | null,
tokenShared?: string | null,
emailShared?: string | null,
): boolean => {
return (
(!!nextUser && !tokenShared) ||
(!nextUser && !!tokenShared) ||
(!!nextUser && !!emailShared && nextUser.email !== emailShared)
);
};
type UserRoleData = { role: string; groups: string[] };
/**
* Check and synchronize user role and groups with cookies.
*
* @param role - The latest user role from backend.
* @param groups - The latest user groups from backend (default is []).
* @returns True if role or groups were changed and updated in cookies, otherwise false.
*/
export const checkSyncUserRole = ({
role,
groups = [],
}: UserRoleData): boolean => {
const currentUserRole = cookieStorage.getItem(COOKIE_KEYS.USER_ROLE);
const currentUserGroups = getCookieArray(COOKIE_KEYS.USER_GROUPS);
const isChangedRole = !!role && !!currentUserRole && role !== currentUserRole;
const isChangedGroups =
!!currentUserRole && !compareStringArrays(currentUserGroups, groups);
cookieStorage.setItem(COOKIE_KEYS.USER_ROLE, role);
setCookieArray(COOKIE_KEYS.USER_GROUPS, groups);
return isChangedRole || isChangedGroups;
};
/**
* Clear authentication-related cookies
*
* @param normalKeys - List of cookie keys that can be removed without extra config
* @param sharedKeys - List of cookie keys that require special cookie config
*/
export const clearAuthCookies = (
normalKeys: string[],
sharedKeys: string[],
sharedConfig: {
domain: string;
sameSite: CookieAttributes['sameSite'];
secure: true;
},
) => {
normalKeys.forEach((key) => cookieStorage.removeItem(key));
sharedKeys.forEach((key) => cookieStorage.removeItem(key, sharedConfig));
};
export const findMatchedRoute = (
pathname: string,
routes: { path: string; role?: UserRole; groups?: UserGroup[] }[],
) => {
return routes.find((r) => pathname.startsWith(r.path));
};
/**
* Check if a user has access to a private route
*
* @param user - Current logged-in user
* @param route - The private route to check
* @returns boolean - true if user has permission, false otherwise
*/
export const checkPermission = (
user: UserProfile,
route: PrivateRoute,
): boolean => {
if (!user) return false;
const matchRole = !!route.role && user.role === route.role;
const matchGroup =
!!route.groups &&
!!user.groups &&
user.groups.some((g: UserGroup) => route.groups!.includes(g));
return matchRole || matchGroup;
};
/**
* Checks whether a JWT token is expired.
*
* @param token - The JWT token string to validate.
* @returns {boolean} - Returns `true` if the token is expired or invalid, otherwise `false`.
*/
export const isTokenExpired = (token: string): boolean => {
try {
const [, payload] = token.split('.');
if (!payload) return true;
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const decodedStr = globalThis.atob
? globalThis.atob(base64)
: Buffer.from(base64, 'base64').toString('binary');
const decoded = JSON.parse(
decodeURIComponent(
decodedStr
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join(''),
),
);
const exp = decoded?.exp;
if (!exp) return true;
return exp < Math.floor(Date.now() / 1000);
} catch {
return true;
}
};
/**
* Determines whether a user is valid based on their role and groups.
*
* @param role - The user's role (e.g. ADMIN, USER).
* @param groups - A list of groups the user belongs to.
* @param requiredGroups - One or more groups that are required for access.
* @returns `true` if the user is an ADMIN or belongs to any of the required groups.
*/
export const isValidMember = (
role?: UserRole,
groups: UserGroup[] = [],
requiredGroups: UserGroup[] = [],
): boolean => {
if (!role) return false;
// Admin always passes validation
if (role === UserRole.ADMIN) return true;
// Check if the user belongs to at least one required group
return groups.some((g) => requiredGroups.includes(g));
};
|