import { Controller } from "@/common/controller"; import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware"; import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware"; import { isPartnerMiddleware, type PartnerRequestPlugin, } from "@/common/middlewares/is-partner.middleware"; import { paginationQuerySchema } from "@/common/schemas"; import type { AbstractEmailService } from "@/common/services/email-service/abstract.email-service"; import type { AbstractFileStorage, FileResult, } from "@/common/services/file-storage/abstract.file-storage"; import { JwtType, type AbstractJwtService, } from "@/common/services/jwt-service/abstract.jwt-service"; import type { ErrorResponse, ListResponse, SingleResponse, } from "@/common/types"; import { generateRandomCode } from "@/common/utils"; import { Partner } from "@/database/entities/partner.entity"; import { Verification } from "@/database/entities/verification.entity"; import { AdminPermission } from "@/database/enums/admin-permission.enum"; import { VerificationType } from "@/database/enums/verification-type.enum"; import { orm } from "@/database/orm"; import type { PartnerMapper } from "@/modules/partner/partner.mapper"; import { partnerChangeEmailRequestSchema, partnerChangePasswordRequestSchema, partnerLoginRequestSchema, partnerParamsSchema, partnerRequestSchema, partnerUpdateRequestSchema, partnerVerifyRequestSchema, } from "@/modules/partner/partner.schemas"; import type { PartnerResponse } from "@/modules/partner/partner.types"; import * as dateFns from "date-fns"; import { Router, type Request, type Response } from "express"; import { ulid } from "ulid"; export class PartnerController extends Controller { public constructor( private readonly mapper: PartnerMapper, private readonly fileStorage: AbstractFileStorage, private readonly emailService: AbstractEmailService, private readonly jwtService: AbstractJwtService, ) { super(); } async create(req: Request, res: Response) { const parseBodyResult = partnerRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; let avatarFile: null | FileResult = null; if (body.avatar !== null) { avatarFile = await this.fileStorage.storeFile( Buffer.from(body.avatar, "base64"), ); } const verification = orm.em.create(Verification, { id: ulid(), code: generateRandomCode(6, "0123456789"), type: VerificationType.createPartner, expiredAt: dateFns.addHours(new Date(), 1), createdAt: new Date(), updatedAt: new Date(), }); const partner = orm.em.create(Partner, { id: ulid(), name: body.name, email: body.email, whatsapp: body.whatsapp, password: await Bun.password.hash(body.password), avatar: avatarFile?.name, verification, createdAt: new Date(), updatedAt: new Date(), }); await orm.em.flush(); await this.emailService.sendVerificationEmail( partner.email, verification.code, ); return res.status(201).json({ data: { message: "Partner created successfully. Please check your email for verification.", }, errors: null, } satisfies SingleResponse); } async login(req: Request, res: Response) { const parseBodyResult = partnerLoginRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne(Partner, { email: body.email }); if (!partner) { return res.status(401).json({ data: null, errors: [ { location: "body", message: "Incorrect email or password.", }, ], } satisfies ErrorResponse); } if (!(await Bun.password.verify(body.password, partner.password))) { return res.status(401).json({ data: null, errors: [ { location: "body", message: "Incorrect email or password.", }, ], } satisfies ErrorResponse); } if (partner.verification !== null) { return res.status(400).json({ data: null, errors: [ { message: "Partner is not verified.", }, ], } satisfies ErrorResponse); } const access = await this.jwtService.createPartnerToken( partner, JwtType.access, ); const refresh = await this.jwtService.createPartnerToken( partner, JwtType.refresh, ); return res.status(200).json({ data: { access_token: access.token, access_token_expires_at: access.expiresAt, refresh_token: refresh.token, refresh_token_expires_at: refresh.expiresAt, }, errors: null, } satisfies SingleResponse); } async list(req: Request, res: Response) { const parseQueryResult = paginationQuerySchema.safeParse(req.query); if (!parseQueryResult.success) { return this.handleZodError(parseQueryResult.error, res, "query"); } const query = parseQueryResult.data; const count = await orm.em.count(Partner); const partners = await orm.em.find( Partner, {}, { limit: query.per_page, offset: (query.page - 1) * query.per_page, orderBy: { createdAt: "DESC" }, populate: ["*"], }, ); return res.status(200).json({ data: partners.map(this.mapper.mapEntityToResponse.bind(this.mapper)), errors: null, meta: { page: query.page, per_page: query.per_page, total_pages: Math.ceil(count / query.per_page), total_items: count, }, } satisfies ListResponse); } async view(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const partner = await orm.em.findOne( Partner, { id: params.id, verification: null, }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } return res.status(200).json({ data: this.mapper.mapEntityToResponse(partner), errors: null, } satisfies SingleResponse); } async update(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = partnerUpdateRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne( Partner, { id: params.id, verification: null, }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } if (body.avatar !== null) { await this.fileStorage.storeFile( Buffer.from(body.avatar, "base64"), partner.avatar ?? undefined, ); } else if (partner.avatar !== null) { await this.fileStorage.removeFile(partner.avatar); } partner.name = body.name; partner.verification = orm.em.create(Verification, { id: ulid(), code: generateRandomCode(6, "0123456789"), type: VerificationType.updatePartner, expiredAt: dateFns.addHours(new Date(), 1), createdAt: new Date(), updatedAt: new Date(), }); partner.updatedAt = new Date(); await orm.em.flush(); await this.emailService.sendVerificationEmail( partner.email, partner.verification.code, ); return res.status(200).json({ data: { message: "Partner updated successfully. Please check your email for verification.", }, errors: null, } satisfies SingleResponse); } async changeEmail(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = partnerChangeEmailRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne( Partner, { id: params.id, verification: null, }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } partner.email = body.new_email; partner.verification = orm.em.create(Verification, { id: ulid(), code: generateRandomCode(6, "0123456789"), type: VerificationType.changeEmailPartner, expiredAt: dateFns.addHours(new Date(), 1), createdAt: new Date(), updatedAt: new Date(), }); partner.updatedAt = new Date(); await orm.em.flush(); await this.emailService.sendVerificationEmail( partner.email, partner.verification.code, ); return res.status(200).json({ data: { message: "Partner's email changed successfully. Please check your email for verification.", }, errors: null, } satisfies SingleResponse); } async changePassword(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = partnerChangePasswordRequestSchema.safeParse( req.body, ); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne( Partner, { id: params.id, verification: null, }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } if (!(await Bun.password.verify(body.old_password, partner.password))) { return res.status(400).json({ data: null, errors: [ { path: "old_password", location: "body", message: "Incorrect.", }, ], } satisfies ErrorResponse); } partner.password = await Bun.password.hash(body.new_password); partner.verification = orm.em.create(Verification, { id: ulid(), code: generateRandomCode(6, "0123456789"), type: VerificationType.changePasswordPartner, expiredAt: dateFns.addHours(new Date(), 1), createdAt: new Date(), updatedAt: new Date(), }); partner.updatedAt = new Date(); await orm.em.flush(); await this.emailService.sendVerificationEmail( partner.email, partner.verification.code, ); return res.status(200).json({ data: { message: "Partner's password changed successfully. Please check your email for verification.", }, errors: null, } satisfies SingleResponse); } async verify(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = partnerVerifyRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne( Partner, { id: params.id }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } if (partner.verification === null) { return res.status(400).json({ data: null, errors: [ { message: "Partner is already verified.", }, ], } satisfies ErrorResponse); } if (partner.verification.code !== body.code) { return res.status(400).json({ data: null, errors: [ { path: "code", location: "body", message: "Incorrect.", }, ], } satisfies ErrorResponse); } orm.em.remove(partner.verification); partner.verification = null; partner.updatedAt = new Date(); await orm.em.flush(); return res.status(200).json({ data: this.mapper.mapEntityToResponse(partner), errors: null, } satisfies SingleResponse); } async refresh(_req: Request, res: Response) { const req = _req as Request & PartnerRequestPlugin; const access = await this.jwtService.createPartnerToken( req.partner, JwtType.access, ); const refresh = await this.jwtService.createPartnerToken( req.partner, JwtType.refresh, ); return res.status(200).json({ data: { access_token: access.token, access_token_expires_at: access.expiresAt, refresh_token: refresh.token, refresh_token_expires_at: refresh.expiresAt, }, errors: null, } satisfies SingleResponse); } async delete(req: Request, res: Response) { const parseParamsResult = partnerParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const partner = await orm.em.findOne( Partner, { id: params.id, verification: null }, { populate: ["*"], }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Partner not found.", }, ], } satisfies ErrorResponse); } if (partner.avatar !== null) { await this.fileStorage.removeFile(partner.avatar); } await orm.em.removeAndFlush(partner); return res.status(204).send(); } public buildRouter(): Router { const router = Router(); router.post( "/", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.createPartner]), this.create.bind(this), ); router.post("/login", createOrmContextMiddleware, this.login.bind(this)); router.get("/", createOrmContextMiddleware, this.list.bind(this)); router.get("/:id", createOrmContextMiddleware, this.view.bind(this)); router.put( "/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]), this.update.bind(this), ); router.put( "/:id/email", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]), this.changeEmail.bind(this), ); router.put( "/:id/password", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]), this.changePassword.bind(this), ); router.put( "/:id/verify", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]), this.verify.bind(this), ); router.put( "/refresh", createOrmContextMiddleware, isPartnerMiddleware(this.jwtService), this.refresh.bind(this), ); router.delete( "/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.deletePartner]), this.delete.bind(this), ); return router; } }