Files
yara724api/src/auth/auth-services/user.auth.service.ts
SepehrYahyaee ed2b6948cf FAKE SMS
2026-06-23 10:26:51 +03:30

193 lines
5.6 KiB
TypeScript

import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Types } from "mongoose";
import {
UserAuthErrorCode,
throwUserAuthError,
} 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,
FAKE_OTP_CODE,
isFakeOtpEnabled,
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";
import { HashService } from "src/utils/hash/hash.service";
export interface LinkBinding {
linkToken?: string;
linkContext?: string;
}
@Injectable()
export class UserAuthService {
private readonly logger = new Logger(UserAuthService.name);
constructor(
private readonly jwtService: JwtService,
private readonly userDbService: UserDbService,
private readonly hashService: HashService,
private readonly otpCreator: OtpGeneratorService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly userLinkAccessService: UserLinkAccessService,
) {}
async validateUser(
username: string,
pass: string,
binding: LinkBinding = {},
): Promise<any> {
const canonicalMobile = normalizeIranMobile(username) ?? username.trim();
await this.userLinkAccessService.assertMobileAllowed({
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const user = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
if (!user) throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
if (user.otp == null) throwUserAuthError(UserAuthErrorCode.OTP_REQUIRED);
if (!isOtpExpiryActive(user.otpExpire)) {
throwUserAuthError(UserAuthErrorCode.OTP_EXPIRED);
}
if (await this.hashService.compare(pass, user.otp)) {
return user;
}
return false;
}
async login(user: any) {
const userId = String(user._id ?? user.id ?? "");
const payload = {
username: user.username,
sub: userId,
role: "user",
};
const accToken = this.jwtService.sign(payload, {
secret: `${process.env.JWT_SECRET}`,
});
await this.userDbService.findOneAndUpdate(
{ username: user.username },
{
tokens: { token: accToken },
otp: null,
otpExpire: 0,
},
);
return {
userId,
access_token: accToken,
};
}
async sendOtpRequest(
mobile: string,
binding: LinkBinding = {},
): Promise<LoginDtoRs> {
const canonicalMobile = normalizeIranMobile(mobile) ?? mobile.trim();
if (!canonicalMobile) {
throwUserAuthError(UserAuthErrorCode.USER_NOT_FOUND);
}
await this.userLinkAccessService.assertMobileAllowed({
mobile: canonicalMobile,
linkToken: binding.linkToken,
linkContext: binding.linkContext,
});
const userExist = await this.userDbService.findOne(
buildUserLookupByPhone(canonicalMobile),
);
const otp = this.createOtpForRequest();
const hashOtp = await this.hashService.hash(otp);
const expireMinutes = readOtpExpireMinutesFromEnv();
const nowMs = Date.now();
const otpExpire = computeOtpExpireMs(expireMinutes, nowMs);
if (!userExist) {
await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
const newUser = await this.userDbService.createUser({
mobile: canonicalMobile,
username: canonicalMobile,
otp: hashOtp,
tokens: {
token: "",
rfToken: "",
},
fullName: "",
nationalCode: "",
lastLogin: new Date(),
clientKey: new Types.ObjectId(),
birthDay: "",
city: "",
address: "",
state: "",
otpExpire,
});
return new LoginDtoRs(newUser);
}
if (isOtpExpiryActive(userExist.otpExpire, nowMs)) {
// throwUserAuthError(UserAuthErrorCode.OTP_REQUEST_TOO_SOON);
return new LoginDtoRs(userExist, "OTP Still valid");
}
await this.smsSender(otp, canonicalMobile);
// console.log(`OTP for ${canonicalMobile}: ${otp}`);
await this.userDbService.findOneAndUpdate(
buildUserLookupByPhone(canonicalMobile),
{
otp: hashOtp,
otpExpire,
mobile: canonicalMobile,
username: userExist.username || canonicalMobile,
},
);
return new LoginDtoRs(userExist);
}
private createOtpForRequest(): string {
if (isFakeOtpEnabled()) {
this.logger.warn(
"FAKE_OTP=true — using fixed dev OTP; SMS provider is not called",
);
return FAKE_OTP_CODE;
}
return this.otpCreator.create();
}
private async smsSender(otp: string, mobile: string) {
if (isFakeOtpEnabled()) {
this.logger.log(
`FAKE_OTP=true — skipped SMS for phone=${mobile} (use OTP ${FAKE_OTP_CODE})`,
);
return;
}
const ok = await this.smsOrchestrationService.sendAuthOtp(
mobile,
otp,
process.env.AUTH_SMS_TEMPLATE,
);
if (!ok) {
throw new HttpException("auth sms send failed", HttpStatus.BAD_GATEWAY);
}
this.logger.log(
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
);
}
}