import { Body, Controller, Delete, Get, Put, Path, Post, Query, Request, Route, Security, Tags, } from "tsoa"; import { Prisma, Status } from "@prisma/client"; 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"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); } const MINIO_BUCKET = process.env.MINIO_BUCKET; type UserCreate = { status?: Status; keycloakId: string; userType: string; userRole: string; firstName: string; firstNameEN: string; lastName: string; lastNameEN: string; gender: string; code?: string; registrationNo?: string; startDate?: Date; retireDate?: Date; discountCondition?: string; licenseNo?: string; licenseIssueDate?: Date; licenseExpireDate?: Date; sourceNationality?: string; importNationality?: string; trainingPlace?: string; address: string; addressEN: string; zipCode: string; email: string; telephoneNo: string; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; }; type UserUpdate = { status?: "ACTIVE" | "INACTIVE"; userType?: string; userRole?: string; firstName?: string; firstNameEN?: string; lastName?: string; lastNameEN?: string; gender?: string; code?: string; registrationNo?: string; startDate?: Date; retireDate?: Date; discountCondition?: string; licenseNo?: string; licenseIssueDate?: Date; licenseExpireDate?: Date; sourceNationality?: string; importNationality?: string; trainingPlace?: string; 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 { @Get() async getUser( @Query() userType?: string, @Query() zipCode?: string, @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({ orderBy: { createdAt: "asc" }, include: { province: true, district: true, subDistrict: true, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.user.count({ where }), ]); return { result: await Promise.all( result.map(async (v) => ({ ...v, 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) 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, ...rest } = body; const record = await prisma.user.create({ include: { province: true, district: true, subDistrict: true }, data: { ...rest, 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", ); } const { provinceId, districtId, subDistrictId, ...rest } = body; const record = await prisma.user.update({ include: { province: true, district: true, subDistrict: true }, data: { ...rest, 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 }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found."); } 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) { 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"); } if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used"); } await minio.removeObject(MINIO_BUCKET, imageLocation(userId), { forceDelete: true, }); return await prisma.user.delete({ include: { province: true, district: true, subDistrict: true, }, where: { id: userId }, }); } }