import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import type { DataVisibility } from '@mento/shared'; import { BillingService } from '../billing/billing.service'; import { type Actor, normalizeDataVisibility, parseVisibilityScopes, } from '../common/record-scope'; import { PrismaService } from '../prisma/prisma.service'; import { DATA_SCOPES, canViewAllRecords } from '@mento/shared'; export type JwtPayload = { sub: string; organizationId: string; role: string; }; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( config: ConfigService, private readonly prisma: PrismaService, private readonly billing: BillingService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: config.get('JWT_SECRET') ?? 'dev-secret', }); } async validate(payload: JwtPayload & { typ?: string; mobile?: string }) { if ( payload.typ === 'portal' || payload.typ === 'org-select' || payload.typ === 'platform-admin' ) { throw new UnauthorizedException(); } if (!payload.sub || !payload.organizationId) { throw new UnauthorizedException(); } const user = await this.prisma.user.findUnique({ where: { id: payload.sub }, select: { id: true, isActive: true }, }); if (!user || user.isActive === false) { throw new UnauthorizedException(); } const membership = await this.prisma.membership.findFirst({ where: { userId: user.id, organizationId: payload.organizationId, isActive: true, organization: { isActive: true }, }, select: { organizationId: true, role: true, dataVisibility: true, dataVisibilityScopes: true, priorArchiveEnabled: true, priorArchiveLens: true, organization: { select: { enabledModules: true, priorArchiveEnabledAt: true } }, }, }); if (!membership) { throw new UnauthorizedException(); } await this.billing.requireActiveForOrganization(membership.organizationId); const dataVisibility = normalizeDataVisibility(membership.dataVisibility); const scopeLevels = parseVisibilityScopes(membership.dataVisibilityScopes); const elevatedRole = canViewAllRecords(membership.role); let teamIds: string[] | undefined; const resolve = async (level: DataVisibility): Promise => { if (elevatedRole || level === 'org') return null; if (level === 'own') return [user.id]; teamIds ??= await this.teamPeerIds(membership.organizationId, user.id); return teamIds; }; const visibleUserIds = await resolve(dataVisibility); const scopes: Actor['scopes'] = {}; if (!elevatedRole) { for (const scope of DATA_SCOPES) { const level = scopeLevels[scope]; if (!level || level === dataVisibility) continue; scopes[scope] = { dataVisibility: level, visibleUserIds: await resolve(level) }; } } const modules = this.billing.normalizeModules(membership.organization.enabledModules); const priorGranted = modules.includes('prior_data_archive') && membership.priorArchiveEnabled; const priorLens = membership.priorArchiveLens === 'archive' ? 'archive' : 'current'; return { userId: user.id, organizationId: membership.organizationId, role: membership.role, dataVisibility, visibleUserIds, scopes, priorLens: priorGranted ? priorLens : 'off', priorCutoff: priorGranted && membership.organization.priorArchiveEnabledAt ? membership.organization.priorArchiveEnabledAt : null, }; } private async teamPeerIds(organizationId: string, userId: string): Promise { const memberships = await this.prisma.teamMember.findMany({ where: { userId, team: { organizationId }, }, select: { teamId: true }, }); if (!memberships.length) return [userId]; const peers = await this.prisma.teamMember.findMany({ where: { teamId: { in: memberships.map((m) => m.teamId) }, }, select: { userId: true }, }); return [...new Set([userId, ...peers.map((p) => p.userId)])]; } }