import { BadRequestException, Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import { AuditService } from '../audit/audit.service'; import { AuditAction } from '../audit/audit.types'; import { ConsentScope, MemberGroupSummary, MemberOptOutReason, MemberProfile, OptInRequest, OptOutRequest } from '@tower/types'; import { ConsentStatus, MemberOptOutReason as MemberOptOutReasonEnum } from '@prisma/client'; @Injectable() export class MyService { private readonly logger = new Logger(MyService.name); constructor( private readonly prisma: PrismaService, private readonly audit: AuditService, ) {} async getProfile(userId: string, tenantId: string): Promise { const user = await this.prisma.towerUser.findFirst({ where: { id: userId, tenantId } }); if (!user) throw new NotFoundException('User not found'); return { id: user.id, tenantId: user.tenantId, jid: user.jid, displayName: user.displayName, createdAt: user.createdAt.toISOString(), }; } async listGroups(userId: string, tenantId: string): Promise { const consents = await this.prisma.consentRecord.findMany({ where: { userId, tenantId }, include: { group: true }, }); return consents.map((c) => ({ id: c.group.id, name: c.group.name, tenantId: c.tenantId, scopes: c.scopes as ConsentScope[], retentionDays: c.retentionDays, policyVersion: c.policyVersion, consentStatus: c.status as ConsentStatus, joinedAt: c.effectiveAt.toISOString(), })); } async getGroup(userId: string, tenantId: string, groupId: string): Promise { const consent = await this.prisma.consentRecord.findFirst({ where: { userId, tenantId, groupId }, include: { group: true }, }); if (!consent) throw new NotFoundException('Not a member of this group'); return { id: consent.group.id, name: consent.group.name, tenantId: consent.tenantId, scopes: consent.scopes as ConsentScope[], retentionDays: consent.retentionDays, policyVersion: consent.policyVersion, consentStatus: consent.status as ConsentStatus, joinedAt: consent.effectiveAt.toISOString(), }; } async optOut( userId: string, tenantId: string, body: OptOutRequest, ): Promise<{ ok: true; revoked: number }> { const where = body.groupId ? { userId, tenantId, groupId: body.groupId } : { userId, tenantId }; const consents = await this.prisma.consentRecord.findMany({ where }); if (consents.length === 0) { throw new NotFoundException('No matching consent records'); } const reason = body.reason ?? MemberOptOutReasonEnum.SELF_PORTAL; await this.prisma.$transaction(async (tx) => { for (const consent of consents) { if (body.scopes && body.scopes.length > 0) { const remaining = (consent.scopes as ConsentScope[]).filter((s) => !body.scopes!.includes(s)); if (remaining.length === 0) { await tx.consentRecord.update({ where: { id: consent.id }, data: { status: ConsentStatus.REVOKED, revokedAt: new Date() }, }); } else { await tx.consentRecord.update({ where: { id: consent.id }, data: { scopes: remaining }, }); } } else { await tx.consentRecord.update({ where: { id: consent.id }, data: { status: ConsentStatus.REVOKED, revokedAt: new Date() }, }); } await tx.memberOptOut.create({ data: { tenantId, userId, groupId: consent.groupId, reason, notes: body.notes ?? null, }, }); } }); await this.audit.log({ tenantId, action: AuditAction.MEMBER_OPT_OUT, resourceType: 'TowerUser', resourceId: userId, payload: { groupId: body.groupId, scopes: body.scopes, reason }, }); return { ok: true, revoked: consents.length }; } async optIn( userId: string, tenantId: string, body: OptInRequest, ): Promise<{ ok: true; consentId: string }> { if (body.scopes.length === 0) { throw new BadRequestException('At least one scope is required'); } const group = await this.prisma.group.findFirst({ where: { id: body.groupId, tenantId } }); if (!group) throw new NotFoundException('Group not found in your tenant'); const user = await this.prisma.towerUser.findFirst({ where: { id: userId, tenantId } }); if (!user) throw new UnauthorizedException('User not found'); const existing = await this.prisma.consentRecord.findFirst({ where: { userId, tenantId, groupId: body.groupId }, }); let consent; if (existing) { consent = await this.prisma.consentRecord.update({ where: { id: existing.id }, data: { scopes: body.scopes, retentionDays: body.retentionDays ?? existing.retentionDays, status: ConsentStatus.GRANTED, revokedAt: null, effectiveAt: new Date(), }, }); } else { consent = await this.prisma.consentRecord.create({ data: { tenantId, groupId: body.groupId, userId, scopes: body.scopes, retentionDays: body.retentionDays ?? 90, policyVersion: 'v1', status: ConsentStatus.GRANTED, proofEventId: 'self', }, }); } await this.audit.log({ tenantId, action: AuditAction.MEMBER_OPT_IN, resourceType: 'TowerUser', resourceId: userId, payload: { groupId: body.groupId, scopes: body.scopes }, }); return { ok: true, consentId: consent.id }; } async deleteAccount(userId: string, tenantId: string): Promise<{ ok: true }> { await this.prisma.$transaction(async (tx) => { await tx.consentRecord.deleteMany({ where: { userId, tenantId } }); await tx.memberOptOut.deleteMany({ where: { userId, tenantId } }); await tx.towerSession.deleteMany({ where: { userId } }); await tx.towerUser.delete({ where: { id: userId } }); }); await this.audit.log({ tenantId, action: AuditAction.MEMBER_DELETED, resourceType: 'TowerUser', resourceId: userId, }); return { ok: true }; } }