fix architecture and write new tests

This commit is contained in:
root
2026-06-10 00:40:19 -04:00
parent 560da1cadf
commit 80a597bc10
377 changed files with 84020 additions and 1337 deletions
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { formatCurrency } from '@rentaldrivego/types'
import { API_BASE, apiFetch, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
import { API_BASE, apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
interface ReportRow {
@@ -110,9 +110,8 @@ export default function ReportsPage() {
setExporting(true)
setError(null)
try {
const token = window.localStorage.getItem(EMPLOYEE_TOKEN_KEY)
const res = await fetch(`${API_BASE}/analytics/report?period=${period}&format=CSV`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
credentials: 'include',
})
if (!res.ok) {
const json = await res.json().catch(() => null)
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
import InviteModal from '@/components/team/InviteModal'
import EditMemberModal from '@/components/team/EditMemberModal'
import PermissionsMatrix from '@/components/team/PermissionsMatrix'
import { EMPLOYEE_TOKEN_KEY } from '@/lib/api'
import { apiFetch } from '@/lib/api'
import { useTeam, TeamMember, InvitePayload } from '@/hooks/useTeam'
import { useDashboardI18n } from '@/components/I18nProvider'
@@ -193,18 +193,17 @@ export default function TeamPage() {
const [toast, setToast] = useState<string | null>(null)
useEffect(() => {
const token = window.localStorage.getItem(EMPLOYEE_TOKEN_KEY)
if (!token) return
let cancelled = false
try {
const encoded = token.split('.')[1] ?? ''
const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/')
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
const payload = JSON.parse(atob(padded))
setCurrentEmployeeId(typeof payload?.sub === 'string' ? payload.sub : null)
} catch {
setCurrentEmployeeId(null)
}
apiFetch<{ employee: { id: string } }>('/auth/employee/me')
.then(({ employee }) => {
if (!cancelled) setCurrentEmployeeId(employee.id)
})
.catch(() => {
if (!cancelled) setCurrentEmployeeId(null)
})
return () => { cancelled = true }
}, [])
const currentEmployee = members.find((member) => member.id === currentEmployeeId)
+2 -2
View File
@@ -13,8 +13,8 @@ function resolveInitialLanguage(value: string | undefined): 'en' | 'fr' | 'ar' {
return value === 'fr' || value === 'ar' || value === 'en' ? value : 'en'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
const cookieStore = cookies()
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies()
const initialLanguage = resolveInitialLanguage(
cookieStore.get(SHARED_LANGUAGE_COOKIE)?.value ?? cookieStore.get('dashboard-language')?.value,
)
@@ -0,0 +1,57 @@
import React, { isValidElement } from 'react'
import { describe, expect, it } from 'vitest'
import ForgotPasswordPageClient from './forgot-password/ForgotPasswordPageClient'
import ResetPasswordPageClient from './reset-password/ResetPasswordPageClient'
import ForgotPasswordPage from './forgot-password/page'
import ResetPasswordPage from './reset-password/page'
import AcceptInvitePage from './onboarding/accept-invite/page'
function collectText(node: unknown): string[] {
if (node === null || node === undefined || typeof node === 'boolean') return []
if (typeof node === 'string' || typeof node === 'number') return [String(node)]
if (Array.isArray(node)) return node.flatMap(collectText)
if (isValidElement<{ children?: React.ReactNode }>(node)) return collectText(node.props.children)
return []
}
function findElement(node: unknown, predicate: (element: React.ReactElement) => boolean): React.ReactElement | null {
if (node === null || node === undefined || typeof node === 'boolean') return null
if (Array.isArray(node)) {
for (const child of node) {
const found = findElement(child, predicate)
if (found) return found
}
return null
}
if (!isValidElement<{ children?: React.ReactNode }>(node)) return null
if (predicate(node)) return node
return findElement(node.props.children, predicate)
}
describe('dashboard public auth pages', () => {
it('renders forgot-password in embedded mode for the public shell route', () => {
const page = ForgotPasswordPage()
expect(isValidElement(page)).toBe(true)
expect(page.type).toBe(ForgotPasswordPageClient)
expect(page.props.embedded).toBe(true)
})
it('renders reset-password in embedded mode for the public shell route', () => {
const page = ResetPasswordPage()
expect(isValidElement(page)).toBe(true)
expect(page.type).toBe(ResetPasswordPageClient)
expect(page.props.embedded).toBe(true)
})
it('keeps accept-invite copy and sign-in handoff visible', () => {
const page = AcceptInvitePage()
const text = collectText(page).join(' ')
const signInLink = findElement(page, (element) => element.props.href === '/sign-in')
expect(text).toContain('Invitation accepted')
expect(text).toContain('Sign in to dashboard')
expect(signInLink).not.toBeNull()
})
})
@@ -7,7 +7,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useDashboardI18n } from '@/components/I18nProvider'
import PublicShell from '@/components/layout/PublicShell'
import { adminUrl, marketplaceUrl } from '@/lib/urls'
import { API_BASE, EMPLOYEE_PROFILE_KEY, EMPLOYEE_TOKEN_KEY } from '@/lib/api'
import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api'
import { toPublicDashboardPath } from '@/lib/dashboardPaths'
const DASHBOARD_LOGO_SRC = '/dashboard/rentalcardrive.png'
@@ -235,14 +235,13 @@ function LocalSignInForm({
const empRes = await fetch(`${API_BASE}/auth/employee/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
})
const empJson = await empRes.json()
if (empRes.ok && empJson?.data?.token) {
const token = empJson.data.token
if (empRes.ok && empJson?.data?.employee) {
const targetPath = toPublicDashboardPath(employeeRedirect)
localStorage.setItem(EMPLOYEE_TOKEN_KEY, token)
if (empJson?.data?.employee) {
localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee))
const prefLang = empJson.data.employee?.preferredLanguage
@@ -251,7 +250,6 @@ function LocalSignInForm({
document.cookie = `rentaldrivego-language=${prefLang}; path=/; max-age=31536000; samesite=lax`
}
}
document.cookie = `employee_token=${token}; path=/; max-age=28800; samesite=lax`
window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed'))
notifyParent({ type: 'rentaldrivego:employee-login', path: targetPath })
// Use a document navigation here so the authenticated dashboard bootstraps
@@ -268,12 +266,13 @@ function LocalSignInForm({
const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
})
const adminJson = await adminRes.json()
if (adminRes.ok && adminJson?.data?.token) {
redirectAdmin(adminJson.data.token)
if (adminRes.ok && adminJson?.data?.admin) {
window.location.href = `${adminUrl}/dashboard`
return
}
@@ -296,15 +295,21 @@ function LocalSignInForm({
setError(null)
try {
const normalizedCode = totpCode.trim().toUpperCase()
const secondFactor = /^\d{6}$/.test(normalizedCode)
? { totpCode: normalizedCode }
: { recoveryCode: normalizedCode }
const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, totpCode }),
credentials: 'include',
body: JSON.stringify({ email, password, ...secondFactor }),
})
const adminJson = await adminRes.json()
if (adminRes.ok && adminJson?.data?.token) {
redirectAdmin(adminJson.data.token)
if (adminRes.ok && adminJson?.data?.admin) {
window.location.href = `${adminUrl}/dashboard`
return
}
@@ -393,13 +398,13 @@ function LocalSignInForm({
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">{dict.authCode}</label>
<input
type="text"
inputMode="numeric"
pattern="[0-9]{6}"
maxLength={6}
inputMode="text"
pattern="[0-9A-Za-z-]{6,14}"
maxLength={14}
required
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
placeholder="000000"
onChange={(e) => setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase())}
placeholder="000000 or XXXX-XXXX-XXXX"
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-center text-xl tracking-[0.45em] text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
/>
</div>