Files
goumrah-api/src/modules/partner/partner.controller.ts
2025-12-01 21:09:41 +07:00

627 lines
15 KiB
TypeScript

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<PartnerResponse>);
}
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<PartnerResponse>);
}
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<PartnerResponse>);
}
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;
}
}