good forst commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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<MemberProfile> {
|
||||
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<MemberGroupSummary[]> {
|
||||
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<MemberGroupSummary> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user