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 { CaptchaChallengeService } from "src/captcha/captcha-challenge.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, keys: string[]) { const out: Record = {}; 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 captchaChallengeService: CaptchaChallengeService, ) {} // 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 { 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 { 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) { 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 captchaId = typeof body?.captchaId === "string" ? body.captchaId : undefined; const captcha = typeof body?.captcha === "string" ? body.captcha : undefined; await this.captchaChallengeService.verify(captchaId, 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); } 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) { const role = currentUser?.role; const userId = currentUser?.sub; if (!role || !userId) { throw new BadRequestException("Invalid actor payload"); } const allowedFieldsByRole: Record = { 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 it’s 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; } } }