jws-backend/src/controllers/user-controller.ts

577 lines
15 KiB
TypeScript
Raw Normal View History

import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
2024-04-09 13:05:49 +07:00
import { Prisma, Status, UserType } from "@prisma/client";
2024-04-05 10:42:16 +07:00
import prisma from "../db";
import minio from "../services/minio";
import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
2024-04-17 16:35:35 +07:00
import {
addUserRoles,
createUser,
deleteUser,
editUser,
2024-04-17 16:35:35 +07:00
getRoles,
getUserRoles,
removeUserRoles,
} from "../services/keycloak";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type UserCreate = {
status?: Status;
2024-04-09 13:05:49 +07:00
userType: UserType;
userRole: string;
username: string;
firstName: string;
firstNameEN: string;
lastName: string;
lastNameEN: string;
2024-04-05 16:43:59 +07:00
gender: string;
2024-04-17 16:22:07 +07:00
checkpoint?: string | null;
checkpointEN?: string | null;
2024-04-09 17:34:53 +07:00
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
2024-04-10 11:37:12 +07:00
responsibleArea?: string | null;
birthDate?: Date | null;
address: string;
addressEN: string;
zipCode: string;
email: string;
telephoneNo: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type UserUpdate = {
status?: "ACTIVE" | "INACTIVE";
username?: string;
2024-04-09 13:05:49 +07:00
userType?: UserType;
userRole?: string;
firstName?: string;
firstNameEN?: string;
lastName?: string;
lastNameEN?: string;
2024-04-05 16:43:59 +07:00
gender?: string;
2024-04-17 16:22:07 +07:00
checkpoint?: string | null;
checkpointEN?: string | null;
2024-04-09 17:34:53 +07:00
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
2024-04-10 11:37:12 +07:00
responsibleArea?: string | null;
birthDate?: Date | null;
address?: string;
addressEN?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
function imageLocation(id: string) {
return `user/profile-img-${id}`;
}
@Route("api/user")
@Tags("User")
@Security("keycloak")
export class UserController extends Controller {
2024-04-09 17:51:46 +07:00
@Get("type-stats")
async getUserTypeStats() {
const list = await prisma.user.groupBy({
by: "userType",
_count: true,
});
return list.reduce<Record<UserType, number>>(
(a, c) => {
a[c.userType] = c._count;
return a;
},
{
USER: 0,
MESSENGER: 0,
DELEGATE: 0,
AGENCY: 0,
},
);
}
@Get()
async getUser(
2024-04-09 13:05:49 +07:00
@Query() userType?: UserType,
@Query() zipCode?: string,
@Query() includeBranch: boolean = false,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query }, zipCode, userType },
{ firstNameEN: { contains: query }, zipCode, userType },
{ lastName: { contains: query }, zipCode, userType },
{ lastNameEN: { contains: query }, zipCode, userType },
{ email: { contains: query }, zipCode, userType },
{ telephoneNo: { contains: query }, zipCode, userType },
],
} satisfies Prisma.UserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
2024-04-09 08:51:22 +07:00
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
branch: { include: { branch: includeBranch } },
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.user.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{userId}")
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record)
2024-04-09 18:04:26 +07:00
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
60 * 60,
),
});
}
@Post()
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
if (body.provinceId || body.districtId || body.subDistrictId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
]);
if (body.provinceId && !province) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.districtId && !district) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.subDistrictId && !subDistrict) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
}
const { provinceId, districtId, subDistrictId, username, ...rest } = body;
2024-04-17 16:22:07 +07:00
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
);
}
const userId = await createUser(username, username, {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
});
2024-04-17 16:22:07 +07:00
if (!userId || typeof userId !== "string") {
throw new Error("Cannot create user with keycloak service.");
}
2024-04-18 16:47:50 +07:00
const role = list.find((v) => v.name === body.userRole);
2024-04-17 16:22:07 +07:00
const resultAddRole = role && (await addUserRoles(userId, [role]));
if (!resultAddRole) {
await deleteUser(userId);
throw new Error("Failed. Cannot set user's role.");
}
const record = await prisma.user.create({
include: { province: true, district: true, subDistrict: true },
data: {
2024-04-17 16:22:07 +07:00
id: userId,
...rest,
2024-04-17 16:48:49 +07:00
username,
2024-04-17 16:22:07 +07:00
userRole: role.name,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{userId}")
async editUser(
@Request() req: RequestWithUser,
@Body() body: UserUpdate,
@Path() userId: string,
) {
if (body.subDistrictId || body.districtId || body.provinceId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
2024-04-18 16:32:38 +07:00
let userRole: string | undefined;
if (body.userRole) {
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) =>
a.name.includes(b),
),
);
}
const currentRole = await getUserRoles(userId);
2024-04-17 16:35:35 +07:00
2024-04-18 16:32:38 +07:00
const role = list.find((v) => v.id === body.userRole);
2024-04-17 16:35:35 +07:00
2024-04-18 16:32:38 +07:00
const resultAddRole = role && (await addUserRoles(userId, [role]));
2024-04-17 16:35:35 +07:00
2024-04-18 16:32:38 +07:00
if (!resultAddRole) {
throw new Error("Failed. Cannot set user's role.");
} else {
if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole);
}
2024-04-17 16:35:35 +07:00
2024-04-18 16:32:38 +07:00
userRole = role.name;
2024-04-17 16:35:35 +07:00
}
if (body.username) {
await editUser(userId, { username: body.username });
}
const { provinceId, districtId, subDistrictId, ...rest } = body;
2024-04-09 13:05:49 +07:00
const user = await prisma.user.findFirst({
where: { id: userId },
});
if (!user) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const lastUserOfType =
body.userType &&
body.userType !== user.userType &&
user.code &&
(await prisma.user.findFirst({
orderBy: { createdAt: "desc" },
where: {
userType: body.userType,
code: { startsWith: `${user.code?.slice(0, 3)}` },
},
}));
const record = await prisma.user.update({
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
2024-04-18 16:32:38 +07:00
userRole,
2024-04-09 13:05:49 +07:00
code:
(lastUserOfType &&
`${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) ||
undefined,
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
updateBy: req.user.name,
},
where: { id: userId },
});
2024-04-09 13:05:49 +07:00
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{userId}")
async deleteUser(@Path() userId: string) {
2024-04-03 16:26:52 +07:00
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
2024-04-03 16:26:52 +07:00
}
if (record.status !== Status.CREATED) {
2024-04-03 16:26:52 +07:00
throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used");
}
2024-04-04 11:02:01 +07:00
await minio.removeObject(MINIO_BUCKET, imageLocation(userId), {
forceDelete: true,
});
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
}).then((list) => {
list.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
});
});
await deleteUser(userId);
2024-04-03 16:26:52 +07:00
return await prisma.user.delete({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
}
}
function attachmentLocation(uid: string) {
return `user-attachment/${uid}`;
}
@Route("api/user/{userId}/attachment")
@Tags("User")
@Security("keycloak")
export class UserAttachmentController extends Controller {
@Get()
async listAttachment(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
const list = await new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
});
return await Promise.all(
list.map(async (v) => ({
name: v.split("/").at(-1) as string,
url: await minio.presignedGetObject(MINIO_BUCKET, v, 12 * 60 * 60),
})),
);
}
@Post()
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
return await Promise.all(
payload.file.map(async (v) => ({
name: v,
url: await minio.presignedGetObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`),
uploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
`${attachmentLocation(userId)}/${v}`,
12 * 60 * 60,
),
})),
);
}
@Delete()
async deleteAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
await Promise.all(
payload.file.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
}),
);
}
}