forked from Yara724/api
655 lines
21 KiB
TypeScript
655 lines
21 KiB
TypeScript
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 { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||
import { CallCenterAgentDbService } from "src/users/entities/db-service/call-center-agent.db.service";
|
||
import { HashService } from "src/utils/hash/hash.service";
|
||
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
|
||
import { SuperAdminDbService } from "src/super-admin/entities/db-service/super-admin.db.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 clientDbService: ClientDbService,
|
||
private readonly otpService: OtpGeneratorService,
|
||
private readonly captchaChallengeService: CaptchaChallengeService,
|
||
private readonly fileMakerDbService: FileMakerDbService,
|
||
private readonly fileReviewerDbService: FileReviewerDbService,
|
||
private readonly superAdminDbService: SuperAdminDbService,
|
||
private readonly callCenterAgentDbService: CallCenterAgentDbService,
|
||
) {}
|
||
|
||
// 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.findActorByLoginIdentifier(this.expertDbService, username);
|
||
break;
|
||
case RoleEnum.DAMAGE_EXPERT:
|
||
if (username == null && userId)
|
||
res = await this.damageExpertDbService.findOne({
|
||
_id: new Types.ObjectId(userId),
|
||
});
|
||
else
|
||
res = await this.findActorByLoginIdentifier(
|
||
this.damageExpertDbService,
|
||
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.findByLoginIdentifier(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;
|
||
case RoleEnum.FILE_MAKER:
|
||
if (username == null && userId)
|
||
res = await this.fileMakerDbService.findOne({
|
||
_id: new Types.ObjectId(userId),
|
||
});
|
||
else res = await this.fileMakerDbService.findByLoginIdentifier(username);
|
||
break;
|
||
case RoleEnum.FILE_REVIEWER:
|
||
if (username == null && userId)
|
||
res = await this.fileReviewerDbService.findOne({
|
||
_id: new Types.ObjectId(userId),
|
||
});
|
||
else
|
||
res =
|
||
await this.fileReviewerDbService.findByLoginIdentifier(username);
|
||
break;
|
||
case RoleEnum.SUPER_ADMIN:
|
||
if (username == null && userId)
|
||
res = await this.superAdminDbService.findOne({
|
||
_id: new Types.ObjectId(userId),
|
||
});
|
||
else res = await this.superAdminDbService.findByLoginIdentifier(username);
|
||
break;
|
||
case RoleEnum.CALL_CENTER:
|
||
if (username == null && userId)
|
||
res = await this.callCenterAgentDbService.findOne({
|
||
_id: new Types.ObjectId(userId),
|
||
});
|
||
else
|
||
res = await this.callCenterAgentDbService.findByLoginIdentifier(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 ?? body?.nationalCode;
|
||
if (typeof username !== "string" || !username.trim()) {
|
||
throw new BadRequestException(
|
||
"username, email, or nationalCode is required",
|
||
);
|
||
}
|
||
return username.trim();
|
||
}
|
||
|
||
private async findActorByLoginIdentifier(
|
||
dbService: { findOne: (filter: any) => Promise<any> },
|
||
identifier: string,
|
||
) {
|
||
const id = identifier.trim();
|
||
const or: Record<string, string>[] = [{ email: id }, { username: id }];
|
||
if (/^\d{10}$/.test(id)) {
|
||
or.push({ nationalCode: id });
|
||
}
|
||
return dbService.findOne({ $or: or });
|
||
}
|
||
|
||
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 || (actor as any).nationalCode || null,
|
||
sub: actor._id,
|
||
fullName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
|
||
role: actor.role || "User",
|
||
userType: actor.userType || "UserType",
|
||
clientKey: actor.clientKey ? String(actor.clientKey) : null,
|
||
};
|
||
|
||
const access_token = this.jwtService.sign(payload, {
|
||
secret: `${process.env.JWT_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 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<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 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;
|
||
}
|
||
}
|
||
}
|