1
0
forked from Yara724/api

Added linkToken and linkContext to OTP

This commit is contained in:
SepehrYahyaee
2026-05-13 09:23:45 +03:30
parent e26c533a52
commit 5b6409fc2e
9 changed files with 344 additions and 28 deletions

View File

@@ -0,0 +1,187 @@
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 { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
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<RequestManagementModel>,
@InjectModel(BlameRequest.name)
private readonly blameRequestModel: Model<BlameRequest>,
@InjectModel(ClaimRequestManagementModel.name)
private readonly claimRequestManagementModel: Model<ClaimRequestManagementModel>,
private readonly userDbService: UserDbService,
) {}
async assertMobileAllowed(params: {
mobile: string;
linkToken?: string;
linkContext?: string;
}): Promise<void> {
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 = this.normalizePhone(params.mobile);
if (
!normalizedMobile ||
!allowedMobiles.some((mobile) => this.normalizePhone(mobile) === normalizedMobile)
) {
throwUserAuthError(UserAuthErrorCode.LINK_MOBILE_MISMATCH);
}
}
async resolveAllowedMobiles(
linkToken: string,
linkContext?: string,
): Promise<string[]> {
if (!Types.ObjectId.isValid(linkToken)) return [];
const id = new Types.ObjectId(linkToken);
const context = this.normalizeContext(linkContext);
const allowedMobiles = new Set<string>();
const [legacyRequest, blameRequest, claimRequest] = await Promise.all([
this.requestManagementModel.findById(id).lean().exec(),
this.blameRequestModel.findById(id).lean().exec(),
this.claimRequestManagementModel.findById(id).lean().exec(),
]);
this.addLegacyRequestPhones(allowedMobiles, legacyRequest, context);
this.addBlameRequestPhones(allowedMobiles, blameRequest, context);
await this.addClaimOwnerPhone(allowedMobiles, claimRequest);
return Array.from(allowedMobiles);
}
private addLegacyRequestPhones(
allowedMobiles: Set<string>,
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<string>,
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 addClaimOwnerPhone(
allowedMobiles: Set<string>,
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);
}
}
private addPhone(allowedMobiles: Set<string>, phone?: string) {
const normalized = this.normalizePhone(phone);
if (normalized) allowedMobiles.add(normalized);
}
private normalizePhone(phone?: string): string | undefined {
if (!phone) return undefined;
const digits = String(phone).replace(/\D/g, "");
if (!digits) return undefined;
if (digits.startsWith("0098")) return `0${digits.slice(4)}`;
if (digits.startsWith("98") && digits.length === 12) {
return `0${digits.slice(2)}`;
}
if (digits.length === 10 && digits.startsWith("9")) return `0${digits}`;
return digits;
}
private normalizeContext(linkContext?: string): string | undefined {
return linkContext?.trim().toUpperCase();
}
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);
}
}