import { Injectable } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { Model, Types } from "mongoose"; import { UserAuthErrorCode, throwUserAuthError, } from "src/auth/auth-services/user-auth-error"; import { ClaimCase } from "src/claim-request-management/entites/schema/claim-cases.schema"; import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema"; import { normalizeIranMobile } from "src/helpers/iran-mobile"; import { BlameRequest } from "src/request-management/entities/schema/blame-cases.schema"; import { PartyRole } from "src/request-management/entities/schema/partyRole.enum"; import { RequestManagementModel } from "src/request-management/entities/schema/request-management.schema"; import { UserDbService } from "src/users/entities/db-service/user.db.service"; @Injectable() export class UserLinkAccessService { constructor( @InjectModel(RequestManagementModel.name) private readonly requestManagementModel: Model, @InjectModel(BlameRequest.name) private readonly blameRequestModel: Model, @InjectModel(ClaimRequestManagementModel.name) private readonly claimRequestManagementModel: Model, @InjectModel(ClaimCase.name) private readonly claimCaseModel: Model, private readonly userDbService: UserDbService, ) {} async assertMobileAllowed(params: { mobile: string; linkToken?: string; linkContext?: string; }): Promise { const linkToken = params.linkToken?.trim(); if (!linkToken) return; const allowedMobiles = await this.resolveAllowedMobiles( linkToken, params.linkContext, ); if (allowedMobiles.length === 0) { throwUserAuthError(UserAuthErrorCode.LINK_NOT_FOUND); } const normalizedMobile = normalizeIranMobile(params.mobile); if ( !normalizedMobile || !allowedMobiles.some( (mobile) => normalizeIranMobile(mobile) === normalizedMobile, ) ) { throwUserAuthError(UserAuthErrorCode.LINK_MOBILE_MISMATCH); } } async resolveAllowedMobiles( linkToken: string, linkContext?: string, ): Promise { if (!Types.ObjectId.isValid(linkToken)) return []; const id = new Types.ObjectId(linkToken); const context = this.normalizeContext(linkContext); const allowedMobiles = new Set(); const [legacyRequest, blameRequest, legacyClaim, claimCase] = await Promise.all([ this.requestManagementModel.findById(id).lean().exec(), this.blameRequestModel.findById(id).lean().exec(), this.claimRequestManagementModel.findById(id).lean().exec(), this.claimCaseModel.findById(id).lean().exec(), ]); this.addLegacyRequestPhones(allowedMobiles, legacyRequest, context); this.addBlameRequestPhones(allowedMobiles, blameRequest, context); await this.addLegacyClaimOwnerPhone(allowedMobiles, legacyClaim); await this.addClaimCaseOwnerPhone(allowedMobiles, claimCase); return Array.from(allowedMobiles); } private addLegacyRequestPhones( allowedMobiles: Set, legacyRequest: any, context?: string, ) { if (!legacyRequest) return; const shouldAddFirst = !context || this.isFirstContext(context); const shouldAddSecond = !context || this.isSecondContext(context); if (shouldAddFirst) { this.addPhone( allowedMobiles, legacyRequest.firstPartyDetails?.firstPartyPhoneNumber, ); } if (shouldAddSecond) { this.addPhone( allowedMobiles, legacyRequest.secondPartyDetails?.secondPartyPhoneNumber, ); } for (const event of legacyRequest.history || []) { const metadata = event?.metadata; if (shouldAddSecond) this.addPhone(allowedMobiles, metadata?.secondPartyPhone); for (const sent of metadata?.sentTo || []) { if (!context || this.matchesRoleContext(context, sent?.role)) { this.addPhone(allowedMobiles, sent?.phoneNumber); } } } } private addBlameRequestPhones( allowedMobiles: Set, blameRequest: any, context?: string, ) { if (!blameRequest) return; for (const party of blameRequest.parties || []) { if (context && !this.matchesRoleContext(context, party?.role)) continue; this.addPhone(allowedMobiles, party?.person?.phoneNumber); } } private async addLegacyClaimOwnerPhone( allowedMobiles: Set, claimRequest: any, ) { if (!claimRequest) return; const ownerUserId = claimRequest.owner?.userId || claimRequest.userId; if (!ownerUserId) return; const ownerUserIdText = String(ownerUserId); if (claimRequest.blameRequestId) { const blameRequest = await this.blameRequestModel .findById(claimRequest.blameRequestId) .lean() .exec(); const ownerParty = (blameRequest?.parties || []).find( (party: any) => party?.person?.userId && String(party.person.userId) === ownerUserIdText, ); this.addPhone(allowedMobiles, ownerParty?.person?.phoneNumber); } if (Types.ObjectId.isValid(ownerUserIdText)) { const user = await this.userDbService.findOne({ _id: new Types.ObjectId(ownerUserIdText), }); this.addPhone(allowedMobiles, user?.mobile); this.addPhone(allowedMobiles, user?.username); } } /** V2 `claimCases` — token in `/caseClaim?token=...` SMS links. */ private async addClaimCaseOwnerPhone( allowedMobiles: Set, claimCase: any, ) { if (!claimCase?.owner?.userId) return; const ownerUserIdText = String(claimCase.owner.userId); if (claimCase.blameRequestId) { const blameRequest = await this.blameRequestModel .findById(claimCase.blameRequestId) .lean() .exec(); const ownerParty = (blameRequest?.parties || []).find( (party: any) => party?.person?.userId && String(party.person.userId) === ownerUserIdText, ); this.addPhone(allowedMobiles, ownerParty?.person?.phoneNumber); } if (Types.ObjectId.isValid(ownerUserIdText)) { const user = await this.userDbService.findOne({ _id: new Types.ObjectId(ownerUserIdText), }); this.addPhone(allowedMobiles, user?.mobile); this.addPhone(allowedMobiles, user?.username); } } private addPhone(allowedMobiles: Set, phone?: string) { const normalized = normalizeIranMobile(phone); if (normalized) allowedMobiles.add(normalized); } private normalizeContext(linkContext?: string): string | undefined { const ctx = linkContext?.trim().toUpperCase(); if (!ctx) return undefined; if (ctx === "USER" || ctx === "USER1") return "FIRST"; if (ctx === "USER2") return "SECOND"; if (ctx === "CASECLAIM" || ctx === "CLAIM") return undefined; return ctx; } private matchesRoleContext(context: string, role?: string): boolean { if (this.isFirstContext(context)) return role === PartyRole.FIRST; if (this.isSecondContext(context)) return role === PartyRole.SECOND; return true; } private isFirstContext(context: string): boolean { return ["FIRST", "USER", "USER1", "FIRST_PARTY"].includes(context); } private isSecondContext(context: string): boolean { return ["SECOND", "USER2", "SECOND_PARTY"].includes(context); } }