Compare commits
2 commits
main
...
feat/story
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0ded1f482 | ||
|
|
8af5354f22 |
7 changed files with 79 additions and 0 deletions
|
|
@ -8,6 +8,41 @@ vi.mock('@/lib/auth', () => ({ getSession: getSessionMock }))
|
|||
vi.mock('@/lib/auth/pairing', () => ({ isPairedSessionExpired: isPairedSessionExpiredMock }))
|
||||
vi.mock('next/navigation', () => ({ redirect: redirectMock }))
|
||||
|
||||
describe('requireAdmin', () => {
|
||||
beforeEach(() => {
|
||||
getSessionMock.mockReset()
|
||||
isPairedSessionExpiredMock.mockReset()
|
||||
redirectMock.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('redirect /dashboard als userId ontbreekt', async () => {
|
||||
getSessionMock.mockResolvedValue({ userId: undefined, isAdmin: false })
|
||||
const { requireAdmin } = await import('@/lib/auth-guard')
|
||||
await expect(requireAdmin()).rejects.toThrow('REDIRECT_CALLED')
|
||||
expect(redirectMock).toHaveBeenCalledWith('/dashboard')
|
||||
})
|
||||
|
||||
it('redirect /dashboard als isAdmin false is', async () => {
|
||||
getSessionMock.mockResolvedValue({ userId: 'u1', isAdmin: false })
|
||||
const { requireAdmin } = await import('@/lib/auth-guard')
|
||||
await expect(requireAdmin()).rejects.toThrow('REDIRECT_CALLED')
|
||||
expect(redirectMock).toHaveBeenCalledWith('/dashboard')
|
||||
})
|
||||
|
||||
it('geeft sessie terug als isAdmin true is', async () => {
|
||||
const sess = { userId: 'u1', isAdmin: true }
|
||||
getSessionMock.mockResolvedValue(sess)
|
||||
const { requireAdmin } = await import('@/lib/auth-guard')
|
||||
const result = await requireAdmin()
|
||||
expect(result).toBe(sess)
|
||||
expect(redirectMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('requireSession', () => {
|
||||
beforeEach(() => {
|
||||
getSessionMock.mockReset()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { redirect } from 'next/navigation'
|
|||
import { cookies, headers } from 'next/headers'
|
||||
import { getIronSession } from 'iron-session'
|
||||
import { z } from 'zod'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { registerUser, verifyUser } from '@/lib/auth'
|
||||
import { SessionData, sessionOptions } from '@/lib/session'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
|
@ -45,6 +46,7 @@ export async function registerAction(_prevState: unknown, formData: FormData) {
|
|||
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
session.userId = result.user!.id
|
||||
session.isDemo = false
|
||||
session.isAdmin = false
|
||||
await session.save()
|
||||
|
||||
redirect('/dashboard')
|
||||
|
|
@ -70,11 +72,22 @@ export async function loginAction(_prevState: unknown, formData: FormData) {
|
|||
return { error: 'Onjuiste gebruikersnaam of wachtwoord' }
|
||||
}
|
||||
|
||||
const adminRole = await prisma.userRole.findFirst({
|
||||
where: { user_id: user.id, role: 'ADMIN' },
|
||||
})
|
||||
|
||||
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
session.userId = user.id
|
||||
session.isDemo = user.is_demo
|
||||
session.isAdmin = !!adminRole
|
||||
await session.save()
|
||||
|
||||
if (user.must_reset_password) {
|
||||
redirect('/reset-password')
|
||||
} else if (adminRole) {
|
||||
redirect('/admin')
|
||||
}
|
||||
|
||||
// PBI-11 / ST-1135: telefoon-UA's krijgen de mobile-shell.
|
||||
// Tablets en desktop volgen het bestaande /dashboard-pad.
|
||||
const ua = (await headers()).get('user-agent')
|
||||
|
|
|
|||
16
app/(app)/admin/layout.tsx
Normal file
16
app/(app)/admin/layout.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { requireAdmin } from '@/lib/auth-guard'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
await requireAdmin()
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<nav className="w-48 border-r p-4 flex flex-col gap-2">
|
||||
<Link href="/admin/users">Gebruikers</Link>
|
||||
<Link href="/admin/jobs">Claude Jobs</Link>
|
||||
<Link href="/admin/products">Producten</Link>
|
||||
</nav>
|
||||
<main className="flex-1 p-6">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
5
app/(app)/admin/page.tsx
Normal file
5
app/(app)/admin/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AdminPage() {
|
||||
redirect('/admin/users')
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ export async function POST(request: Request) {
|
|||
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
session.userId = pairing.user_id
|
||||
session.isDemo = pairing.user?.is_demo ?? false
|
||||
session.isAdmin = false
|
||||
session.paired = true
|
||||
session.pairedExpiresAt = Date.now() + PAIRED_TTL_MS
|
||||
await session.save()
|
||||
|
|
|
|||
|
|
@ -22,3 +22,11 @@ export async function requireSession() {
|
|||
|
||||
return session
|
||||
}
|
||||
|
||||
export async function requireAdmin() {
|
||||
const session = await getSession()
|
||||
if (!session.userId || !session.isAdmin) {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { SessionOptions } from 'iron-session'
|
|||
export interface SessionData {
|
||||
userId: string
|
||||
isDemo: boolean
|
||||
isAdmin: boolean
|
||||
// ST-1002 (M10) — gezet door /api/auth/pair/claim na een succesvolle QR-pairing.
|
||||
// Beide velden zijn optioneel zodat bestaande wachtwoord-sessies onveranderd blijven.
|
||||
paired?: boolean
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue