add auth and payment api

This commit is contained in:
ItsMalma
2025-11-19 23:53:56 +07:00
parent 8f91994f29
commit 7e7a865368
64 changed files with 9067 additions and 2662 deletions

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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'.",
),
});

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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),
);

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -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),
);

View File

@@ -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,

View File

@@ -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."),

View File

@@ -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;

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -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),
);