admin/lib/authRouting.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

121 lines
3.6 KiB
TypeScript

import type { UserRole } from '@/types'
import { APP_ROUTES } from '@/constants/routes'
export const ADMIN_ROUTE_PREFIXES = [
APP_ROUTES.DASHBOARD,
'/users',
APP_ROUTES.MANAGE_EVENTS,
'/payments',
'/bookings',
'/settlements',
'/withdrawal-requests',
'/bank-accounts',
APP_ROUTES.EVENT_CATEGORIES,
APP_ROUTES.BLOG_ARTICLES,
'/guest-lists',
'/reviews',
'/cities',
'/identity-verifications',
'/notifications',
'/sms-messages',
'/notification-rules',
'/manual-notifications',
'/push-deliveries',
'/user-reports',
'/contact-messages',
'/chat-oversight',
'/admin',
'/discount-codes',
'/wallet-deposits',
'/audit-logs',
'/support-tickets',
'/provinces',
] as const
export const isAdminOnlyRoute = (pathname: string) => {
if (pathname === '/events') return true
return ADMIN_ROUTE_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))
}
/** Backoffice: only `/auth` is reachable without a session. */
export const isPublicBrowseRoute = (pathname: string) => pathname === '/auth' || pathname.startsWith('/auth/')
/** Backoffice is never search-indexable. */
export const isIndexableRoute = (_pathname: string) => false
/**
* Unauthenticated requests always go to admin `/auth` (never consumer AuthGate).
*/
export const buildGuestLoginRedirect = (requestUrl: string, pathname: string, search = ''): URL => {
const returnPath = `${pathname}${search}`
const loginUrl = new URL('/auth', requestUrl)
if (returnPath !== '/' && !returnPath.startsWith('/auth')) {
loginUrl.searchParams.set('redirect', returnPath)
}
return loginUrl
}
export const getDefaultPostLoginPath = (_role?: UserRole | null) => APP_ROUTES.DASHBOARD
export const getSafeInternalRedirect = (value: string | null | undefined, fallback: string): string => {
if (!value || !value.startsWith('/') || value.startsWith('//')) return fallback
try {
const base = 'https://internal.ghabilee.invalid'
const url = new URL(value, base)
if (url.origin !== base) return fallback
return `${url.pathname}${url.search}${url.hash}`
} catch {
return fallback
}
}
interface AccessTokenRoutingHint {
role?: UserRole
sub?: unknown
}
const decodeAccessTokenRoutingHint = (accessToken?: string): AccessTokenRoutingHint | null => {
if (!accessToken) return null
try {
const payload = accessToken.split('.')[1]
if (!payload) return null
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as AccessTokenRoutingHint
} catch {
return null
}
}
export const getRoleFromAccessToken = (accessToken?: string): UserRole | null => {
const decoded = decodeAccessTokenRoutingHint(accessToken)
return decoded?.role === 'admin' ? 'admin' : decoded?.role === 'user' ? 'user' : null
}
/** Unverified JWT subject hint for viewer-scoped client cache keys only. */
export const getViewerIdFromAccessToken = (accessToken?: string): string | null => {
const subject = decodeAccessTokenRoutingHint(accessToken)?.sub
return typeof subject === 'string' && subject.length > 0 ? subject : null
}
// Kept for shared axios/auth code that still references these symbols.
export const CONSUMER_AUTH_QUERY = 'auth'
export const CONSUMER_SESSION_EXPIRED_EVENT = 'ghabilee:consumer-session-expired'
export function isConsumerAccessTokenExpired(user: { AccessTokenExpireTime?: number | null } | null | undefined): boolean {
return user?.AccessTokenExpireTime != null && user.AccessTokenExpireTime <= Date.now()
}
export const isConsumerShellRoute = (_pathname: string) => false
export { isLegacyEventDetailRoute } from '@/lib/routing/eventRoute'