262 lines
7.1 KiB
TypeScript
262 lines
7.1 KiB
TypeScript
import { match } from '@formatjs/intl-localematcher'
|
|
import Negotiator from 'negotiator'
|
|
import type { UserSession } from '@heygermany/functions/src/application-types.d.js'
|
|
import type { DB, UserRole, UserType } from '@heygermany/sdk'
|
|
import { UserTypes } from '@heygermany/sdk'
|
|
import type { SupportedLocale } from '@heygermany/sdk/config'
|
|
import { LOCALES, DEFAULT_LOCALE } from '@/config'
|
|
|
|
type RequestSnapshot = {
|
|
url: string
|
|
pathname: string
|
|
search: string
|
|
headers: Headers
|
|
cookies: Map<string, string>
|
|
}
|
|
|
|
type RequestRoutingResult =
|
|
| {
|
|
type: 'redirect'
|
|
location: string
|
|
}
|
|
| {
|
|
type: 'continue'
|
|
locale: string
|
|
}
|
|
|
|
const LOCALE_EXEMPT_PREFIXES = ['/pilot']
|
|
const LOCALE_EXEMPT_LOCALES: Record<string, SupportedLocale> = {
|
|
'/pilot': 'de',
|
|
}
|
|
|
|
const getApiBase = (request: RequestSnapshot) => {
|
|
return process.env.VITE_API_BASE_URL || process.env.API_URL || new URL(request.url).origin
|
|
}
|
|
|
|
function shouldBypassRouting(pathname: string): boolean {
|
|
return (
|
|
pathname.startsWith('/api/') ||
|
|
pathname === '/api' ||
|
|
pathname.startsWith('/_build/') ||
|
|
pathname === '/_build' ||
|
|
pathname.startsWith('/_serverFn/') ||
|
|
pathname === '/_serverFn' ||
|
|
pathname.startsWith('/@') ||
|
|
pathname.startsWith('/src/') ||
|
|
pathname.startsWith('/node_modules/') ||
|
|
/\.[a-z0-9]+$/i.test(pathname)
|
|
)
|
|
}
|
|
|
|
const getCookieHeader = (cookies: Map<string, string>) => {
|
|
return Array.from(cookies.entries())
|
|
.map(([name, value]) => `${name}=${value}`)
|
|
.join('; ')
|
|
}
|
|
|
|
const getBetterAuthSession = async (
|
|
request: RequestSnapshot
|
|
): Promise<UserSession | null> => {
|
|
const response = await fetch(`${getApiBase(request)}/api/auth/get-session`, {
|
|
method: 'GET',
|
|
headers: {
|
|
cookie: getCookieHeader(request.cookies),
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
return null
|
|
}
|
|
|
|
const data = (await response.json()) as {
|
|
user?: {
|
|
id: string
|
|
name: string
|
|
role: UserRole
|
|
type: UserType
|
|
} | null
|
|
} | null
|
|
|
|
if (!data?.user) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
userId: data.user.id,
|
|
name: data.user.name,
|
|
role: data.user.role,
|
|
type: data.user.type,
|
|
}
|
|
}
|
|
|
|
function getLocale(request: RequestSnapshot): string {
|
|
try {
|
|
const cookieLocale = request.cookies.get('NEXT_LOCALE')
|
|
if (cookieLocale && LOCALES.includes(cookieLocale as SupportedLocale)) {
|
|
return cookieLocale
|
|
}
|
|
|
|
const headers = Object.fromEntries(request.headers.entries()) as Negotiator.Headers
|
|
const languages = new Negotiator({ headers }).languages()
|
|
const validLanguages = languages.filter((lang) => {
|
|
try {
|
|
Intl.getCanonicalLocales(lang)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
|
|
if (validLanguages.length === 0) {
|
|
return DEFAULT_LOCALE
|
|
}
|
|
|
|
return match(validLanguages, LOCALES, DEFAULT_LOCALE)
|
|
} catch (error) {
|
|
console.error('[Middleware] Error in locale detection:', error)
|
|
return DEFAULT_LOCALE
|
|
}
|
|
}
|
|
|
|
function hasLocale(pathname: string): boolean {
|
|
return LOCALES.some((locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`)
|
|
}
|
|
|
|
function isLocaleExemptPath(pathname: string): boolean {
|
|
return LOCALE_EXEMPT_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))
|
|
}
|
|
|
|
function getLocaleExemptPathLocale(pathname: string): SupportedLocale | null {
|
|
for (const [prefix, locale] of Object.entries(LOCALE_EXEMPT_LOCALES)) {
|
|
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
return locale
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function getLocaleFromPath(pathname: string): string | null {
|
|
for (const locale of LOCALES) {
|
|
if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) {
|
|
return locale
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function removeLocalePrefix(pathname: string): string {
|
|
const locale = getLocaleFromPath(pathname)
|
|
if (locale) {
|
|
return pathname.slice(`/${locale}`.length) || '/'
|
|
}
|
|
|
|
return pathname
|
|
}
|
|
|
|
function requiresAuth(pathname: string): { required: boolean; type?: DB.UserType } {
|
|
const pathWithoutLocale = removeLocalePrefix(pathname)
|
|
|
|
if (pathWithoutLocale.includes('/auth/login')) {
|
|
return { required: false }
|
|
}
|
|
|
|
if (pathWithoutLocale.startsWith('/backoffice')) {
|
|
return { required: true, type: UserTypes.BACKOFFICE }
|
|
}
|
|
|
|
if (pathWithoutLocale.startsWith('/company')) {
|
|
return { required: true, type: UserTypes.COMPANY }
|
|
}
|
|
|
|
if (pathWithoutLocale.startsWith('/jobs/application')) {
|
|
return { required: true, type: UserTypes.CANDIDATE }
|
|
}
|
|
|
|
return { required: false }
|
|
}
|
|
|
|
const toAbsoluteUrl = (request: RequestSnapshot, location: string) => {
|
|
return new URL(location, request.url).toString()
|
|
}
|
|
|
|
export const getResolvedLocale = (pathname: string) => {
|
|
return getLocaleFromPath(pathname) || getLocaleExemptPathLocale(pathname) || DEFAULT_LOCALE
|
|
}
|
|
|
|
export async function resolveRequestRouting(
|
|
request: RequestSnapshot
|
|
): Promise<RequestRoutingResult> {
|
|
if (shouldBypassRouting(request.pathname)) {
|
|
return {
|
|
type: 'continue',
|
|
locale: getResolvedLocale(request.pathname),
|
|
}
|
|
}
|
|
|
|
const fullUrl = request.pathname + request.search
|
|
|
|
if (!hasLocale(request.pathname) && !isLocaleExemptPath(request.pathname)) {
|
|
const locale = getLocale(request)
|
|
return {
|
|
type: 'redirect',
|
|
location: toAbsoluteUrl(request, `/${locale}${request.pathname}${request.search}`),
|
|
}
|
|
}
|
|
|
|
const userSession = await getBetterAuthSession(request)
|
|
const authCheck = requiresAuth(request.pathname)
|
|
|
|
if (authCheck.required) {
|
|
const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE
|
|
|
|
if (userSession?.type !== UserTypes.BACKOFFICE) {
|
|
if (!userSession || userSession.type !== authCheck.type) {
|
|
let loginPath = ''
|
|
if (authCheck.type === UserTypes.BACKOFFICE) {
|
|
loginPath = `/${currentLocale}/backoffice/auth/login`
|
|
} else if (authCheck.type === UserTypes.COMPANY) {
|
|
loginPath = `/${currentLocale}/company/auth/login`
|
|
} else if (authCheck.type === UserTypes.CANDIDATE) {
|
|
loginPath = `/${currentLocale}/jobs/apply`
|
|
}
|
|
|
|
const returnUrl = encodeURIComponent(fullUrl)
|
|
return {
|
|
type: 'redirect',
|
|
location: toAbsoluteUrl(request, `${loginPath}?return=${returnUrl}`),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (userSession) {
|
|
const pathWithoutLocale = removeLocalePrefix(request.pathname)
|
|
const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE
|
|
|
|
if (pathWithoutLocale.includes('/auth/login')) {
|
|
let dashboardPath = ''
|
|
if (userSession.type === UserTypes.BACKOFFICE) {
|
|
dashboardPath = `/${currentLocale}/backoffice/candidates`
|
|
} else if (userSession.type === UserTypes.COMPANY) {
|
|
dashboardPath = `/${currentLocale}/company/candidates`
|
|
} else if (userSession.type === UserTypes.CANDIDATE) {
|
|
dashboardPath = `/${currentLocale}/jobs/application`
|
|
}
|
|
|
|
if (dashboardPath) {
|
|
return {
|
|
type: 'redirect',
|
|
location: toAbsoluteUrl(request, dashboardPath),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'continue',
|
|
locale: getResolvedLocale(request.pathname),
|
|
}
|
|
}
|