diff --git a/src/consents/consents.controller.ts b/src/consents/consents.controller.ts new file mode 100644 index 0000000..19a0960 --- /dev/null +++ b/src/consents/consents.controller.ts @@ -0,0 +1,81 @@ +import { Body, Controller, Get, IsString, Post } from '@nestjs/common'; +import { and, desc, eq, isNull } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { consentRecords } from '../db/schema'; + +class GrantConsentDto { + @IsString() purpose!: string; +} + +class RevokeConsentDto { + @IsString() purpose!: string; +} + +@Controller('consents') +export class ConsentsController { + @Get() + async list(@CurrentSession() session: AuthenticatedSession) { + return db.query.consentRecords.findMany({ + where: and( + eq(consentRecords.tenantId, session.tenantId), + eq(consentRecords.userId, session.userId), + ), + orderBy: [desc(consentRecords.grantedAt)], + }); + } + + @Post('grant') + async grant( + @CurrentSession() session: AuthenticatedSession, + @Body() dto: GrantConsentDto, + ) { + // Revoke any previous record for this purpose first + const existing = await db.query.consentRecords.findFirst({ + where: and( + eq(consentRecords.tenantId, session.tenantId), + eq(consentRecords.userId, session.userId), + eq(consentRecords.purpose, dto.purpose), + isNull(consentRecords.revokedAt), + ), + }); + if (existing) { + return existing; + } + const [created] = await db + .insert(consentRecords) + .values({ + tenantId: session.tenantId, + userId: session.userId, + purpose: dto.purpose, + grantedAt: new Date(), + }) + .returning(); + return created; + } + + @Post('revoke') + async revoke( + @CurrentSession() session: AuthenticatedSession, + @Body() dto: RevokeConsentDto, + ) { + const active = await db.query.consentRecords.findFirst({ + where: and( + eq(consentRecords.tenantId, session.tenantId), + eq(consentRecords.userId, session.userId), + eq(consentRecords.purpose, dto.purpose), + isNull(consentRecords.revokedAt), + ), + }); + if (!active) { + return { revoked: false, reason: 'no active consent for this purpose' }; + } + const [updated] = await db + .update(consentRecords) + .set({ revokedAt: new Date() }) + .where(eq(consentRecords.id, active.id)) + .returning(); + return { revoked: true, record: updated }; + } +}