627 lines
15 KiB
TypeScript
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;
|
|
}
|
|
}
|