fix(api): support shared session cookie domain
Build & Deploy / Build & Push Docker Image (push) Successful in 2m49s
Test / Type Check (all packages) (push) Successful in 55s
Build & Deploy / Deploy to VPS (push) Successful in 3s
Test / API Unit Tests (push) Successful in 48s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 47s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 40s
Test / API Integration Tests (push) Successful in 59s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m49s
Test / Type Check (all packages) (push) Successful in 55s
Build & Deploy / Deploy to VPS (push) Successful in 3s
Test / API Unit Tests (push) Successful in 48s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 47s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 40s
Test / API Integration Tests (push) Successful in 59s
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { clearSessionCookie, setSessionCookie } from './sessionCookies'
|
||||||
|
|
||||||
|
const originalEnv = { ...process.env }
|
||||||
|
|
||||||
|
function mockResponse() {
|
||||||
|
return {
|
||||||
|
cookie: vi.fn(),
|
||||||
|
clearCookie: vi.fn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('session cookies', () => {
|
||||||
|
it('sets employee session cookies for the configured parent domain', () => {
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
process.env.SESSION_COOKIE_DOMAIN = '.rentaldrivego.ma'
|
||||||
|
const res = mockResponse()
|
||||||
|
|
||||||
|
setSessionCookie(res as never, 'employee', 'token-123', 1000)
|
||||||
|
|
||||||
|
expect(res.cookie).toHaveBeenCalledWith('employee_session', 'token-123', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
domain: '.rentaldrivego.ma',
|
||||||
|
maxAge: 1000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears cookies with the same domain attributes used when setting them', () => {
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
process.env.SESSION_COOKIE_DOMAIN = '.rentaldrivego.ma'
|
||||||
|
const res = mockResponse()
|
||||||
|
|
||||||
|
clearSessionCookie(res as never, 'employee')
|
||||||
|
|
||||||
|
expect(res.clearCookie).toHaveBeenCalledWith('employee_session', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
domain: '.rentaldrivego.ma',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('omits the domain option when no shared cookie domain is configured', () => {
|
||||||
|
delete process.env.SESSION_COOKIE_DOMAIN
|
||||||
|
const res = mockResponse()
|
||||||
|
|
||||||
|
setSessionCookie(res as never, 'employee', 'token-123')
|
||||||
|
|
||||||
|
expect(res.cookie).toHaveBeenCalledWith('employee_session', 'token-123', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Response } from 'express'
|
import type { CookieOptions, Response } from 'express'
|
||||||
import type { ActorType } from './tokens'
|
import type { ActorType } from './tokens'
|
||||||
|
|
||||||
const COOKIE_NAMES: Record<ActorType, string> = {
|
const COOKIE_NAMES: Record<ActorType, string> = {
|
||||||
@@ -11,21 +11,23 @@ export function getSessionCookieName(actorType: ActorType) {
|
|||||||
return COOKIE_NAMES[actorType]
|
return COOKIE_NAMES[actorType]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSessionCookie(res: Response, actorType: ActorType, token: string, maxAgeMs?: number) {
|
function getSessionCookieOptions(actorType: ActorType, maxAgeMs?: number): CookieOptions {
|
||||||
res.cookie(getSessionCookieName(actorType), token, {
|
const domain = process.env.SESSION_COOKIE_DOMAIN?.trim()
|
||||||
|
|
||||||
|
return {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: actorType === 'admin' ? 'strict' : 'lax',
|
sameSite: actorType === 'admin' ? 'strict' : 'lax',
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: maxAgeMs,
|
...(domain ? { domain } : {}),
|
||||||
})
|
...(maxAgeMs !== undefined ? { maxAge: maxAgeMs } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSessionCookie(res: Response, actorType: ActorType, token: string, maxAgeMs?: number) {
|
||||||
|
res.cookie(getSessionCookieName(actorType), token, getSessionCookieOptions(actorType, maxAgeMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSessionCookie(res: Response, actorType: ActorType) {
|
export function clearSessionCookie(res: Response, actorType: ActorType) {
|
||||||
res.clearCookie(getSessionCookieName(actorType), {
|
res.clearCookie(getSessionCookieName(actorType), getSessionCookieOptions(actorType))
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: actorType === 'admin' ? 'strict' : 'lax',
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('dashboard apiFetch', () => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps browser requests on the dashboard proxy even when a public API origin is configured', async () => {
|
it('normalizes a public API origin to the versioned API base', async () => {
|
||||||
installBrowser()
|
installBrowser()
|
||||||
process.env.NEXT_PUBLIC_API_URL = 'https://api.rentaldrivego.ma'
|
process.env.NEXT_PUBLIC_API_URL = 'https://api.rentaldrivego.ma'
|
||||||
const fetchMock = vi.fn(async () => ({
|
const fetchMock = vi.fn(async () => ({
|
||||||
@@ -51,7 +51,7 @@ describe('dashboard apiFetch', () => {
|
|||||||
const { apiFetch } = await import('./api')
|
const { apiFetch } = await import('./api')
|
||||||
await apiFetch('/auth/account/start', { method: 'POST', body: JSON.stringify({}) })
|
await apiFetch('/auth/account/start', { method: 'POST', body: JSON.stringify({}) })
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith('/dashboard/api/v1/auth/account/start', expect.any(Object))
|
expect(fetchMock).toHaveBeenCalledWith('https://api.rentaldrivego.ma/api/v1/auth/account/start', expect.any(Object))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not force JSON content type for FormData payloads', async () => {
|
it('does not force JSON content type for FormData payloads', async () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function normalizeApiBase(value: string): string {
|
|||||||
export const API_BASE = normalizeApiBase(
|
export const API_BASE = normalizeApiBase(
|
||||||
typeof window === 'undefined'
|
typeof window === 'undefined'
|
||||||
? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
|
? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
|
||||||
: '/dashboard/api/v1',
|
: (process.env.NEXT_PUBLIC_API_URL || '/dashboard/api/v1'),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
|
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
|
||||||
|
|||||||
Reference in New Issue
Block a user