add auth and payment api
This commit is contained in:
627
src/modules/admin/admin.controller.ts
Normal file
627
src/modules/admin/admin.controller.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import {
|
||||
isAdminMiddleware,
|
||||
type AdminRequestPlugin,
|
||||
} from "@/common/middlewares/is-admin.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 { Admin } from "@/database/entities/admin.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 { AdminMapper } from "@/modules/admin/admin.mapper";
|
||||
import {
|
||||
adminChangeEmailRequestSchema,
|
||||
adminChangePasswordRequestSchema,
|
||||
adminParamsSchema,
|
||||
adminRequestSchema,
|
||||
adminUpdateRequestSchema,
|
||||
adminVerifyRequestSchema,
|
||||
} from "@/modules/admin/admin.schemas";
|
||||
import type { AdminResponse } from "@/modules/admin/admin.types";
|
||||
import * as dateFns from "date-fns";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AdminController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: AdminMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly emailService: AbstractEmailService,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = adminRequestSchema.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),
|
||||
type: VerificationType.createAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const admin = orm.em.create(Admin, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
password: await Bun.password.hash(body.password),
|
||||
avatar: avatarFile?.name,
|
||||
permissions: body.permissions,
|
||||
verification,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
verification.code,
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin created successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
const parseBodyResult = adminRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(Admin, { email: body.email });
|
||||
if (!admin) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (!(await Bun.password.verify(body.password, admin.password))) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (admin.verification !== null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Admin is not verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const access = await this.jwtService.createAdminToken(
|
||||
admin,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createAdminToken(
|
||||
admin,
|
||||
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(Admin);
|
||||
|
||||
const admins = await orm.em.find(
|
||||
Admin,
|
||||
{
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: admins.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<AdminResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(admin),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AdminResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminUpdateRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (body.avatar !== null) {
|
||||
await this.fileStorage.storeFile(
|
||||
Buffer.from(body.avatar, "base64"),
|
||||
admin.avatar ?? undefined,
|
||||
);
|
||||
} else if (admin.avatar !== null) {
|
||||
await this.fileStorage.removeFile(admin.avatar);
|
||||
}
|
||||
|
||||
admin.name = body.name;
|
||||
admin.permissions = body.permissions;
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.updateAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin updated successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changeEmail(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminChangeEmailRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
admin.email = body.new_email;
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changeEmailAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin's email changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changePassword(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminChangePasswordRequestSchema.safeParse(
|
||||
req.body,
|
||||
);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (!(await Bun.password.verify(body.old_password, admin.password))) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "old_password",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
admin.password = await Bun.password.hash(body.new_password);
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changePasswordAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin's password changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async verify(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminVerifyRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.verification === null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Admin is already verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.verification.code !== body.code) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "code",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
orm.em.remove(admin.verification);
|
||||
|
||||
admin.verification = null;
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(admin),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AdminResponse>);
|
||||
}
|
||||
|
||||
async refresh(_req: Request, res: Response) {
|
||||
const req = _req as Request & AdminRequestPlugin;
|
||||
|
||||
const access = await this.jwtService.createAdminToken(
|
||||
req.admin,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createAdminToken(
|
||||
req.admin,
|
||||
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 = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{ id: params.id, verification: null },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.avatar !== null) {
|
||||
await this.fileStorage.removeFile(admin.avatar);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(admin);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAdmin]),
|
||||
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.updateAdmin]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/email",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.changeEmail.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/password",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.changePassword.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/verify",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.verify.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/refresh",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService),
|
||||
this.refresh.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAdmin]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
18
src/modules/admin/admin.mapper.ts
Normal file
18
src/modules/admin/admin.mapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Admin } from "@/database/entities/admin.entity";
|
||||
import type { AdminResponse } from "@/modules/admin/admin.types";
|
||||
|
||||
export class AdminMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapEntityToResponse(admin: Admin): AdminResponse {
|
||||
return {
|
||||
id: admin.id,
|
||||
name: admin.name,
|
||||
email: admin.email,
|
||||
avatar: admin.avatar,
|
||||
permissions: admin.permissions,
|
||||
created_at: admin.createdAt,
|
||||
updated_at: admin.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
56
src/modules/admin/admin.schemas.ts
Normal file
56
src/modules/admin/admin.schemas.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { emailSchema, passwordSchema } from "@/common/schemas";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const adminRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
avatar: z
|
||||
.base64("Must be base64 string.")
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
permissions: z
|
||||
.array(
|
||||
z.enum(AdminPermission, "Must be valid permission."),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
});
|
||||
|
||||
export const adminLoginRequestSchema = adminRequestSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export const adminUpdateRequestSchema = adminRequestSchema.pick({
|
||||
name: true,
|
||||
avatar: true,
|
||||
permissions: true,
|
||||
});
|
||||
|
||||
export const adminChangeEmailRequestSchema = z.object({
|
||||
new_email: emailSchema,
|
||||
});
|
||||
|
||||
export const adminChangePasswordRequestSchema = z.object({
|
||||
old_password: passwordSchema,
|
||||
new_password: passwordSchema,
|
||||
});
|
||||
|
||||
export const adminVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const adminParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
33
src/modules/admin/admin.types.ts
Normal file
33
src/modules/admin/admin.types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import type {
|
||||
adminChangeEmailRequestSchema,
|
||||
adminChangePasswordRequestSchema,
|
||||
adminParamsSchema,
|
||||
adminRequestSchema,
|
||||
adminUpdateRequestSchema,
|
||||
} from "@/modules/admin/admin.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type AdminRequest = z.infer<typeof adminRequestSchema>;
|
||||
|
||||
export type AdminUpdateRequest = z.infer<typeof adminUpdateRequestSchema>;
|
||||
|
||||
export type AdminChangeEmailRequest = z.infer<
|
||||
typeof adminChangeEmailRequestSchema
|
||||
>;
|
||||
|
||||
export type AdminChangePasswordRequest = z.infer<
|
||||
typeof adminChangePasswordRequestSchema
|
||||
>;
|
||||
|
||||
export type AdminParams = z.infer<typeof adminParamsSchema>;
|
||||
|
||||
export type AdminResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
permissions: AdminPermission[];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Airline } from "@/database/entities/airline.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||
import {
|
||||
@@ -22,6 +25,7 @@ export class AirlineController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: AirlineMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -43,7 +47,7 @@ export class AirlineController extends Controller {
|
||||
code: body.code,
|
||||
logo: logoFile.name,
|
||||
skytraxRating: body.skytrax_rating,
|
||||
skytraxType: this.mapper.mapSkytraxType(body.skytrax_type),
|
||||
skytraxType: body.skytrax_type,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
@@ -149,7 +153,7 @@ export class AirlineController extends Controller {
|
||||
airline.name = body.name;
|
||||
airline.code = body.code;
|
||||
airline.skytraxRating = body.skytrax_rating;
|
||||
airline.skytraxType = this.mapper.mapSkytraxType(body.skytrax_type);
|
||||
airline.skytraxType = body.skytrax_type;
|
||||
airline.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
@@ -196,11 +200,26 @@ export class AirlineController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAirline]),
|
||||
this.create.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.updateAirline]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAirline]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import type { Airline } from "@/database/entities/airline.entity";
|
||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import type {
|
||||
AirlineRequest,
|
||||
AirlineResponse,
|
||||
} from "@/modules/airline/airline.types";
|
||||
import type { AirlineResponse } from "@/modules/airline/airline.types";
|
||||
|
||||
export class AirlineMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapSkytraxType(
|
||||
skytraxType: AirlineRequest["skytrax_type"],
|
||||
): SkytraxType {
|
||||
switch (skytraxType) {
|
||||
case "full_service":
|
||||
return SkytraxType.fullService;
|
||||
case "low_cost":
|
||||
return SkytraxType.lowCost;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(airline: Airline): AirlineResponse {
|
||||
return {
|
||||
id: airline.id,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const airlineRequestSchema = z.object({
|
||||
@@ -16,7 +17,7 @@ export const airlineRequestSchema = z.object({
|
||||
.min(1, "Minimum 1.")
|
||||
.max(5, "Maximum 5."),
|
||||
skytrax_type: z.enum(
|
||||
["full_service", "low_cost"],
|
||||
SkytraxType,
|
||||
"Must be either 'full_service' or 'low_cost'.",
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import type {
|
||||
airlineParamsSchema,
|
||||
airlineRequestSchema,
|
||||
@@ -14,7 +15,7 @@ export type AirlineResponse = {
|
||||
code: string;
|
||||
logo: string;
|
||||
skytrax_rating: number;
|
||||
skytrax_type: "full_service" | "low_cost";
|
||||
skytrax_type: SkytraxType;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
} from "@/common/types";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirportMapper } from "@/modules/airport/airport.mapper";
|
||||
import {
|
||||
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AirportController extends Controller {
|
||||
public constructor(private readonly mapper: AirportMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: AirportMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -224,11 +230,26 @@ export class AirportController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAirport]),
|
||||
this.create.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.updateAirport]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAirport]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
} from "@/common/types";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CityMapper } from "@/modules/city/city.mapper";
|
||||
import {
|
||||
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CityController extends Controller {
|
||||
public constructor(private readonly mapper: CityMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: CityMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -210,11 +216,26 @@ export class CityController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createCity]),
|
||||
this.create.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.updateCity]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteCity]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CountryMapper } from "@/modules/country/country.mapper";
|
||||
import {
|
||||
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CountryController extends Controller {
|
||||
public constructor(private readonly mapper: CountryMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: CountryMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -174,11 +180,26 @@ export class CountryController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createCountry]),
|
||||
this.create.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.updateCountry]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteCountry]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -10,6 +12,7 @@ import { Airline } from "@/database/entities/airline.entity";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { FlightClass } from "@/database/entities/flight-class.entity";
|
||||
import { Flight } from "@/database/entities/flight.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import {
|
||||
@@ -26,7 +29,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class FlightController extends Controller {
|
||||
public constructor(private readonly mapper: FlightMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: FlightMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -622,26 +628,52 @@ export class FlightController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createFlight]),
|
||||
this.create.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.updateFlight]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteFlight]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createFlightClass]),
|
||||
this.createClass.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
this.listClasses.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateFlightClass]),
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteFlightClass]),
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||
import {
|
||||
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class HotelFacilityController extends Controller {
|
||||
public constructor(private readonly mapper: HotelFacilityMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: HotelFacilityMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -184,11 +190,26 @@ export class HotelFacilityController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createHotelFacility]),
|
||||
this.create.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.updateHotelFacility]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteHotelFacility]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -11,6 +13,7 @@ import { City } from "@/database/entities/city.entity";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { HotelImage } from "@/database/entities/hotel-image.entity";
|
||||
import { Hotel } from "@/database/entities/hotel.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
import {
|
||||
@@ -25,6 +28,7 @@ export class HotelController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: HotelMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -332,11 +336,26 @@ export class HotelController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createHotel]),
|
||||
this.create.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.updateHotel]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteHotel]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
389
src/modules/order/order.controller.ts
Normal file
389
src/modules/order/order.controller.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import {
|
||||
isPartnerMiddleware,
|
||||
type PartnerRequestPlugin,
|
||||
} from "@/common/middlewares/is-partner.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { generateRandomCode } from "@/common/utils";
|
||||
import { OrderDetail } from "@/database/entities/order-detail.entity";
|
||||
import { Order } from "@/database/entities/order.entity";
|
||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import { VerificationType } from "@/database/enums/verification-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { OrderMapper } from "@/modules/order/order.mapper";
|
||||
import {
|
||||
orderParamsSchema,
|
||||
orderRequestSchema,
|
||||
orderVerifyRequestSchema,
|
||||
} from "@/modules/order/order.schemas";
|
||||
import type { OrderResponse } from "@/modules/order/order.types";
|
||||
import * as dateFns from "date-fns";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class OrderController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: OrderMapper,
|
||||
private readonly paymentService: AbstractPaymentService,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = orderRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const packageDetail = await orm.em.findOne(PackageDetail, {
|
||||
id: body.package_id,
|
||||
});
|
||||
if (!packageDetail) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "package_id",
|
||||
location: "body",
|
||||
message: "Package detail not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.createOrder,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const order = orm.em.create(Order, {
|
||||
id: ulid(),
|
||||
package: packageDetail,
|
||||
name: body.name,
|
||||
whatsapp: body.whatsapp,
|
||||
verification,
|
||||
partner: null,
|
||||
expiredAt: null,
|
||||
purchasedAt: null,
|
||||
finishedAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
for (const roomType of body.room_types) {
|
||||
order.details.add(
|
||||
orm.em.create(OrderDetail, {
|
||||
id: ulid(),
|
||||
order,
|
||||
roomType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: {
|
||||
message:
|
||||
"Order created successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async list(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
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(Order);
|
||||
|
||||
const orders = await orm.em.find(
|
||||
Order,
|
||||
{
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: orders.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<OrderResponse>);
|
||||
}
|
||||
|
||||
async view(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(order),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<OrderResponse>);
|
||||
}
|
||||
|
||||
async finish(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
order.finishedAt = new Date();
|
||||
order.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(order),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<OrderResponse>);
|
||||
}
|
||||
|
||||
async verify(req: Request, res: Response) {
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = orderVerifyRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (order.verification === null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Order is already verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (order.verification.code !== body.code) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "code",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
orm.em.remove(order.verification);
|
||||
|
||||
const partners = await orm.em.findAll(Partner, { populate: ["*"] });
|
||||
const partner = partners.toSorted(
|
||||
(a, b) =>
|
||||
a.orders.filter((order) => order.finishedAt === null).length -
|
||||
b.orders.filter((order) => order.finishedAt === null).length,
|
||||
)[0];
|
||||
|
||||
order.verification = null;
|
||||
order.partner = partner;
|
||||
order.expiredAt = dateFns.addHours(new Date(), 24);
|
||||
order.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
const paymentUrl = await this.paymentService.createPaymentUrl(order);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
...this.mapper.mapEntityToResponse(order),
|
||||
payment_url: paymentUrl,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse<
|
||||
OrderResponse & {
|
||||
payment_url: string;
|
||||
}
|
||||
>);
|
||||
}
|
||||
|
||||
async delete(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{ id: params.id, verification: null, partner: req.partner },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(order);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", createOrmContextMiddleware, this.create.bind(this));
|
||||
router.get(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.list.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.view.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/finish",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.finish.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/verify",
|
||||
createOrmContextMiddleware,
|
||||
this.verify.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
59
src/modules/order/order.mapper.ts
Normal file
59
src/modules/order/order.mapper.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Order } from "@/database/entities/order.entity";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
import type { OrderResponse } from "@/modules/order/order.types";
|
||||
import type { PackageMapper } from "@/modules/package/package.mapper";
|
||||
import type { PartnerMapper } from "@/modules/partner/partner.mapper";
|
||||
|
||||
export class OrderMapper {
|
||||
public constructor(
|
||||
private readonly packageMapper: PackageMapper,
|
||||
private readonly partnerMapper: PartnerMapper,
|
||||
) {}
|
||||
|
||||
public mapEntityToResponse(order: Order): OrderResponse {
|
||||
const details: OrderResponse["details"] = [];
|
||||
let totalPrice = 0;
|
||||
for (const detail of order.details) {
|
||||
let price = 0;
|
||||
switch (detail.roomType) {
|
||||
case RoomType.double:
|
||||
price = order.package.doublePrice;
|
||||
break;
|
||||
case RoomType.triple:
|
||||
price = order.package.triplePrice;
|
||||
break;
|
||||
case RoomType.quad:
|
||||
price = order.package.quadPrice;
|
||||
break;
|
||||
case RoomType.infant:
|
||||
price = order.package.infantPrice ?? 0;
|
||||
break;
|
||||
}
|
||||
|
||||
details.push({
|
||||
price,
|
||||
room_type: detail.roomType,
|
||||
});
|
||||
|
||||
totalPrice += price;
|
||||
}
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
package: this.packageMapper.mapDetailEntityToResponse(order.package),
|
||||
name: order.name,
|
||||
whatsapp: order.whatsapp,
|
||||
details,
|
||||
total_price: totalPrice,
|
||||
is_verified: order.verification === null,
|
||||
partner: order.partner
|
||||
? this.partnerMapper.mapEntityToResponse(order.partner)
|
||||
: null,
|
||||
expired_at: order.expiredAt,
|
||||
purchased_at: order.purchasedAt,
|
||||
finished_at: order.finishedAt,
|
||||
created_at: order.createdAt,
|
||||
updated_at: order.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
38
src/modules/order/order.schemas.ts
Normal file
38
src/modules/order/order.schemas.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { phoneNumberSchema } from "@/common/schemas";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const orderRequestSchema = z.object({
|
||||
package_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
whatsapp: phoneNumberSchema,
|
||||
room_types: z
|
||||
.array(
|
||||
z.enum(
|
||||
RoomType,
|
||||
"Must be either 'double', 'triple', 'quad', or 'infant'.",
|
||||
),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
});
|
||||
|
||||
export const orderVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const orderParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
31
src/modules/order/order.types.ts
Normal file
31
src/modules/order/order.types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { RoomType } from "@/database/enums/room-type.enum";
|
||||
import type {
|
||||
orderParamsSchema,
|
||||
orderRequestSchema,
|
||||
} from "@/modules/order/order.schemas";
|
||||
import type { PackageDetailResponse } from "@/modules/package/package.types";
|
||||
import type { PartnerResponse } from "@/modules/partner/partner.types";
|
||||
import z from "zod";
|
||||
|
||||
export type OrderRequest = z.infer<typeof orderRequestSchema>;
|
||||
|
||||
export type OrderParams = z.infer<typeof orderParamsSchema>;
|
||||
|
||||
export type OrderResponse = {
|
||||
id: string;
|
||||
package: PackageDetailResponse;
|
||||
name: string;
|
||||
whatsapp: string;
|
||||
details: {
|
||||
room_type: RoomType;
|
||||
price: number;
|
||||
}[];
|
||||
total_price: number;
|
||||
is_verified: boolean;
|
||||
partner: PartnerResponse | null;
|
||||
expired_at: Date | null;
|
||||
purchased_at: Date | null;
|
||||
finished_at: Date | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||
import { Package } from "@/database/entities/package.entity";
|
||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { PackageMapper } from "@/modules/package/package.mapper";
|
||||
@@ -43,6 +46,7 @@ export class PackageController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: PackageMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -61,8 +65,8 @@ export class PackageController extends Controller {
|
||||
const package_ = orm.em.create(Package, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
type: this.mapper.mapPackageType(body.type),
|
||||
class: this.mapper.mapPackageClass(body.class),
|
||||
type: body.type,
|
||||
class: body.class,
|
||||
thumbnail: thumbnailFile.name,
|
||||
useFastTrain: body.use_fast_train,
|
||||
createdAt: new Date(),
|
||||
@@ -177,8 +181,8 @@ export class PackageController extends Controller {
|
||||
);
|
||||
|
||||
package_.name = body.name;
|
||||
package_.type = this.mapper.mapPackageType(body.type);
|
||||
package_.class = this.mapper.mapPackageClass(body.class);
|
||||
package_.type = body.type;
|
||||
package_.class = body.class;
|
||||
package_.useFastTrain = body.use_fast_train;
|
||||
package_.updatedAt = new Date();
|
||||
|
||||
@@ -1330,26 +1334,52 @@ export class PackageController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/details", ormMiddleware, this.createDetail.bind(this));
|
||||
router.get("/:id/details", ormMiddleware, this.listDetails.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createPackage]),
|
||||
this.create.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.updatePackage]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deletePackage]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/details",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createPackageDetail]),
|
||||
this.createDetail.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/details",
|
||||
createOrmContextMiddleware,
|
||||
this.listDetails.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewDetail.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePackageDetail]),
|
||||
this.updateDetail.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deletePackageDetail]),
|
||||
this.deleteDetail.bind(this),
|
||||
);
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import type {
|
||||
} from "@/database/entities/package-itinerary-widget.entity";
|
||||
import type { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||
import type { Package } from "@/database/entities/package.entity";
|
||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||
import { PackageType } from "@/database/enums/package-type.enum";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
@@ -22,7 +20,6 @@ import type {
|
||||
PackageItineraryDayResponse,
|
||||
PackageItineraryResponse,
|
||||
PackageItineraryWidgetResponse,
|
||||
PackageRequest,
|
||||
PackageResponse,
|
||||
} from "@/modules/package/package.types";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
@@ -35,26 +32,6 @@ export class PackageMapper {
|
||||
private readonly transportationMapper: TransportationMapper,
|
||||
) {}
|
||||
|
||||
public mapPackageType(packageType: PackageRequest["type"]): PackageType {
|
||||
switch (packageType) {
|
||||
case "reguler":
|
||||
return PackageType.reguler;
|
||||
case "plus":
|
||||
return PackageType.plus;
|
||||
}
|
||||
}
|
||||
|
||||
public mapPackageClass(packageClass: PackageRequest["class"]): PackageClass {
|
||||
switch (packageClass) {
|
||||
case "silver":
|
||||
return PackageClass.silver;
|
||||
case "gold":
|
||||
return PackageClass.gold;
|
||||
case "platinum":
|
||||
return PackageClass.platinum;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(package_: Package): PackageResponse {
|
||||
return {
|
||||
id: package_.id,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { dateSchema, timeSchema } from "@/common/schemas";
|
||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import { PackageType } from "@/database/enums/package-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const packageRequestSchema = z.object({
|
||||
@@ -6,9 +8,9 @@ export const packageRequestSchema = z.object({
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
type: z.enum(["reguler", "plus"], "Must be either 'reguler' or 'plus'."),
|
||||
type: z.enum(PackageType, "Must be either 'reguler' or 'plus'."),
|
||||
class: z.enum(
|
||||
["silver", "gold", "platinum"],
|
||||
PackageClass,
|
||||
"Must be either 'silver', 'gold', or 'platinum'.",
|
||||
),
|
||||
thumbnail: z.base64("Must be base64 string.").nonempty("Must not empty."),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import type { PackageType } from "@/database/enums/package-type.enum";
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||
import type {
|
||||
@@ -20,8 +22,8 @@ export type PackageDetailParams = z.infer<typeof packageDetailParamsSchema>;
|
||||
export type PackageResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "reguler" | "plus";
|
||||
class: "silver" | "gold" | "platinum";
|
||||
type: PackageType;
|
||||
class: PackageClass;
|
||||
thumbnail: string;
|
||||
use_fast_train: boolean;
|
||||
created_at: Date;
|
||||
|
||||
627
src/modules/partner/partner.controller.ts
Normal file
627
src/modules/partner/partner.controller.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
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,
|
||||
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),
|
||||
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 = partnerRequestSchema.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,
|
||||
{
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
src/modules/partner/partner.mapper.ts
Normal file
18
src/modules/partner/partner.mapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Partner } from "@/database/entities/partner.entity";
|
||||
import type { PartnerResponse } from "@/modules/partner/partner.types";
|
||||
|
||||
export class PartnerMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapEntityToResponse(partner: Partner): PartnerResponse {
|
||||
return {
|
||||
id: partner.id,
|
||||
name: partner.name,
|
||||
email: partner.email,
|
||||
whatsapp: partner.whatsapp,
|
||||
avatar: partner.avatar,
|
||||
created_at: partner.createdAt,
|
||||
updated_at: partner.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
54
src/modules/partner/partner.schemas.ts
Normal file
54
src/modules/partner/partner.schemas.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
emailSchema,
|
||||
passwordSchema,
|
||||
phoneNumberSchema,
|
||||
} from "@/common/schemas";
|
||||
import z from "zod";
|
||||
|
||||
export const partnerRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
email: emailSchema,
|
||||
whatsapp: phoneNumberSchema,
|
||||
password: passwordSchema,
|
||||
avatar: z
|
||||
.base64("Must be base64 string.")
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const partnerLoginRequestSchema = partnerRequestSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export const partnerUpdateRequestSchema = partnerRequestSchema.pick({
|
||||
name: true,
|
||||
avatar: true,
|
||||
permissions: true,
|
||||
});
|
||||
|
||||
export const partnerChangeEmailRequestSchema = z.object({
|
||||
new_email: emailSchema,
|
||||
});
|
||||
|
||||
export const partnerChangePasswordRequestSchema = z.object({
|
||||
old_password: passwordSchema,
|
||||
new_password: passwordSchema,
|
||||
});
|
||||
|
||||
export const partnerVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const partnerParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
32
src/modules/partner/partner.types.ts
Normal file
32
src/modules/partner/partner.types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type {
|
||||
partnerChangeEmailRequestSchema,
|
||||
partnerChangePasswordRequestSchema,
|
||||
partnerParamsSchema,
|
||||
partnerRequestSchema,
|
||||
partnerUpdateRequestSchema,
|
||||
} from "@/modules/partner/partner.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type PartnerRequest = z.infer<typeof partnerRequestSchema>;
|
||||
|
||||
export type PartnerUpdateRequest = z.infer<typeof partnerUpdateRequestSchema>;
|
||||
|
||||
export type PartnerChangeEmailRequest = z.infer<
|
||||
typeof partnerChangeEmailRequestSchema
|
||||
>;
|
||||
|
||||
export type PartnerChangePasswordRequest = z.infer<
|
||||
typeof partnerChangePasswordRequestSchema
|
||||
>;
|
||||
|
||||
export type PartnerParams = z.infer<typeof partnerParamsSchema>;
|
||||
|
||||
export type PartnerResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
whatsapp: string;
|
||||
avatar: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -10,6 +12,7 @@ import type {
|
||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import { TransportationImage } from "@/database/entities/transportation-image.entity";
|
||||
import { Transportation } from "@/database/entities/transportation.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
import {
|
||||
@@ -29,6 +32,7 @@ export class TransportationController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: TransportationMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -526,26 +530,64 @@ export class TransportationController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.createTransportation,
|
||||
]),
|
||||
this.create.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.updateTransportation,
|
||||
]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.deleteTransportation,
|
||||
]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.createTransportationClass,
|
||||
]),
|
||||
this.createClass.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
this.listClasses.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.updateTransportationClass,
|
||||
]),
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.deleteTransportationClass,
|
||||
]),
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user