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 { resolveAllSectionAccess, sectionNeedsTeamPeers, } from '../common/data-access'; 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; impersonatorUserId?: string; viewAsReadOnly?: boolean; }; @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; iat?: number }) { if ( payload.typ === 'portal' || payload.typ === 'org-select' || payload.typ === 'platform-admin' ) { throw new UnauthorizedException(); } if (!payload.sub || !payload.organizationId) { throw new UnauthorizedException(); } let impersonatorUserId: string | undefined; let impersonatorRole: string | undefined; const viewAsReadOnly = Boolean(payload.viewAsReadOnly && payload.impersonatorUserId); if (viewAsReadOnly && payload.impersonatorUserId) { const ownerMembership = await this.prisma.membership.findFirst({ where: { userId: payload.impersonatorUserId, organizationId: payload.organizationId, isActive: true, role: 'owner', organization: { isActive: true }, }, select: { userId: true, role: true }, }); if (!ownerMembership) { throw new UnauthorizedException(); } impersonatorUserId = ownerMembership.userId; impersonatorRole = ownerMembership.role; } 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, dataAccessOverride: true, priorArchiveEnabled: true, priorArchiveLens: true, orgRole: { select: { dataAccessPolicy: 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 deepAccess = resolveAllSectionAccess({ role: membership.role, rolePolicy: membership.orgRole?.dataAccessPolicy, override: membership.dataAccessOverride, legacyVisibility: dataVisibility, legacyScopes: scopeLevels, }); const needsTeamPeers = !elevatedRole && (sectionNeedsTeamPeers(deepAccess.customers) || sectionNeedsTeamPeers(deepAccess.leads) || sectionNeedsTeamPeers(deepAccess.sales) || sectionNeedsTeamPeers(deepAccess.activities) || sectionNeedsTeamPeers(deepAccess.notes) || sectionNeedsTeamPeers(deepAccess.routines) || sectionNeedsTeamPeers(deepAccess.projects) || sectionNeedsTeamPeers(deepAccess.tickets)); if (needsTeamPeers) { teamIds ??= await this.teamPeerIds(membership.organizationId, user.id); } 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, customersAccess: deepAccess.customers, leadsAccess: deepAccess.leads, salesAccess: deepAccess.sales, activitiesAccess: deepAccess.activities, notesAccess: deepAccess.notes, routinesAccess: deepAccess.routines, projectsAccess: deepAccess.projects, ticketsAccess: deepAccess.tickets, teamPeerIds: teamIds ?? (elevatedRole ? null : [user.id]), priorLens: priorGranted ? priorLens : 'off', priorCutoff: priorGranted && membership.organization.priorArchiveEnabledAt ? membership.organization.priorArchiveEnabledAt : null, sessionStartedAt: payload.iat ? new Date(payload.iat * 1000) : new Date(), ...(viewAsReadOnly && impersonatorUserId ? { impersonatorUserId, impersonatorRole: impersonatorRole ?? 'owner', viewAsReadOnly: true, } : {}), }; } 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)])]; } }