Fixed resendCarParts label_fa's + OTP

This commit is contained in:
SepehrYahyaee
2026-05-25 14:13:17 +03:30
parent 64fa560f73
commit 680f3c1798
10 changed files with 435 additions and 84 deletions

View File

@@ -5,7 +5,9 @@ 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";
@@ -20,6 +22,8 @@ export class UserLinkAccessService {
private readonly blameRequestModel: Model<BlameRequest>,
@InjectModel(ClaimRequestManagementModel.name)
private readonly claimRequestManagementModel: Model<ClaimRequestManagementModel>,
@InjectModel(ClaimCase.name)
private readonly claimCaseModel: Model<ClaimCase>,
private readonly userDbService: UserDbService,
) {}
@@ -39,10 +43,12 @@ export class UserLinkAccessService {
throwUserAuthError(UserAuthErrorCode.LINK_NOT_FOUND);
}
const normalizedMobile = this.normalizePhone(params.mobile);
const normalizedMobile = normalizeIranMobile(params.mobile);
if (
!normalizedMobile ||
!allowedMobiles.some((mobile) => this.normalizePhone(mobile) === normalizedMobile)
!allowedMobiles.some(
(mobile) => normalizeIranMobile(mobile) === normalizedMobile,
)
) {
throwUserAuthError(UserAuthErrorCode.LINK_MOBILE_MISMATCH);
}
@@ -58,15 +64,18 @@ export class UserLinkAccessService {
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(),
]);
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.addClaimOwnerPhone(allowedMobiles, claimRequest);
await this.addLegacyClaimOwnerPhone(allowedMobiles, legacyClaim);
await this.addClaimCaseOwnerPhone(allowedMobiles, claimCase);
return Array.from(allowedMobiles);
}
@@ -119,7 +128,7 @@ export class UserLinkAccessService {
}
}
private async addClaimOwnerPhone(
private async addLegacyClaimOwnerPhone(
allowedMobiles: Set<string>,
claimRequest: any,
) {
@@ -146,29 +155,51 @@ export class UserLinkAccessService {
_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<string>,
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<string>, phone?: string) {
const normalized = this.normalizePhone(phone);
const normalized = normalizeIranMobile(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();
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 {

View File

@@ -7,6 +7,15 @@ import {
} from "src/auth/auth-services/user-auth-error";
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
import { LoginDtoRs } from "src/auth/dto/user/login.dto";
import {
buildUserLookupByPhone,
normalizeIranMobile,
} from "src/helpers/iran-mobile";
import {
computeOtpExpireMs,
isOtpExpiryActive,
readOtpExpireMinutesFromEnv,
} from "src/helpers/user-otp-expiry";
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
@@ -17,7 +26,6 @@ export interface LinkBinding {
linkContext?: string;
}
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
@Injectable()
export class UserAuthService {
private readonly logger = new Logger(UserAuthService.name);
@@ -36,18 +44,21 @@ export class UserAuthService {
pass: string,
binding: LinkBinding = {},
): Promise<any> {
const canonicalMobile = normalizeIranMobile(username) ?? username.trim();
await this.userLinkAccessService.assertMobileAllowed({
mobile: username,
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const user = await this.userDbService.findOne({ username });
const user = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
if (!user) throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
const now = new Date().getTime();
if (user.otp == null) throwUserAuthError(UserAuthErrorCode.OTP_REQUIRED);
if (user.otpExpire < now) {
if (!isOtpExpiryActive(user.otpExpire)) {
throwUserAuthError(UserAuthErrorCode.OTP_EXPIRED);
}
if (await this.hashService.compare(pass, user.otp)) {
@@ -57,9 +68,10 @@ export class UserAuthService {
}
async login(user: any) {
const userId = String(user._id ?? user.id ?? "");
const payload = {
username: user.username,
sub: user.id,
sub: userId,
role: "user",
};
const accToken = this.jwtService.sign(payload, {
@@ -70,10 +82,11 @@ export class UserAuthService {
{
tokens: { token: accToken },
otp: null,
otpExpire: 0,
},
);
return {
userId: user._id,
userId,
access_token: accToken,
};
}
@@ -82,29 +95,31 @@ export class UserAuthService {
mobile: string,
binding: LinkBinding = {},
): Promise<LoginDtoRs> {
const canonicalMobile = normalizeIranMobile(mobile) ?? mobile.trim();
if (!canonicalMobile) {
throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
}
await this.userLinkAccessService.assertMobileAllowed({
mobile,
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const userExist = await this.userDbService.findOne({
mobile,
});
const userExist = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
const otp = this.otpCreator.create();
const hashOtp = await this.hashService.hash(otp);
const rawExpireMinutes = Number(process.env.EXP_OTP_TIME ?? "2");
const expireMinutes =
Number.isFinite(rawExpireMinutes) && rawExpireMinutes > 0
? rawExpireMinutes
: 2;
const otpExpire = Date.now() + expireMinutes * 60 * 1000;
const expireMinutes = readOtpExpireMinutesFromEnv();
const nowMs = Date.now();
const otpExpire = computeOtpExpireMs(expireMinutes, nowMs);
if (!userExist) {
await this.smsSender(otp, mobile);
/// create otp request
await this.smsSender(otp, canonicalMobile);
const newUser = await this.userDbService.createUser({
mobile,
username: mobile,
mobile: canonicalMobile,
username: canonicalMobile,
otp: hashOtp,
tokens: {
token: "",
@@ -122,22 +137,22 @@ export class UserAuthService {
});
return new LoginDtoRs(newUser);
}
if (userExist) {
if (userExist.otpExpire > new Date(new Date().getTime()).getTime()) {
throwUserAuthError(UserAuthErrorCode.OTP_REQUEST_TOO_SOON);
}
await this.smsSender(otp, mobile);
const updateTokens = await this.userDbService.findOneAndUpdate(
{
username: userExist.username,
},
{
otp: hashOtp,
otpExpire,
},
);
if (updateTokens) return new LoginDtoRs(userExist);
if (isOtpExpiryActive(userExist.otpExpire, nowMs)) {
throwUserAuthError(UserAuthErrorCode.OTP_REQUEST_TOO_SOON);
}
await this.smsSender(otp, canonicalMobile);
await this.userDbService.findOneAndUpdate(
buildUserLookupByPhone(canonicalMobile),
{
otp: hashOtp,
otpExpire,
mobile: canonicalMobile,
username: userExist.username || canonicalMobile,
},
);
return new LoginDtoRs(userExist);
}
private async smsSender(otp: string, mobile: string) {

View File

@@ -9,6 +9,10 @@ import { UserAuthController } from "src/auth/auth-controllers/user/user.auth.con
import { ActorAuthService } from "src/auth/auth-services/actor.auth.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
import { UserLinkAccessService } from "src/auth/auth-services/user-link-access.service";
import {
ClaimCase,
ClaimCaseSchema,
} from "src/claim-request-management/entites/schema/claim-cases.schema";
import {
ClaimRequestManagementModel,
ClaimRequestManagementSchema,
@@ -44,6 +48,7 @@ import { CaptchaModule } from "src/captcha/captcha.module";
name: ClaimRequestManagementModel.name,
schema: ClaimRequestManagementSchema,
},
{ name: ClaimCase.name, schema: ClaimCaseSchema },
]),
JwtModule.register({
signOptions: { expiresIn: "1h" },