Files
yara724api/src/auth/auth-services/actor.auth.service.ts
SepehrYahyaee af875a4773 YARA-913
2026-05-24 10:10:17 +03:30

606 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from "node:fs";
import {
BadGatewayException,
BadRequestException,
ForbiddenException,
Injectable,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Types } from "mongoose";
import { ForgetPasswordVerifyCodeDtoRs } from "src/auth/dto/actor/forget-password.actor.dto";
import { ProfileActor } from "src/auth/dto/actor/profile.actor.dto";
import {
InsurerRegisterDto,
RegisterDto,
RegisterDtoRs,
} from "src/auth/dto/actor/register.actor.dto";
import { StateListDtoRs } from "src/auth/dto/actor/states.dto";
import { CaptchaAccountService } from "src/auth/auth-services/captcha-account.service";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-expert.db.service";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
import { HashService } from "src/utils/hash/hash.service";
// import { MailService } from "src/utils/mail/mail.service";
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
function pick(obj: Record<string, any>, keys: string[]) {
const out: Record<string, any> = {};
for (const key of keys) {
if (obj[key] !== undefined) out[key] = obj[key];
}
return out;
}
// TODO FIX REGISTER TO ACTOR.SERVICE AND AUTH IN THIS MODULE
@Injectable()
export class ActorAuthService {
constructor(
private readonly jwtService: JwtService,
private readonly hashService: HashService,
private readonly expertDbService: ExpertDbService,
private readonly damageExpertDbService: DamageExpertDbService,
private readonly fieldExpertDbService: FieldExpertDbService,
private readonly registrarDbService: RegistrarDbService,
private readonly insurerExpertDbService: InsurerExpertDbService,
// private readonly mailService: MailService, // Mailer disabled not used
private readonly clientDbService: ClientDbService,
private readonly otpService: OtpGeneratorService,
private readonly captchaAccountService: CaptchaAccountService,
) {}
// TODO convrt to class for dynamic controller
public async dynamicDbController(role, username, userId?: string) {
let res;
switch (role) {
case RoleEnum.EXPERT:
if (username == null && userId)
res = await this.expertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.expertDbService.findOne({ email: username });
break;
case RoleEnum.DAMAGE_EXPERT:
if (username == null && userId)
res = await this.damageExpertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else
res = await this.damageExpertDbService.findOne({ email: username });
break;
case RoleEnum.FIELD_EXPERT:
if (username == null && userId)
res = await this.fieldExpertDbService.findOne({
_id: new Types.ObjectId(userId),
});
else
res = await this.fieldExpertDbService.findOne({ email: username });
break;
case RoleEnum.REGISTRAR:
if (username == null && userId)
res = await this.registrarDbService.findOne({
_id: new Types.ObjectId(userId),
});
else res = await this.registrarDbService.findOne({ email: username });
break;
case RoleEnum.COMPANY:
res = await this.insurerExpertDbService.findOne({ email: username });
break;
default:
return null;
}
return res;
}
/** Normalizes `role` from login body (string or single-element array). */
parseActorLoginRole(role: unknown): RoleEnum {
const raw = Array.isArray(role) ? role[0] : role;
if (
typeof raw !== "string" ||
!Object.values(RoleEnum).includes(raw as RoleEnum)
) {
throw new BadRequestException(
`Invalid role. Expected one of: ${Object.values(RoleEnum).join(", ")}`,
);
}
return raw as RoleEnum;
}
parseActorLoginUsername(body: Record<string, unknown>): string {
const username = body?.username ?? body?.email;
if (typeof username !== "string" || !username.trim()) {
throw new BadRequestException("username (email) is required");
}
return username.trim();
}
issueActorTokens(actor: {
_id: Types.ObjectId;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
role?: string;
userType?: string;
clientKey?: Types.ObjectId | string | null;
}) {
const payload = {
username: actor.username || actor.email,
sub: actor._id,
fullName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
role: actor.role || "User",
userType: actor.userType || "UserType",
clientKey: actor.clientKey ?? null,
};
const access_token = this.jwtService.sign(payload, {
secret: `${process.env.SECRET}`,
expiresIn: "1h",
});
return { ...payload, access_token };
}
async validateActor(
username: string,
pass: string,
role: RoleEnum,
): Promise<any> {
const user = await this.dynamicDbController(role, username);
if (!user) {
throw new NotFoundException("Actor account not found");
}
if (user.role !== role) {
throw new UnauthorizedException("user not assigned to this role");
}
if (!(await this.hashService.compare(pass, user.password))) {
throw new UnauthorizedException(
"password is incorrect or access Denied",
);
}
return user;
}
/**
* Authenticates an actor from a login request body (role, username/email, password).
*/
async loginFromCredentials(body: Record<string, unknown>) {
const role = this.parseActorLoginRole(body?.role);
const username = this.parseActorLoginUsername(body);
const password = body?.password;
if (typeof password !== "string" || !password) {
throw new BadRequestException("password is required");
}
const captcha =
typeof body?.captcha === "string" ? body.captcha : undefined;
await this.captchaAccountService.verifyActorCaptcha(
username,
role,
captcha,
);
const actor = await this.validateActor(username, password, role);
return this.issueActorTokens(actor);
}
/** @deprecated Prefer {@link loginFromCredentials}. Kept for internal callers. */
async loginActors(user: any) {
if (user?.access_token && user?.sub) {
return user;
}
return this.loginFromCredentials(user as Record<string, unknown>);
}
async registerActors(
body: RegisterDto | InsurerRegisterDto,
userType: UserType,
) {
const hashPassword = await this.hashService.hash(body.password);
body.password = hashPassword;
(body as any).userType = userType;
try {
if ("username" in body) {
if (userType === UserType.INSURER) {
if (!body.clientKey) {
throw new BadRequestException(
"clientKey is required for this user type.",
);
}
const clientName = await this.clientDbService.find({
_id: new Types.ObjectId(body.clientKey),
});
if (!clientName) throw new NotFoundException("Client Not Found");
}
const newCompanyPayload = {
email: body.username,
password: body.password,
role: RoleEnum.COMPANY,
userType: (body as any).userType,
clientKey: body.clientKey,
firstName: body.firstName,
lastName: body.lastName,
phone: body.phone,
mobile:body.mobile,
city: body.city,
state: body.state,
address: body.address,
};
return new RegisterDtoRs(
await this.insurerExpertDbService.create(newCompanyPayload),
);
} else {
if (userType === UserType.LEGAL || userType === UserType.GENUINE) {
if (!body.insuActivityCo) {
throw new BadRequestException(
"insuActivityCo is required for this user type.",
);
}
const clientName = await this.clientDbService.find({
_id: body.insuActivityCo,
});
if (!clientName) throw new NotFoundException("Client Not Found");
body.clientKey = body.insuActivityCo;
body.insuActivityCo = clientName.clientName.english;
}
switch (body.role) {
case RoleEnum.EXPERT:
return new RegisterDtoRs(await this.expertDbService.create(body));
case RoleEnum.DAMAGE_EXPERT:
return new RegisterDtoRs(
await this.damageExpertDbService.create(body),
);
default:
throw new BadRequestException(
`Invalid role for this registration type: ${body.role}`,
);
}
}
} catch (er) {
if (er.code === 11000) {
throw new BadRequestException(
"A user with these details already exists.",
);
} else if (er.errors) {
const errObjKey = Object.keys(er.errors)[0];
const { path, kind } = er.errors[errObjKey];
throw new BadRequestException(
`Validation failed for field '${path}'. Expected a valid ${kind}.`,
);
} else {
throw er;
}
}
}
async legalRegister(body: RegisterDto) {
return await this.registerActors(body, UserType.LEGAL);
}
async genuineRegister(body: RegisterDto) {
return await this.registerActors(body, UserType.GENUINE);
}
async insurerRegister(body: InsurerRegisterDto) {
Object.assign(body, { role: RoleEnum.COMPANY });
return await this.registerActors(body, UserType.INSURER);
}
/// TODO need to seed
async getStates() {
const states = JSON.parse(
readFileSync(`${process.cwd()}/src/static/states.json`, "utf8"),
);
return new StateListDtoRs(states);
}
/// TODO need to seeds
async getCities(id: number) {
const cities = JSON.parse(
readFileSync(`${process.cwd()}/src/static/cities.json`, "utf8"),
);
const citiesList = cities.filter((c) => c.province_id == id);
return new StateListDtoRs(citiesList);
}
async forgetPasswordSendMail(email: string) {
const normalizedEmail = email.toLowerCase().trim();
const filter = { email: normalizedEmail };
let actor;
// expert
actor = await this.expertDbService.findOne(filter);
let dbServiceToUpdate = this.expertDbService;
if (!actor) {
actor = (await this.damageExpertDbService.findOne(filter)) as any;
dbServiceToUpdate = this.damageExpertDbService as any;
}
if (!actor) {
actor = await this.insurerExpertDbService.findOne(filter);
dbServiceToUpdate = this.insurerExpertDbService as any;
}
if (!actor) {
actor = await this.fieldExpertDbService.findOne(filter);
dbServiceToUpdate = this.fieldExpertDbService as any;
}
if (!actor) {
actor = await this.registrarDbService.findOne(filter);
dbServiceToUpdate = this.registrarDbService as any;
}
if (!actor) {
throw new NotFoundException("actor not found");
}
const otp = this.otpService.create();
// Mailer disabled not used
// const sendEmail = await this.mailService.sendMail(normalizedEmail, otp);
// if (sendEmail) {
const hashOtp = await this.hashService.hash(otp);
await dbServiceToUpdate.findOneAndUpdate(filter, { otp: hashOtp });
return {
message: "OTP generated (mail sending disabled)",
code: 200,
emailSent: false,
};
// } else {
// throw new BadGatewayException("mailServer in unavailable");
// }
}
async forgetPasswordVerify(email, otp, newPassword) {
let userExist: any = await this.expertDbService.findOne({ email });
let dbServiceToUpdate: any = this.expertDbService;
if (!userExist) {
userExist = await this.damageExpertDbService.findOne({ email });
dbServiceToUpdate = this.damageExpertDbService;
}
if (!userExist) {
userExist = await this.insurerExpertDbService.findOne({ email });
dbServiceToUpdate = this.insurerExpertDbService;
}
if (!userExist) {
userExist = await this.fieldExpertDbService.findOne({ email });
dbServiceToUpdate = this.fieldExpertDbService;
}
if (!userExist) {
userExist = await this.registrarDbService.findOne({ email });
dbServiceToUpdate = this.registrarDbService as any;
}
if (!userExist) throw new NotFoundException("user not found");
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
const hashNewPassword = await this.hashService.hash(newPassword);
await dbServiceToUpdate.findOneAndUpdate(
{ email },
{ password: hashNewPassword },
);
return new ForgetPasswordVerifyCodeDtoRs(userExist, "update password");
}
async getProfiles(currentUser) {
const userDetail = await this.dynamicDbController(
currentUser.role,
currentUser.role === "company" ? currentUser.username : null,
currentUser.sub,
);
return new ProfileActor(userDetail);
}
async modifyProfile(currentUser: any, update: Record<string, any>) {
const role = currentUser?.role;
const userId = currentUser?.sub;
if (!role || !userId) {
throw new BadRequestException("Invalid actor payload");
}
const allowedFieldsByRole: Record<string, string[]> = {
expert: [
"fullName",
"firstName",
"lastName",
"email",
"phone",
"city",
"state",
"bio",
"nationalCode",
"birthDate",
"avatarUrl",
"address",
],
damage_expert: [
"fullName",
"firstName",
"lastName",
"email",
"phone",
"companyName",
"city",
"state",
"bio",
"nationalCode",
"birthDate",
"avatarUrl",
"address",
],
company: [
"firstName",
"lastName",
"phone",
"mobile",
"city",
"state",
"address",
],
field_expert: [
"firstName",
"lastName",
"email",
"phone",
"mobile",
],
registrar: ["email"],
};
const allowedFields = allowedFieldsByRole[role];
if (!allowedFields) {
throw new ForbiddenException("Profile update not allowed for this role");
}
const sanitized = Object.fromEntries(
Object.entries(update).filter(([key]) => allowedFields.includes(key)),
);
if (Object.keys(sanitized).length === 0) {
throw new BadRequestException("No valid fields to update");
}
if (sanitized.birthDate && typeof sanitized.birthDate === "string") {
const parsed = new Date(sanitized.birthDate);
if (isNaN(parsed.getTime())) {
throw new BadRequestException("birthDate must be a valid date");
}
sanitized.birthDate = parsed;
}
// fetch user detail (document or plain object)
const document = await this.dynamicDbController(role, currentUser.role === "company" ? currentUser.username : null, userId);
if (!document) throw new NotFoundException("Profile not found");
try {
// if its a mongoose document (has .save)
if (typeof document.save === "function") {
Object.assign(document, sanitized);
await document.save();
return document;
}
// else, update directly via corresponding DB service
switch (role) {
case "expert":
await this.expertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
case "damage_expert":
await this.damageExpertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
case "company":
await this.insurerExpertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
case "field_expert":
await this.fieldExpertDbService.updateOne(
{ _id: new Types.ObjectId(userId) },
{ $set: sanitized },
);
break;
default:
throw new ForbiddenException("Unsupported role for update");
}
// return the fresh document
return await this.dynamicDbController(role, null, userId);
} catch (err) {
throw new InternalServerErrorException("Failed to update profile");
}
}
/** Mock endpoint: create a field expert for testing. Make private later. */
async createFieldExpertMock(body: {
email: string;
password: string;
firstName: string;
lastName: string;
mobile: string;
phone?: string;
}) {
const hashPassword = await this.hashService.hash(body.password);
const payload = {
email: body.email.toLowerCase().trim(),
password: hashPassword,
firstName: body.firstName,
lastName: body.lastName,
mobile: body.mobile,
phone: body.phone,
role: RoleEnum.FIELD_EXPERT,
};
try {
const created = await this.fieldExpertDbService.create(payload);
return {
id: created._id,
email: created.email,
firstName: created.firstName,
lastName: created.lastName,
mobile: created.mobile,
role: created.role,
};
} catch (er) {
if (er.code === 11000) {
throw new BadRequestException(
"A field expert with this email already exists.",
);
}
throw er;
}
}
async createRegistrarMock(body: {
email: string;
password: string;
clientId: string;
}) {
const hashPassword = await this.hashService.hash(body.password);
const payload = {
email: body.email.toLowerCase().trim(),
password: hashPassword,
clientKey: new Types.ObjectId(body.clientId),
role: RoleEnum.REGISTRAR,
};
try {
const created = await this.registrarDbService.create(payload as any);
return {
id: (created as any)._id,
email: (created as any).email,
clientKey: (created as any).clientKey,
role: (created as any).role,
};
} catch (er) {
if (er.code === 11000) {
throw new BadRequestException(
"A registrar with this email already exists.",
);
}
throw er;
}
}
}