diff --git a/src/controllers/product/product-controller.ts b/src/controllers/product/product-controller.ts index 7fb7cd1..0c28c47 100644 --- a/src/controllers/product/product-controller.ts +++ b/src/controllers/product/product-controller.ts @@ -25,6 +25,15 @@ if (!process.env.MINIO_BUCKET) { } const MINIO_BUCKET = process.env.MINIO_BUCKET; +const MANAGE_ROLES = [ + "system", + "head_of_admin", + "admin", + "branch_admin", + "branch_manager", + "accountant", + "branch_accountant", +]; type ProductCreate = { status?: Status; @@ -51,6 +60,8 @@ type ProductCreate = { serviceCharge: number; productTypeId: string; remark?: string; + + registeredBranchId?: string; }; type ProductUpdate = { @@ -63,12 +74,20 @@ type ProductUpdate = { serviceCharge?: number; remark?: string; productTypeId?: string; + + registeredBranchId?: string; }; function imageLocation(id: string) { return `product/${id}/image`; } +function globalAllow(roles?: string[]) { + return ["system", "head_of_admin", "admin", "branch_admin", "branch_manager", "accountant"].some( + (v) => roles?.includes(v), + ); +} + @Route("api/v1/product") @Tags("Product") export class ProductController extends Controller { @@ -85,6 +104,7 @@ export class ProductController extends Controller { @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, + @Query() branchId?: string, ) { const filterStatus = (val?: Status) => { if (!val) return {}; @@ -99,6 +119,9 @@ export class ProductController extends Controller { { name: { contains: query }, productTypeId, ...filterStatus(status) }, { detail: { contains: query }, productTypeId, ...filterStatus(status) }, ], + AND: { + OR: [{ registeredBranchId: branchId }, { registeredBranchId: null }], + }, } satisfies Prisma.ProductWhereInput; const [result, total] = await prisma.$transaction([ @@ -164,15 +187,29 @@ export class ProductController extends Controller { } @Post() - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) + @Security("keycloak", MANAGE_ROLES) async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) { - const productType = await prisma.productType.findFirst({ - include: { - createdBy: true, - updatedBy: true, - }, - where: { id: body.productTypeId }, - }); + const [productType, branch] = await prisma.$transaction([ + prisma.productType.findFirst({ + include: { + createdBy: true, + updatedBy: true, + }, + where: { id: body.productTypeId }, + }), + prisma.branch.findFirst({ + include: { user: { where: { id: req.user.sub } } }, + where: { id: body.registeredBranchId }, + }), + ]); + + if (!globalAllow(req.user.roles) && !branch?.user.find((v) => v.userId === req.user.sub)) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } if (!productType) { throw new HttpError( @@ -182,6 +219,14 @@ export class ProductController extends Controller { ); } + if (body.registeredBranchId && !branch) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch cannot be found.", + "relationBranchNotFound", + ); + } + const record = await prisma.$transaction( async (tx) => { const last = await tx.runningNo.upsert({ @@ -241,19 +286,44 @@ export class ProductController extends Controller { } @Put("{productId}") - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) + @Security("keycloak", MANAGE_ROLES) async editProduct( @Request() req: RequestWithUser, @Body() body: ProductUpdate, @Path() productId: string, ) { - if (!(await prisma.product.findUnique({ where: { id: productId } }))) { + const [product, productType, branch] = await prisma.$transaction([ + prisma.product.findUnique({ + include: { + registeredBranch: { + where: { + user: { some: { userId: req.user.sub } }, + }, + }, + }, + where: { id: productId }, + }), + prisma.productType.findFirst({ + include: { + createdBy: true, + updatedBy: true, + }, + where: { id: body.productTypeId }, + }), + prisma.branch.findFirst({ where: { id: body.registeredBranchId } }), + ]); + + if (!product) { throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound"); } - const productType = await prisma.productType.findFirst({ - where: { id: body.productTypeId }, - }); + if (!globalAllow(req.user.roles) && !product.registeredBranch) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } if (!productType) { throw new HttpError( @@ -263,6 +333,14 @@ export class ProductController extends Controller { ); } + if (body.registeredBranchId && !branch) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch cannot be found.", + "relationBranchNotFound", + ); + } + const record = await prisma.product.update({ include: { createdBy: true, @@ -294,14 +372,32 @@ export class ProductController extends Controller { } @Delete("{productId}") - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) - async deleteProduct(@Path() productId: string) { - const record = await prisma.product.findFirst({ where: { id: productId } }); + @Security("keycloak", MANAGE_ROLES) + async deleteProduct(@Request() req: RequestWithUser, @Path() productId: string) { + const record = await prisma.product.findFirst({ + include: { + registeredBranch: { + where: { + user: { some: { userId: req.user.sub } }, + }, + }, + }, + + where: { id: productId }, + }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "productNotFound"); } + if (!globalAllow(req.user.roles) && !record.registeredBranch) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Product is in used.", "productInUsed"); } diff --git a/src/controllers/service/service-controller.ts b/src/controllers/service/service-controller.ts index 4ca1326..1170ab7 100644 --- a/src/controllers/service/service-controller.ts +++ b/src/controllers/service/service-controller.ts @@ -25,6 +25,15 @@ if (!process.env.MINIO_BUCKET) { } const MINIO_BUCKET = process.env.MINIO_BUCKET; +const MANAGE_ROLES = [ + "system", + "head_of_admin", + "admin", + "branch_admin", + "branch_manager", + "accountant", + "branch_accountant", +]; type ServiceCreate = { code: "MOU" | "mou"; @@ -40,6 +49,7 @@ type ServiceCreate = { attributes?: { [key: string]: any }; }[]; productTypeId: string; + registeredBranchId?: string; }; type ServiceUpdate = { @@ -55,12 +65,17 @@ type ServiceUpdate = { attributes?: { [key: string]: any }; }[]; productTypeId?: string; + registeredBranchId?: string; }; function imageLocation(id: string) { return `service/${id}/service-image`; } +function globalAllow(roles?: string[]) { + return ["system", "head_of_admin", "admin", "accountant"].some((v) => roles?.includes(v)); +} + @Route("api/v1/service") @Tags("Service") export class ServiceController extends Controller { @@ -78,6 +93,7 @@ export class ServiceController extends Controller { @Query() pageSize: number = 30, @Query() status?: Status, @Query() productTypeId?: string, + @Query() branchId?: string, ) { const filterStatus = (val?: Status) => { if (!val) return {}; @@ -92,6 +108,9 @@ export class ServiceController extends Controller { { name: { contains: query }, productTypeId, ...filterStatus(status) }, { detail: { contains: query }, productTypeId, ...filterStatus(status) }, ], + AND: { + OR: [{ registeredBranchId: branchId }, { registeredBranchId: null }], + }, } satisfies Prisma.ServiceWhereInput; const [result, total] = await prisma.$transaction([ @@ -199,17 +218,31 @@ export class ServiceController extends Controller { } @Post() - @Security("keycloak") + @Security("keycloak", MANAGE_ROLES) async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) { const { work, productTypeId, ...payload } = body; - const productType = await prisma.productType.findFirst({ - include: { - createdBy: true, - updatedBy: true, - }, - where: { id: body.productTypeId }, - }); + const [productType, branch] = await prisma.$transaction([ + prisma.productType.findFirst({ + include: { + createdBy: true, + updatedBy: true, + }, + where: { id: body.productTypeId }, + }), + prisma.branch.findFirst({ + include: { user: { where: { id: req.user.sub } } }, + where: { id: body.registeredBranchId }, + }), + ]); + + if (!globalAllow(req.user.roles) && !branch?.user.find((v) => v.userId === req.user.sub)) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } if (!productType) { throw new HttpError( @@ -219,6 +252,14 @@ export class ServiceController extends Controller { ); } + if (body.registeredBranchId && !branch) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch cannot be found.", + "relationBranchNotFound", + ); + } + const record = await prisma.$transaction( async (tx) => { const last = await tx.runningNo.upsert({ @@ -297,7 +338,7 @@ export class ServiceController extends Controller { } @Put("{serviceId}") - @Security("keycloak") + @Security("keycloak", MANAGE_ROLES) async editService( @Request() req: RequestWithUser, @Body() body: ServiceUpdate, @@ -306,7 +347,57 @@ export class ServiceController extends Controller { if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } - const { work, ...payload } = body; + const { work, productTypeId, ...payload } = body; + + const [service, productType, branch] = await prisma.$transaction([ + prisma.service.findUnique({ + include: { + registeredBranch: { + where: { + user: { some: { userId: req.user.sub } }, + }, + }, + }, + where: { id: serviceId }, + }), + prisma.productType.findFirst({ + include: { + createdBy: true, + updatedBy: true, + }, + where: { id: body.productTypeId }, + }), + prisma.branch.findFirst({ where: { id: body.registeredBranchId } }), + ]); + + if (!service) { + throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); + } + + if (!globalAllow(req.user.roles) && !service.registeredBranch) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + + if (!productType) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Product Type cannot be found.", + "relationProductTypeNotFound", + ); + } + + if (body.registeredBranchId && !branch) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch cannot be found.", + "relationBranchNotFound", + ); + } + const record = await prisma.$transaction(async (tx) => { const workList = await Promise.all( (work || []).map(async (w, wIdx) => @@ -361,14 +452,31 @@ export class ServiceController extends Controller { } @Delete("{serviceId}") - @Security("keycloak") - async deleteService(@Path() serviceId: string) { - const record = await prisma.service.findFirst({ where: { id: serviceId } }); + @Security("keycloak", MANAGE_ROLES) + async deleteService(@Request() req: RequestWithUser, @Path() serviceId: string) { + const record = await prisma.service.findFirst({ + include: { + registeredBranch: { + where: { + user: { some: { userId: req.user.sub } }, + }, + }, + }, + where: { id: serviceId }, + }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } + if (!globalAllow(req.user.roles) && !record.registeredBranch) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Service is in used.", "serviceInUsed"); }