diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts new file mode 100644 index 0000000..1df8673 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts @@ -0,0 +1,14 @@ +import { useCheckPermissionsMutation } from "../_queries/mutations" + +export const useCheckPermissionsHandler = () => { + const { mutateAsync: checkPermissions, isPending } = useCheckPermissionsMutation() + + const handleCheckPermissions = async (userId: string, action: string, resource: string) => { + return await checkPermissions({ userId, action, resource }) + } + + return { + handleCheckPermissions, + isPending, + } +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_queries/mutations.ts b/sigap-website/app/(pages)/(auth)/_queries/mutations.ts index 7eabde1..13fef9c 100644 --- a/sigap-website/app/(pages)/(auth)/_queries/mutations.ts +++ b/sigap-website/app/(pages)/(auth)/_queries/mutations.ts @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query" -import { sendMagicLink, sendPasswordRecovery, signInPasswordless, signInWithPassword, signOut, verifyOtp } from "../action" +import { checkPermissions, sendMagicLink, sendPasswordRecovery, signInPasswordless, signInWithPassword, signOut, verifyOtp } from "../action" export const useSignInPasswordlessMutation = () => { return useMutation({ @@ -41,4 +41,11 @@ export const useVerifyOtpMutation = () => { mutationKey: ["verifyOtp"], mutationFn: async (formData: FormData) => await verifyOtp(formData), }) +} + +export const useCheckPermissionsMutation = () => { + return useMutation({ + mutationKey: ["checkPermissions"], + mutationFn: async ({ userId, action, resource }: { userId: string; action: string; resource: string }) => await checkPermissions(userId, action, resource), + }) } \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/action.ts b/sigap-website/app/(pages)/(auth)/action.ts index ad97233..c61753c 100644 --- a/sigap-website/app/(pages)/(auth)/action.ts +++ b/sigap-website/app/(pages)/(auth)/action.ts @@ -226,4 +226,27 @@ export async function sendPasswordRecovery(email: string) { } } }) +} + +export async function checkPermissions(userId: string, action: string, resource: string) { + const instrumentationService = getInjection("IInstrumentationService") + return await instrumentationService.instrumentServerAction("checkPermissions", { + recordResponse: true + }, async () => { + try { + const checkPermissionsController = getInjection("ICheckPermissionsController") + return await checkPermissionsController({ userId, action, resource }) + } catch (err) { + if (err instanceof InputParseError) { + return { error: err.message } + } + + const crashReporterService = getInjection("ICrashReporterService") + crashReporterService.report(err) + + return { + error: "An error occurred during permissions check. Please try again later.", + } + } + }) } \ No newline at end of file diff --git a/sigap-website/di/container.ts b/sigap-website/di/container.ts index d8ffab6..fa7b993 100644 --- a/sigap-website/di/container.ts +++ b/sigap-website/di/container.ts @@ -5,6 +5,9 @@ import { createAuthenticationModule } from './modules/authentication.module'; import { createMonitoringModule } from './modules/monitoring.module'; import { createTransactionManagerModule } from './modules/database.modul'; import { createUsersModule } from './modules/users.module'; +import { createRolesModule } from './modules/roles.module'; +import { createPermissionsModule } from './modules/permissions.module'; +import { createResourcesModule } from './modules/resources.module'; const ApplicationContainer = createContainer(); @@ -12,6 +15,9 @@ ApplicationContainer.load(Symbol('MonitoringModule'), createMonitoringModule()); ApplicationContainer.load(Symbol('TransactionManagerModule'), createTransactionManagerModule()); ApplicationContainer.load(Symbol('AuthenticationModule'), createAuthenticationModule()); ApplicationContainer.load(Symbol('UsersModule'), createUsersModule()); +ApplicationContainer.load(Symbol('RolesModule'), createRolesModule()); +ApplicationContainer.load(Symbol('PermissionsModule'), createPermissionsModule()); +ApplicationContainer.load(Symbol('ResourcesModule'), createResourcesModule()); export function getInjection( symbol: K diff --git a/sigap-website/di/modules/permissions.module.ts b/sigap-website/di/modules/permissions.module.ts new file mode 100644 index 0000000..024508e --- /dev/null +++ b/sigap-website/di/modules/permissions.module.ts @@ -0,0 +1,107 @@ +import { createModule } from '@evyweb/ioctopus'; + +import { DI_SYMBOLS } from '@/di/types'; +import { createPermissionUseCase } from '@/src/application/use-cases/permissions/create-permissions.use-case'; +import { getAllPermissionsUseCase } from '@/src/application/use-cases/permissions/get-all-permissions'; +import { getPermissionByIdUseCase } from '@/src/application/use-cases/permissions/get-permissions-by-id.use-case'; +import { getPermissionByRoleUseCase } from '@/src/application/use-cases/permissions/get-permissions-by-role.use-case'; +import { updatePermissionUseCase } from '@/src/application/use-cases/permissions/update-permissions.use-case'; +import { deletePermissionUseCase } from '@/src/application/use-cases/permissions/delete-permissions.use-case'; +import { createPermissionController } from '@/src/interface-adapters/controllers/permissions/create-permission.controller'; +import { getAllPermissionsController } from '@/src/interface-adapters/controllers/permissions/get-all-permission.controller'; +import { getPermissionByIdController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-id.controller'; +import { getPermissionByRoleController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-role.controller'; +import { updatePermissionController } from '@/src/interface-adapters/controllers/permissions/update-permission.controller'; +import { deletePermissionController } from '@/src/interface-adapters/controllers/permissions/delete-permission.controller'; + +export function createPermissionsModule() { + const permissionsModule = createModule(); + + // Use Cases + permissionsModule + .bind(DI_SYMBOLS.ICreatePermissionUseCase) + .toHigherOrderFunction(createPermissionUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetAllPermissionsUseCase) + .toHigherOrderFunction(getAllPermissionsUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetPermissionByIdUseCase) + .toHigherOrderFunction(getPermissionByIdUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetPermissionByRoleUseCase) + .toHigherOrderFunction(getPermissionByRoleUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IUpdatePermissionUseCase) + .toHigherOrderFunction(updatePermissionUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IDeletePermissionUseCase) + .toHigherOrderFunction(deletePermissionUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IPermissionsRepository, + ]); + + // Controllers + permissionsModule + .bind(DI_SYMBOLS.ICreatePermissionController) + .toHigherOrderFunction(createPermissionController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.ICreatePermissionUseCase, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetAllPermissionsController) + .toHigherOrderFunction(getAllPermissionsController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetAllPermissionsUseCase, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetPermissionByIdController) + .toHigherOrderFunction(getPermissionByIdController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetPermissionByIdUseCase, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IGetPermissionByRoleController) + .toHigherOrderFunction(getPermissionByRoleController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetPermissionByRoleUseCase, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IUpdatePermissionController) + .toHigherOrderFunction(updatePermissionController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IUpdatePermissionUseCase, + ]); + + permissionsModule + .bind(DI_SYMBOLS.IDeletePermissionController) + .toHigherOrderFunction(deletePermissionController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IDeletePermissionUseCase, + ]); + + return permissionsModule; +} diff --git a/sigap-website/di/modules/resources.module.ts b/sigap-website/di/modules/resources.module.ts new file mode 100644 index 0000000..9042c3e --- /dev/null +++ b/sigap-website/di/modules/resources.module.ts @@ -0,0 +1,91 @@ +import { createModule } from '@evyweb/ioctopus'; + +import { DI_SYMBOLS } from '@/di/types'; +import { createResourceUseCase } from '@/src/application/use-cases/resources/create-resources.use-case'; +import { getResourceByIdUseCase } from '@/src/application/use-cases/resources/get-resource-by-id.use-case'; +import { getResourcesByTypeUseCase } from '@/src/application/use-cases/resources/get-resources-by-type.use-case'; +import { updateResourceUseCase } from '@/src/application/use-cases/resources/update-resource.use-case'; +import { deleteResourceUseCase } from '@/src/application/use-cases/resources/delete-resource.use-case'; +import { createResourceController } from '@/src/interface-adapters/controllers/resources/create-resource.controller'; +import { getResourceByIdController } from '@/src/interface-adapters/controllers/resources/get-resource-by-id.controller'; +import { getResourcesByTypeController } from '@/src/interface-adapters/controllers/resources/get-resources-by-type.controller'; +import { updateResourceController } from '@/src/interface-adapters/controllers/resources/update-resource.controller'; +import { deleteResourceController } from '@/src/interface-adapters/controllers/resources/delete-resource.controller'; + +export function createResourcesModule() { + const resourcesModule = createModule(); + + // Use Cases + resourcesModule + .bind(DI_SYMBOLS.ICreateResourceUseCase) + .toHigherOrderFunction(createResourceUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IResourcesRepository, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IGetResourceByIdUseCase) + .toHigherOrderFunction(getResourceByIdUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IResourcesRepository, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IGetResourcesByTypeUseCase) + .toHigherOrderFunction(getResourcesByTypeUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IResourcesRepository, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IUpdateResourceUseCase) + .toHigherOrderFunction(updateResourceUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IResourcesRepository, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IDeleteResourceUseCase) + .toHigherOrderFunction(deleteResourceUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IResourcesRepository, + ]); + + // Controllers + resourcesModule + .bind(DI_SYMBOLS.ICreateResourceController) + .toHigherOrderFunction(createResourceController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.ICreateResourceUseCase, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IGetResourceByIdController) + .toHigherOrderFunction(getResourceByIdController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetResourceByIdUseCase, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IGetResourcesByTypeController) + .toHigherOrderFunction(getResourcesByTypeController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetResourcesByTypeUseCase, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IUpdateResourceController) + .toHigherOrderFunction(updateResourceController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IUpdateResourceUseCase, + ]); + + resourcesModule + .bind(DI_SYMBOLS.IDeleteResourceController) + .toHigherOrderFunction(deleteResourceController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IDeleteResourceUseCase, + ]); + + return resourcesModule; +} diff --git a/sigap-website/di/modules/roles.module.ts b/sigap-website/di/modules/roles.module.ts new file mode 100644 index 0000000..b55f7fc --- /dev/null +++ b/sigap-website/di/modules/roles.module.ts @@ -0,0 +1,75 @@ +import { createModule } from '@evyweb/ioctopus'; + +import { DI_SYMBOLS } from '@/di/types'; +import { createRoleUseCase } from '@/src/application/use-cases/roles/create-role.use-case'; +import { getRoleByIdUseCase } from '@/src/application/use-cases/roles/get-role-by-id.use-case'; +import { updateRoleUseCase } from '@/src/application/use-cases/roles/update-role.use-case'; +import { deleteRoleUseCase } from '@/src/application/use-cases/roles/delete-role.use-case'; +import { createRoleController } from '@/src/interface-adapters/controllers/roles/create-role.controller'; +import { getRoleByIdController } from '@/src/interface-adapters/controllers/roles/get-role-by-id.controller'; +import { updateRoleController } from '@/src/interface-adapters/controllers/roles/update-role.controller'; +import { deleteRoleController } from '@/src/interface-adapters/controllers/roles/delete-role.controller'; + +export function createRolesModule() { + const rolesModule = createModule(); + + // Use Cases + rolesModule + .bind(DI_SYMBOLS.ICreateRoleUseCase) + .toHigherOrderFunction(createRoleUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IRolesRepository, + ]); + + rolesModule + .bind(DI_SYMBOLS.IGetRoleByIdUseCase) + .toHigherOrderFunction(getRoleByIdUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IRolesRepository, + ]); + + rolesModule + .bind(DI_SYMBOLS.IUpdateRoleUseCase) + .toHigherOrderFunction(updateRoleUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IRolesRepository, + ]); + + rolesModule + .bind(DI_SYMBOLS.IDeleteRoleUseCase) + .toHigherOrderFunction(deleteRoleUseCase, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IRolesRepository, + ]); + + // Controllers + rolesModule + .bind(DI_SYMBOLS.ICreateRoleController) + .toHigherOrderFunction(createRoleController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.ICreateRoleUseCase, + ]); + + rolesModule + .bind(DI_SYMBOLS.IGetRoleByIdController) + .toHigherOrderFunction(getRoleByIdController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IGetRoleByIdUseCase, + ]); + + rolesModule + .bind(DI_SYMBOLS.IUpdateRoleController) + .toHigherOrderFunction(updateRoleController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IUpdateRoleUseCase, + ]); + + rolesModule + .bind(DI_SYMBOLS.IDeleteRoleController) + .toHigherOrderFunction(deleteRoleController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IDeleteRoleUseCase, + ]); + + return rolesModule; +} diff --git a/sigap-website/di/modules/users.module.ts b/sigap-website/di/modules/users.module.ts index 1f0b335..27015c1 100644 --- a/sigap-website/di/modules/users.module.ts +++ b/sigap-website/di/modules/users.module.ts @@ -90,7 +90,7 @@ export function createUsersModule() { ]); usersModule - .bind(DI_SYMBOLS.IGetUserByUserNameUseCase) + .bind(DI_SYMBOLS.IGetUserByUsernameUseCase) .toHigherOrderFunction(getUserByUsernameUseCase, [ DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IUsersRepository @@ -180,7 +180,7 @@ export function createUsersModule() { .bind(DI_SYMBOLS.IGetUserByUsernameController) .toHigherOrderFunction(getUserByUsernameController, [ DI_SYMBOLS.IInstrumentationService, - DI_SYMBOLS.IGetUserByUserNameUseCase + DI_SYMBOLS.IGetUserByUsernameUseCase ]); usersModule diff --git a/sigap-website/di/types.ts b/sigap-website/di/types.ts index 2ed6fdc..5358d33 100644 --- a/sigap-website/di/types.ts +++ b/sigap-website/di/types.ts @@ -40,8 +40,46 @@ import { ISendPasswordRecoveryController } from '@/src/interface-adapters/contro import { IUploadAvatarController } from '@/src/interface-adapters/controllers/users/upload-avatar.controller'; import { IUploadAvatarUseCase } from '@/src/application/use-cases/users/upload-avatar.use-case'; import { ISignInWithPasswordController } from '@/src/interface-adapters/controllers/auth/sign-in-with-password.controller'; +import { IRolesRepository } from '@/src/application/repositories/roles.repository.interface'; +import { IPermissionsRepository } from '@/src/application/repositories/permissions.repository.interface'; +import { IResourcesRepository } from '@/src/application/repositories/resources.repository.interface'; +import { ICreateRoleUseCase } from '@/src/application/use-cases/roles/create-role.use-case'; +import { IGetRoleByIdUseCase } from '@/src/application/use-cases/roles/get-role-by-id.use-case'; +import { IUpdateRoleUseCase } from '@/src/application/use-cases/roles/update-role.use-case'; +import { IDeleteRoleUseCase } from '@/src/application/use-cases/roles/delete-role.use-case'; +import { ICreatePermissionUseCase } from '@/src/application/use-cases/permissions/create-permissions.use-case'; +import { IGetAllPermissionsUseCase } from '@/src/application/use-cases/permissions/get-all-permissions'; +import { IGetPermissionByIdUseCase } from '@/src/application/use-cases/permissions/get-permissions-by-id.use-case'; +import { IGetPermissionByRoleUseCase } from '@/src/application/use-cases/permissions/get-permissions-by-role.use-case'; +import { IGetPermissionByRoleAndResourceController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-role-and-resource.controller'; +import { IGetPermissionByRoleAndResourcesUseCase } from '@/src/application/use-cases/permissions/get-permissions-by-role-and-resources.use-case'; +import { IUpdatePermissionUseCase } from '@/src/application/use-cases/permissions/update-permissions.use-case'; +import { IDeletePermissionUseCase } from '@/src/application/use-cases/permissions/delete-permissions.use-case'; +import { ICreateResourceUseCase } from '@/src/application/use-cases/resources/create-resources.use-case'; +import { IGetResourceByIdUseCase } from '@/src/application/use-cases/resources/get-resource-by-id.use-case'; +import { IGetResourcesByTypeUseCase } from '@/src/application/use-cases/resources/get-resources-by-type.use-case'; +import { IUpdateResourceUseCase } from '@/src/application/use-cases/resources/update-resource.use-case'; +import { IDeleteResourceUseCase } from '@/src/application/use-cases/resources/delete-resource.use-case'; +import { ICheckPermissionsController } from '@/src/interface-adapters/controllers/auth/check-permissions.controller'; +import { ICreateRoleController } from '@/src/interface-adapters/controllers/roles/create-role.controller'; +import { IGetRoleByIdController } from '@/src/interface-adapters/controllers/roles/get-role-by-id.controller'; +import { IUpdateRoleController } from '@/src/interface-adapters/controllers/roles/update-role.controller'; +import { IDeleteRoleController } from '@/src/interface-adapters/controllers/roles/delete-role.controller'; +import { ICreatePermissionController } from '@/src/interface-adapters/controllers/permissions/create-permission.controller'; +import { IGetAllPermissionsController } from '@/src/interface-adapters/controllers/permissions/get-all-permission.controller'; +import { IGetPermissionByIdController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-id.controller'; +import { IGetPermissionByRoleController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-role.controller'; +import { IUpdatePermissionController } from '@/src/interface-adapters/controllers/permissions/update-permission.controller'; +import { IDeletePermissionController } from '@/src/interface-adapters/controllers/permissions/delete-permission.controller'; +import { IGetResourceByIdController } from '@/src/interface-adapters/controllers/resources/get-resource-by-id.controller'; +import { IGetResourcesByTypeController } from '@/src/interface-adapters/controllers/resources/get-resources-by-type.controller'; +import { IDeleteResourceController } from '@/src/interface-adapters/controllers/resources/delete-resource.controller'; +import { IUpdateResourceController } from '@/src/interface-adapters/controllers/resources/update-resource.controller'; +import { ICreateResourceController } from '@/src/interface-adapters/controllers/resources/create-resource.controller'; +import { ICheckPermissionsUseCase } from '@/src/application/use-cases/auth/check-permissions.use-case'; -export const DI_SYMBOLS = { +// Pastikan DI_SYMBOLS memiliki tipe yang sesuai dengan DI_RETURN_TYPES +export const DI_SYMBOLS: { [K in keyof DI_RETURN_TYPES]: symbol } = { // Services IAuthenticationService: Symbol.for('IAuthenticationService'), ITransactionManagerService: Symbol.for('ITransactionManagerService'), @@ -50,37 +88,69 @@ export const DI_SYMBOLS = { // Repositories IUsersRepository: Symbol.for('IUsersRepository'), + IRolesRepository: Symbol.for('IRolesRepository'), + IPermissionsRepository: Symbol.for('IPermissionsRepository'), + IResourcesRepository: Symbol.for('IResourcesRepository'), // Use Cases + + // Auth Use Cases ISignInPasswordlessUseCase: Symbol.for('ISignInPasswordlessUseCase'), - ISignInWithPasswordUseCase: Symbol.for('ISignInWithPasswordUseCase'), ISignUpUseCase: Symbol.for('ISignUpUseCase'), IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'), ISignOutUseCase: Symbol.for('ISignOutUseCase'), ISendMagicLinkUseCase: Symbol.for('ISendMagicLinkUseCase'), ISendPasswordRecoveryUseCase: Symbol.for('ISendPasswordRecoveryUseCase'), + ICheckPermissionsUseCase: Symbol.for('ICheckPermissionsUseCase'), + // User Use Cases IBanUserUseCase: Symbol.for('IBanUserUseCase'), IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'), IGetCurrentUserUseCase: Symbol.for('IGetCurrentUserUseCase'), IGetUsersUseCase: Symbol.for('IGetUsersUseCase'), IGetUserByIdUseCase: Symbol.for('IGetUserByIdUseCase'), IGetUserByEmailUseCase: Symbol.for('IGetUserByEmailUseCase'), - IGetUserByUserNameUseCase: Symbol.for('IGetUserByUserNameUseCase'), + IGetUserByUsernameUseCase: Symbol.for('IGetUserByUsernameUseCase'), IInviteUserUseCase: Symbol.for('IInviteUserUseCase'), ICreateUserUseCase: Symbol.for('ICreateUserUseCase'), IUpdateUserUseCase: Symbol.for('IUpdateUserUseCase'), IDeleteUserUseCase: Symbol.for('IDeleteUserUseCase'), IUploadAvatarUseCase: Symbol.for('IUploadAvatarUseCase'), + // Role Use Cases + ICreateRoleUseCase: Symbol.for('ICreateRoleUseCase'), + IGetRoleByIdUseCase: Symbol.for('IGetRoleByIdUseCase'), + IUpdateRoleUseCase: Symbol.for('IUpdateRoleUseCase'), + IDeleteRoleUseCase: Symbol.for('IDeleteRoleUseCase'), + + // Permission Use Cases + ICreatePermissionUseCase: Symbol.for('ICreatePermissionUseCase'), + IGetAllPermissionsUseCase: Symbol.for('IGetAllPermissionsUseCase'), + IGetPermissionByIdUseCase: Symbol.for('IGetPermissionByIdUseCase'), + IGetPermissionByRoleUseCase: Symbol.for('IGetPermissionByRoleUseCase'), + IGetPermissionByRoleAndResourcesUseCase: Symbol.for('IGetPermissionByRoleAndResourcesUseCase'), + IUpdatePermissionUseCase: Symbol.for('IUpdatePermissionUseCase'), + IDeletePermissionUseCase: Symbol.for('IDeletePermissionUseCase'), + + // Resource Use Cases + ICreateResourceUseCase: Symbol.for('ICreateResourceUseCase'), + IGetResourceByIdUseCase: Symbol.for('IGetResourceByIdUseCase'), + IGetResourcesByTypeUseCase: Symbol.for('IGetResourcesByTypeUseCase'), + IUpdateResourceUseCase: Symbol.for('IUpdateResourceUseCase'), + IDeleteResourceUseCase: Symbol.for('IDeleteResourceUseCase'), + // Controllers + + // Auth Controllers ISignInPasswordlessController: Symbol.for('ISignInPasswordlessController'), ISignInWithPasswordController: Symbol.for('ISignInWithPasswordController'), ISignOutController: Symbol.for('ISignOutController'), IVerifyOtpController: Symbol.for('IVerifyOtpController'), ISendMagicLinkController: Symbol.for('ISendMagicLinkController'), ISendPasswordRecoveryController: Symbol.for('ISendPasswordRecoveryController'), + ICheckPermissionsController: Symbol.for('ICheckPermissionsController'), + // User Controllers IBanUserController: Symbol.for('IBanUserController'), IUnbanUserController: Symbol.for('IUnbanUserController'), IGetCurrentUserController: Symbol.for('IGetCurrentUserController'), @@ -93,6 +163,28 @@ export const DI_SYMBOLS = { IUpdateUserController: Symbol.for('IUpdateUserController'), IDeleteUserController: Symbol.for('IDeleteUserController'), IUploadAvatarController: Symbol.for('IUploadAvatarController'), + + // Role Controllers + ICreateRoleController: Symbol.for('ICreateRoleController'), + IGetRoleByIdController: Symbol.for('IGetRoleByIdController'), + IUpdateRoleController: Symbol.for('IUpdateRoleController'), + IDeleteRoleController: Symbol.for('IDeleteRoleController'), + + // Permission Controllers + ICreatePermissionController: Symbol.for('ICreatePermissionController'), + IGetAllPermissionsController: Symbol.for('IGetAllPermissionsController'), + IGetPermissionByIdController: Symbol.for('IGetPermissionByIdController'), + IGetPermissionByRoleController: Symbol.for('IGetPermissionByRoleController'), + IGetPermissionByRoleAndResourceController: Symbol.for('IGetPermissionByRoleAndResourceController'), + IUpdatePermissionController: Symbol.for('IUpdatePermissionController'), + IDeletePermissionController: Symbol.for('IDeletePermissionController'), + + // Resource Controllers + ICreateResourceController: Symbol.for('ICreateResourceController'), + IGetResourceByIdController: Symbol.for('IGetResourceByIdController'), + IGetResourcesByTypeController: Symbol.for('IGetResourcesByTypeController'), + IUpdateResourceController: Symbol.for('IUpdateResourceController'), + IDeleteResourceController: Symbol.for('IDeleteResourceController'), }; export interface DI_RETURN_TYPES { @@ -104,36 +196,69 @@ export interface DI_RETURN_TYPES { // Repositories IUsersRepository: IUsersRepository; + IRolesRepository: IRolesRepository; + IPermissionsRepository: IPermissionsRepository; + IResourcesRepository: IResourcesRepository; // Use Cases + + // Auth Use Cases ISignInPasswordlessUseCase: ISignInPasswordlessUseCase; ISignUpUseCase: ISignUpUseCase; IVerifyOtpUseCase: IVerifyOtpUseCase; ISignOutUseCase: ISignOutUseCase; ISendMagicLinkUseCase: ISendMagicLinkUseCase; ISendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase; + ICheckPermissionsUseCase: ICheckPermissionsUseCase; + // User Use Cases IBanUserUseCase: IBanUserUseCase; IUnbanUserUseCase: IUnbanUserUseCase; IGetCurrentUserUseCase: IGetCurrentUserUseCase; IGetUsersUseCase: IGetUsersUseCase; IGetUserByIdUseCase: IGetUserByIdUseCase; IGetUserByEmailUseCase: IGetUserByEmailUseCase; - IGetUserByUserNameUseCase: IGetUserByUsernameUseCase; + IGetUserByUsernameUseCase: IGetUserByUsernameUseCase; IInviteUserUseCase: IInviteUserUseCase; ICreateUserUseCase: ICreateUserUseCase; IUpdateUserUseCase: IUpdateUserUseCase; IDeleteUserUseCase: IDeleteUserUseCase; IUploadAvatarUseCase: IUploadAvatarUseCase; + // Role Use Cases + ICreateRoleUseCase: ICreateRoleUseCase; + IGetRoleByIdUseCase: IGetRoleByIdUseCase; + IUpdateRoleUseCase: IUpdateRoleUseCase; + IDeleteRoleUseCase: IDeleteRoleUseCase; + + // Permission Use Cases + ICreatePermissionUseCase: ICreatePermissionUseCase; + IGetAllPermissionsUseCase: IGetAllPermissionsUseCase; + IGetPermissionByIdUseCase: IGetPermissionByIdUseCase; + IGetPermissionByRoleUseCase: IGetPermissionByRoleUseCase; + IGetPermissionByRoleAndResourcesUseCase: IGetPermissionByRoleAndResourcesUseCase; + IUpdatePermissionUseCase: IUpdatePermissionUseCase; + IDeletePermissionUseCase: IDeletePermissionUseCase; + + // Resource Use Cases + ICreateResourceUseCase: ICreateResourceUseCase; + IGetResourceByIdUseCase: IGetResourceByIdUseCase; + IGetResourcesByTypeUseCase: IGetResourcesByTypeUseCase; + IUpdateResourceUseCase: IUpdateResourceUseCase; + IDeleteResourceUseCase: IDeleteResourceUseCase; + // Controllers + + // Auth Controllers ISignInPasswordlessController: ISignInPasswordlessController; ISignInWithPasswordController: ISignInWithPasswordController; IVerifyOtpController: IVerifyOtpController; ISignOutController: ISignOutController; ISendMagicLinkController: ISendMagicLinkController; ISendPasswordRecoveryController: ISendPasswordRecoveryController; + ICheckPermissionsController: ICheckPermissionsController; + // User Controllers IBanUserController: IBanUserController; IUnbanUserController: IUnbanUserController; IGetCurrentUserController: IGetCurrentUserController; @@ -147,4 +272,26 @@ export interface DI_RETURN_TYPES { IDeleteUserController: IDeleteUserController; IUploadAvatarController: IUploadAvatarController; + // Role Controllers + ICreateRoleController: ICreateRoleController; + IGetRoleByIdController: IGetRoleByIdController; + IUpdateRoleController: IUpdateRoleController; + IDeleteRoleController: IDeleteRoleController; + + // Permission Controllers + ICreatePermissionController: ICreatePermissionController; + IGetAllPermissionsController: IGetAllPermissionsController; + IGetPermissionByIdController: IGetPermissionByIdController; + IGetPermissionByRoleController: IGetPermissionByRoleController; + IGetPermissionByRoleAndResourceController: IGetPermissionByRoleAndResourceController; + IUpdatePermissionController: IUpdatePermissionController; + IDeletePermissionController: IDeletePermissionController; + + // Resource Controllers + ICreateResourceController: ICreateResourceController; + IGetResourceByIdController: IGetResourceByIdController; + IGetResourcesByTypeController: IGetResourcesByTypeController; + IUpdateResourceController: IUpdateResourceController; + IDeleteResourceController: IDeleteResourceController; + } \ No newline at end of file diff --git a/sigap-website/package.json b/sigap-website/package.json index ad01913..412663f 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -7,7 +7,7 @@ "db:seed": "npx prisma db seed" }, "prisma": { - "seed": "ts-node prisma/seed.ts" + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }, "dependencies": { "@evyweb/ioctopus": "^1.2.0", @@ -73,4 +73,4 @@ "ts-node": "^10.9.2", "typescript": "^5.7.2" } -} +} \ No newline at end of file diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index a80fb25..494e0ec 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -183,6 +183,7 @@ model roles { model resources { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String @unique @db.VarChar(255) + type String? description String? instance_role String? relations String? @@ -195,8 +196,8 @@ model resources { model permissions { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid action String - resource_id String - role_id String + resource_id String @db.Uuid + role_id String @db.Uuid resource resources @relation(fields: [resource_id], references: [id]) role roles @relation(fields: [role_id], references: [id]) created_at DateTime @default(now()) @db.Timestamptz(6) diff --git a/sigap-website/prisma/seed.ts b/sigap-website/prisma/seed.ts new file mode 100644 index 0000000..44b3aea --- /dev/null +++ b/sigap-website/prisma/seed.ts @@ -0,0 +1,201 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Starting seeding...'); + + // Create roles + const adminRole = await prisma.roles.upsert({ + where: { name: 'admin' }, + update: {}, + create: { + + name: 'admin', + description: 'Administrator with full access to all features', + }, + }); + + const viewerRole = await prisma.roles.upsert({ + where: { name: 'viewer' }, + update: {}, + create: { + + name: 'viewer', + description: 'Read-only access to data', + }, + }); + + const staffRole = await prisma.roles.upsert({ + where: { name: 'staff' }, + update: {}, + create: { + + name: 'staff', + description: 'Staff with limited administrative access', + }, + }); + + console.log('Roles created:', { adminRole, viewerRole, staffRole }); + + // Create resources based on Prisma schema models + const resources = [ + { + name: 'cities', + description: 'City data management', + attributes: { + fields: ['id', 'name', 'code', 'geographic_id', 'created_at', 'updated_at'] + } + }, + { + name: 'contact_messages', + description: 'Contact message management', + attributes: { + fields: ['id', 'name', 'email', 'phone', 'message_type', 'message_type_label', 'message', 'status', 'created_at', 'updated_at'] + } + }, + { + name: 'crime_cases', + description: 'Crime case management', + attributes: { + fields: ['id', 'crime_id', 'crime_category_id', 'date', 'time', 'location', 'latitude', 'longitude', 'description', 'victim_count', 'status', 'created_at', 'updated_at'] + } + }, + { + name: 'crime_categories', + description: 'Crime category management', + attributes: { + fields: ['id', 'name', 'description', 'created_at', 'updated_at'] + } + }, + { + name: 'crimes', + description: 'Crime data management', + attributes: { + fields: ['id', 'district_id', 'city_id', 'year', 'number_of_crime', 'rate', 'heat_map', 'created_at', 'updated_at'] + } + }, + { + name: 'demographics', + description: 'Demographic data management', + attributes: { + fields: ['id', 'district_id', 'city_id', 'province_id', 'year', 'population', 'population_density', 'poverty_rate', 'created_at', 'updated_at'] + } + }, + { + name: 'districts', + description: 'District data management', + attributes: { + fields: ['id', 'city_id', 'name', 'code', 'created_at', 'updated_at'] + } + }, + { + name: 'geographics', + description: 'Geographic data management', + attributes: { + fields: ['id', 'district_id', 'latitude', 'longitude', 'land_area', 'polygon', 'created_at', 'updated_at'] + } + }, + { + name: 'profiles', + description: 'User profile management', + attributes: { + fields: ['id', 'user_id', 'avatar', 'username', 'first_name', 'last_name', 'bio', 'address', 'birth_date'] + } + }, + { + name: 'users', + description: 'User account management', + attributes: { + fields: ['id', 'roles_id', 'email', 'phone', 'encrypted_password', 'invited_at', 'confirmed_at', 'email_confirmed_at', 'recovery_sent_at', 'last_sign_in_at', 'app_metadata', 'user_metadata', 'created_at', 'updated_at', 'banned_until', 'is_anonymous'] + } + }, + { + name: 'roles', + description: 'Role management', + attributes: { + fields: ['id', 'name', 'description', 'created_at', 'updated_at'] + } + }, + { + name: 'resources', + description: 'Resource management', + attributes: { + fields: ['id', 'name', 'description', 'instance_role', 'relations', 'attributes', 'created_at', 'updated_at'] + } + }, + { + name: 'permissions', + description: 'Permission management', + attributes: { + fields: ['id', 'action', 'resource_id', 'role_id', 'created_at', 'updated_at'] + } + } + ]; + + // Create resources in the database + for (const resource of resources) { + const createdResource = await prisma.resources.upsert({ + where: { name: resource.name }, + update: {}, + create: { + name: resource.name, + description: resource.description, + attributes: resource.attributes + }, + }); + console.log(`Resource ${resource.name} created/updated with ID: ${createdResource.id}`); + } + + // Set up basic permissions for each role + const allResources = await prisma.resources.findMany(); + + // Admin permissions - full access to all resources + for (const resource of allResources) { + await createPermissions(adminRole.id, resource.id, ['create', 'read', 'update', 'delete']); + } + + // Viewer permissions - read-only access to all resources + for (const resource of allResources) { + await createPermissions(viewerRole.id, resource.id, ['read']); + } + + // Staff permissions - mixed permissions based on resource + for (const resource of allResources) { + if (['roles', 'permissions', 'resources', 'users'].includes(resource.name)) { + // Staff can only read roles, permissions, resources and users + await createPermissions(staffRole.id, resource.id, ['read']); + } else { + // Staff can create, read, update but not delete other resources + await createPermissions(staffRole.id, resource.id, ['create', 'read', 'update']); + } + } + + console.log('Seeding completed!'); +} + +async function createPermissions(roleId: string, resourceId: string, actions: string[]) { + for (const action of actions) { + await prisma.permissions.createMany({ + data: { + action: action, + resource_id: resourceId, + role_id: roleId, + }, + skipDuplicates: true // Skip if the permission already exists + }).catch((error) => { + console.error(`Error creating permission for role ${roleId} on resource ${resourceId}:`, error); + }); + } +} + + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); \ No newline at end of file diff --git a/sigap-website/src/application/repositories/permissions.repository.interface.ts b/sigap-website/src/application/repositories/permissions.repository.interface.ts new file mode 100644 index 0000000..f4030d7 --- /dev/null +++ b/sigap-website/src/application/repositories/permissions.repository.interface.ts @@ -0,0 +1,15 @@ +import { ICreatePermissionSchema } from "@/src/entities/models/permissions/create-permission.model"; +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IUpdatePermissionSchema } from "@/src/entities/models/permissions/update-permission.model"; +import { permissions } from "@prisma/client"; + +export interface IPermissionsRepository { + create(data: ICreatePermissionSchema): Promise; + getById(id: string): Promise; + getByRoleAndResource(roleId: string, resourceId: string): Promise; + getByRole(role: string): Promise; + getAll(): Promise; + update(id: string, data: IUpdatePermissionSchema): Promise; + delete(id: string): Promise; + checkPermission(role: string, action: string, resource: string): Promise; +} \ No newline at end of file diff --git a/sigap-website/src/application/repositories/resources.repository.interface.ts b/sigap-website/src/application/repositories/resources.repository.interface.ts new file mode 100644 index 0000000..f784e69 --- /dev/null +++ b/sigap-website/src/application/repositories/resources.repository.interface.ts @@ -0,0 +1,14 @@ +import { ICreateResourceSchema } from "@/src/entities/models/resources/create-resources.model"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { IUpdateResourceSchema } from "@/src/entities/models/resources/update-resources.model"; +import { resources } from "@prisma/client"; + +export interface IResourcesRepository { + create(data: ICreateResourceSchema): Promise; + getById(id: string): Promise; + getByName(name: string): Promise; + getByType(type: string): Promise; + getAll(): Promise; + update(id: string, data: IUpdateResourceSchema): Promise; + delete(id: string): Promise; +} \ No newline at end of file diff --git a/sigap-website/src/application/repositories/roles.repository.interface.ts b/sigap-website/src/application/repositories/roles.repository.interface.ts new file mode 100644 index 0000000..8130b9e --- /dev/null +++ b/sigap-website/src/application/repositories/roles.repository.interface.ts @@ -0,0 +1,13 @@ +import { ICreateRoleSchema } from "@/src/entities/models/roles/create-roles.model"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { IUpdateRoleSchema } from "@/src/entities/models/roles/update-roles.model"; +import { roles } from "@prisma/client"; + +export interface IRolesRepository { + create(data: ICreateRoleSchema): Promise; + getById(id: string): Promise; + getByName(name: string): Promise; + getAll(): Promise; + update(id: string, data: IUpdateRoleSchema): Promise; + delete(id: string): Promise; +} \ No newline at end of file diff --git a/sigap-website/src/application/repositories/users.repository.interface.ts b/sigap-website/src/application/repositories/users.repository.interface.ts index a8e6e52..8aa9e03 100644 --- a/sigap-website/src/application/repositories/users.repository.interface.ts +++ b/sigap-website/src/application/repositories/users.repository.interface.ts @@ -1,6 +1,6 @@ import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createClient } from "@/app/_utils/supabase/client"; -import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model"; +import { IUserSchema, IUserSupabaseSchema, UserResponse } from "@/src/entities/models/users/users.model"; import { ITransaction } from "@/src/entities/models/transaction.interface"; import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"; @@ -16,8 +16,8 @@ export interface IUsersRepository { getUserById(credential: ICredentialGetUserByIdSchema): Promise; getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise; getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise; - createUser(input: ICreateUserSchema, tx?: ITransaction): Promise; - inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise; + createUser(input: ICreateUserSchema, tx?: ITransaction): Promise; + inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise; updateUser(credential: ICredentialUpdateUserSchema, input: Partial, tx?: ITransaction): Promise; deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise; banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise; diff --git a/sigap-website/src/application/services/authentication.service.interface.ts b/sigap-website/src/application/services/authentication.service.interface.ts index 41512d1..8345ac0 100644 --- a/sigap-website/src/application/services/authentication.service.interface.ts +++ b/sigap-website/src/application/services/authentication.service.interface.ts @@ -17,4 +17,5 @@ export interface IAuthenticationService { sendMagicLink(credentials: ISendMagicLinkSchema): Promise sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise verifyOtp(credentials: IVerifyOtpSchema): Promise + checkPermission(userId: string, action: string, resource: string): Promise; } \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/auth/check-permissions.use-case.ts b/sigap-website/src/application/use-cases/auth/check-permissions.use-case.ts new file mode 100644 index 0000000..395c59b --- /dev/null +++ b/sigap-website/src/application/use-cases/auth/check-permissions.use-case.ts @@ -0,0 +1,24 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { NotFoundError } from "@/src/entities/errors/common"; +import { IAuthenticationService } from "../../services/authentication.service.interface"; +import { IUsersRepository } from "../../repositories/users.repository.interface"; + +export type ICheckPermissionsUseCase = ReturnType; + +export const checkPermissionsUseCase = ( + instrumentationService: IInstrumentationService, + authenticationService: IAuthenticationService, +) => async (userId: string, action: string, resource: string): Promise => { + return await instrumentationService.startSpan({ name: "Check Permission Use Case", op: "function" }, + async () => { + + const permission = await authenticationService.checkPermission(userId, action, resource); + + if (!permission) { + throw new NotFoundError("Permission not found"); + } + + return permission; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/permissions/create-permissions.use-case.ts b/sigap-website/src/application/use-cases/permissions/create-permissions.use-case.ts new file mode 100644 index 0000000..76bc595 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/create-permissions.use-case.ts @@ -0,0 +1,28 @@ +import { ICreatePermissionSchema } from "@/src/entities/models/permissions/create-permission.model" +import { IInstrumentationService } from "../../services/instrumentation.service.interface" +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface" +import { permissions } from "@prisma/client" +import { AlreadyExistsError } from "@/src/entities/errors/common" +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model" + +export type ICreatePermissionUseCase = ReturnType + +export const createPermissionUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (input: ICreatePermissionSchema): Promise => { + return await instrumentationService.startSpan({ name: "Create Permission Use Case", op: "function" }, + async () => { + + const existingPermission = await permissionsRepository.getByRoleAndResource(input.role_id, input.resource_id) + + if (existingPermission) { + throw new AlreadyExistsError("Permission already exists") + } + + const permission = await permissionsRepository.create(input) + + return permission + } + ) +} diff --git a/sigap-website/src/application/use-cases/permissions/delete-permissions.use-case.ts b/sigap-website/src/application/use-cases/permissions/delete-permissions.use-case.ts new file mode 100644 index 0000000..ebf9c89 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/delete-permissions.use-case.ts @@ -0,0 +1,27 @@ +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { NotFoundError } from "@/src/entities/errors/common"; +import { IDeletePermissionsSchema } from "@/src/entities/models/permissions/delete-permissions.model"; + +export type IDeletePermissionUseCase = ReturnType; + +export const deletePermissionUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (input: IDeletePermissionsSchema): Promise => { + return await instrumentationService.startSpan({ name: "Delete Permissions Use Case", op: "function" }, + async () => { + + const permissions = await permissionsRepository.getById(input.id); + + if (!permissions) { + throw new NotFoundError("Permissions not found"); + } + + const deletedPermissions = await permissionsRepository.delete(input.id); + + return deletedPermissions; + } + ); +} \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/permissions/get-all-permissions.ts b/sigap-website/src/application/use-cases/permissions/get-all-permissions.ts new file mode 100644 index 0000000..78c7181 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/get-all-permissions.ts @@ -0,0 +1,20 @@ +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; + + +export type IGetAllPermissionsUseCase = ReturnType; + +export const getAllPermissionsUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (): Promise => { + return await instrumentationService.startSpan({ name: "Get All Permissions Use Case", op: "function" }, + async () => { + + const permissions = await permissionsRepository.getAll() + + return permissions + } + ) +} diff --git a/sigap-website/src/application/use-cases/permissions/get-permissions-by-id.use-case.ts b/sigap-website/src/application/use-cases/permissions/get-permissions-by-id.use-case.ts new file mode 100644 index 0000000..8a26f4c --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/get-permissions-by-id.use-case.ts @@ -0,0 +1,24 @@ +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetPermissionByIdUseCase = ReturnType; + +export const getPermissionByIdUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (id: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Permissions By Id Use Case", op: "function" }, + async () => { + + const permissions = await permissionsRepository.getById(id) + + if (!permissions) { + throw new NotFoundError("Permissions not found") + } + + return permissions + } + ) +} \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/permissions/get-permissions-by-role-and-resources.use-case.ts b/sigap-website/src/application/use-cases/permissions/get-permissions-by-role-and-resources.use-case.ts new file mode 100644 index 0000000..c4465e1 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/get-permissions-by-role-and-resources.use-case.ts @@ -0,0 +1,24 @@ +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetPermissionByRoleAndResourcesUseCase = ReturnType; + +export const getPermissionByRoleAndResourcesUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (roleId: string, resourceId: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Permissions By Role And Resources Use Case", op: "function" }, + async () => { + + const permissions = await permissionsRepository.getByRoleAndResource(roleId, resourceId) + + if (!permissions) { + throw new NotFoundError("Permissions not found") + } + + return permissions + } + ) +} \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/permissions/get-permissions-by-role.use-case.ts b/sigap-website/src/application/use-cases/permissions/get-permissions-by-role.use-case.ts new file mode 100644 index 0000000..f0c3290 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/get-permissions-by-role.use-case.ts @@ -0,0 +1,24 @@ +import { NotFoundError } from "@/src/entities/errors/common"; +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; + +export type IGetPermissionByRoleUseCase = ReturnType; + +export const getPermissionByRoleUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (roleId: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Permissions By Role Use Case", op: "function" }, + async () => { + + const permissions = await permissionsRepository.getByRole(roleId) + + if (!permissions) { + throw new NotFoundError("Permissions not found") + } + + return permissions + } + ) +} \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/permissions/update-permissions.use-case.ts b/sigap-website/src/application/use-cases/permissions/update-permissions.use-case.ts new file mode 100644 index 0000000..4975969 --- /dev/null +++ b/sigap-website/src/application/use-cases/permissions/update-permissions.use-case.ts @@ -0,0 +1,28 @@ +import { IUpdatePermissionSchema } from "@/src/entities/models/permissions/update-permission.model"; +import { IPermissionsRepository } from "../../repositories/permissions.repository.interface"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IPermissionsSchema } from "@/src/entities/models/permissions/permissions.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + + +export type IUpdatePermissionUseCase = ReturnType; + +export const updatePermissionUseCase = ( + instrumentationService: IInstrumentationService, + permissionsRepository: IPermissionsRepository, +) => async (id: string, input: IUpdatePermissionSchema): Promise => { + return await instrumentationService.startSpan({ name: "Update Permission Use Case", op: "function" }, + async () => { + + const permission = await permissionsRepository.getById(id) + + if (!permission) { + throw new NotFoundError("Permission not found") + } + + const updatedPermission = await permissionsRepository.update(id, input) + + return updatedPermission + } + ) +} \ No newline at end of file diff --git a/sigap-website/src/application/use-cases/resources/create-resources.use-case.ts b/sigap-website/src/application/use-cases/resources/create-resources.use-case.ts new file mode 100644 index 0000000..2bd1579 --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/create-resources.use-case.ts @@ -0,0 +1,27 @@ +import { ICreateResourceSchema } from "@/src/entities/models/resources/create-resources.model"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { AlreadyExistsError } from "@/src/entities/errors/common"; + +export type ICreateResourceUseCase = ReturnType; + +export const createResourceUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (input: ICreateResourceSchema): Promise => { + return await instrumentationService.startSpan({ name: "Create Resource Use Case", op: "function" }, + async () => { + + const existingResource = await resourcesRepository.getByName(input.name); + + if (existingResource) { + throw new AlreadyExistsError("Resource already exists"); + } + + const resource = await resourcesRepository.create(input); + + return resource; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/resources/delete-resource.use-case.ts b/sigap-website/src/application/use-cases/resources/delete-resource.use-case.ts new file mode 100644 index 0000000..568af87 --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/delete-resource.use-case.ts @@ -0,0 +1,26 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IDeleteResourceUseCase = ReturnType; + +export const deleteResourceUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (id: string): Promise => { + return await instrumentationService.startSpan({ name: "Delete Resource Use Case", op: "function" }, + async () => { + + const existingResource = await resourcesRepository.getById(id); + + if (!existingResource) { + throw new NotFoundError("Resource not found"); + } + + const deletedResource = await resourcesRepository.delete(id); + + return deletedResource; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/resources/get-resource-by-id.use-case.ts b/sigap-website/src/application/use-cases/resources/get-resource-by-id.use-case.ts new file mode 100644 index 0000000..d2e3fee --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/get-resource-by-id.use-case.ts @@ -0,0 +1,24 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetResourceByIdUseCase = ReturnType; + +export const getResourceByIdUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (id: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Resource By ID Use Case", op: "function" }, + async () => { + + const resource = await resourcesRepository.getById(id); + + if (!resource) { + throw new NotFoundError("Resource not found"); + } + + return resource; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/resources/get-resource-by-name.use-case.ts b/sigap-website/src/application/use-cases/resources/get-resource-by-name.use-case.ts new file mode 100644 index 0000000..0213e0a --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/get-resource-by-name.use-case.ts @@ -0,0 +1,24 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetResourceByNameUseCase = ReturnType; + +export const getResourceByNameUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (name: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Resource By Name Use Case", op: "function" }, + async () => { + + const resource = await resourcesRepository.getByName(name); + + if (!resource) { + throw new NotFoundError("Resource not found"); + } + + return resource; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/resources/get-resources-by-type.use-case.ts b/sigap-website/src/application/use-cases/resources/get-resources-by-type.use-case.ts new file mode 100644 index 0000000..517153d --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/get-resources-by-type.use-case.ts @@ -0,0 +1,19 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; + +export type IGetResourcesByTypeUseCase = ReturnType; + +export const getResourcesByTypeUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (type: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Resources By Type Use Case", op: "function" }, + async () => { + + const resources = await resourcesRepository.getByType(type); + + return resources || []; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/resources/update-resource.use-case.ts b/sigap-website/src/application/use-cases/resources/update-resource.use-case.ts new file mode 100644 index 0000000..207a95f --- /dev/null +++ b/sigap-website/src/application/use-cases/resources/update-resource.use-case.ts @@ -0,0 +1,27 @@ +import { IUpdateResourceSchema } from "@/src/entities/models/resources/update-resources.model"; +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IResourcesRepository } from "../../repositories/resources.repository.interface"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IUpdateResourceUseCase = ReturnType; + +export const updateResourceUseCase = ( + instrumentationService: IInstrumentationService, + resourcesRepository: IResourcesRepository, +) => async (id: string, data: IUpdateResourceSchema): Promise => { + return await instrumentationService.startSpan({ name: "Update Resource Use Case", op: "function" }, + async () => { + + const existingResource = await resourcesRepository.getById(id); + + if (!existingResource) { + throw new NotFoundError("Resource not found"); + } + + const updatedResource = await resourcesRepository.update(id, data); + + return updatedResource; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/roles/create-role.use-case.ts b/sigap-website/src/application/use-cases/roles/create-role.use-case.ts new file mode 100644 index 0000000..25f64cf --- /dev/null +++ b/sigap-website/src/application/use-cases/roles/create-role.use-case.ts @@ -0,0 +1,28 @@ + +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IRolesRepository } from "../../repositories/roles.repository.interface"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { AlreadyExistsError } from "@/src/entities/errors/common"; +import { ICreateRoleSchema } from "@/src/entities/models/roles/create-roles.model"; + +export type ICreateRoleUseCase = ReturnType; + +export const createRoleUseCase = ( + instrumentationService: IInstrumentationService, + rolesRepository: IRolesRepository, +) => async (input: ICreateRoleSchema): Promise => { + return await instrumentationService.startSpan({ name: "Create Role Use Case", op: "function" }, + async () => { + + const existingRole = await rolesRepository.getByName(input.name); + + if (existingRole) { + throw new AlreadyExistsError("Role already exists"); + } + + const role = await rolesRepository.create(input); + + return role; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/roles/delete-role.use-case.ts b/sigap-website/src/application/use-cases/roles/delete-role.use-case.ts new file mode 100644 index 0000000..35a61ce --- /dev/null +++ b/sigap-website/src/application/use-cases/roles/delete-role.use-case.ts @@ -0,0 +1,26 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IRolesRepository } from "../../repositories/roles.repository.interface"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IDeleteRoleUseCase = ReturnType; + +export const deleteRoleUseCase = ( + instrumentationService: IInstrumentationService, + rolesRepository: IRolesRepository, +) => async (id: string): Promise => { + return await instrumentationService.startSpan({ name: "Delete Role Use Case", op: "function" }, + async () => { + + const existingRole = await rolesRepository.getById(id); + + if (!existingRole) { + throw new NotFoundError("Role not found"); + } + + const deletedRole = await rolesRepository.delete(id); + + return deletedRole; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/roles/get-role-by-id.use-case.ts b/sigap-website/src/application/use-cases/roles/get-role-by-id.use-case.ts new file mode 100644 index 0000000..ce10cc5 --- /dev/null +++ b/sigap-website/src/application/use-cases/roles/get-role-by-id.use-case.ts @@ -0,0 +1,24 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IRolesRepository } from "../../repositories/roles.repository.interface"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetRoleByIdUseCase = ReturnType; + +export const getRoleByIdUseCase = ( + instrumentationService: IInstrumentationService, + rolesRepository: IRolesRepository, +) => async (id: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Role By ID Use Case", op: "function" }, + async () => { + + const role = await rolesRepository.getById(id); + + if (!role) { + throw new NotFoundError("Role not found"); + } + + return role; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/roles/get-role-by-name.use-case.ts b/sigap-website/src/application/use-cases/roles/get-role-by-name.use-case.ts new file mode 100644 index 0000000..a039edf --- /dev/null +++ b/sigap-website/src/application/use-cases/roles/get-role-by-name.use-case.ts @@ -0,0 +1,24 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IRolesRepository } from "../../repositories/roles.repository.interface"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { NotFoundError } from "@/src/entities/errors/common"; + +export type IGetRoleByNameUseCase = ReturnType; + +export const getRoleByNameUseCase = ( + instrumentationService: IInstrumentationService, + rolesRepository: IRolesRepository, +) => async (name: string): Promise => { + return await instrumentationService.startSpan({ name: "Get Role By Name Use Case", op: "function" }, + async () => { + + const role = await rolesRepository.getByName(name); + + if (!role) { + throw new NotFoundError("Role not found"); + } + + return role; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/roles/update-role.use-case.ts b/sigap-website/src/application/use-cases/roles/update-role.use-case.ts new file mode 100644 index 0000000..a135ff0 --- /dev/null +++ b/sigap-website/src/application/use-cases/roles/update-role.use-case.ts @@ -0,0 +1,27 @@ +import { IInstrumentationService } from "../../services/instrumentation.service.interface"; +import { IRolesRepository } from "../../repositories/roles.repository.interface"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { NotFoundError } from "@/src/entities/errors/common"; +import { IUpdateRoleSchema } from "@/src/entities/models/roles/update-roles.model"; + +export type IUpdateRoleUseCase = ReturnType; + +export const updateRoleUseCase = ( + instrumentationService: IInstrumentationService, + rolesRepository: IRolesRepository, +) => async (id: string, data: IUpdateRoleSchema): Promise => { + return await instrumentationService.startSpan({ name: "Update Role Use Case", op: "function" }, + async () => { + + const existingRole = await rolesRepository.getById(id); + + if (!existingRole) { + throw new NotFoundError("Role not found"); + } + + const updatedRole = await rolesRepository.update(id, data); + + return updatedRole; + } + ); +}; diff --git a/sigap-website/src/application/use-cases/users/create-user.use-case.ts b/sigap-website/src/application/use-cases/users/create-user.use-case.ts index 2d7f57a..e33c205 100644 --- a/sigap-website/src/application/use-cases/users/create-user.use-case.ts +++ b/sigap-website/src/application/use-cases/users/create-user.use-case.ts @@ -2,7 +2,7 @@ import { AuthenticationError } from "@/src/entities/errors/auth" import { IUsersRepository } from "../../repositories/users.repository.interface" import { IAuthenticationService } from "../../services/authentication.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface" -import { IUserSchema } from "@/src/entities/models/users/users.model" +import { IUserSchema, IUserSupabaseSchema } from "@/src/entities/models/users/users.model" import { InputParseError } from "@/src/entities/errors/common" import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model" @@ -12,7 +12,7 @@ export type ICreateUserUseCase = ReturnType export const createUserUseCase = ( instrumentationService: IInstrumentationService, usersRepository: IUsersRepository, -) => async (input: ICreateUserSchema): Promise => { +) => async (input: ICreateUserSchema): Promise => { return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" }, async () => { diff --git a/sigap-website/src/application/use-cases/users/invite-user.use-case.ts b/sigap-website/src/application/use-cases/users/invite-user.use-case.ts index 0b1b7d9..4c5ae1c 100644 --- a/sigap-website/src/application/use-cases/users/invite-user.use-case.ts +++ b/sigap-website/src/application/use-cases/users/invite-user.use-case.ts @@ -2,7 +2,7 @@ import { AuthenticationError } from "@/src/entities/errors/auth" import { IUsersRepository } from "../../repositories/users.repository.interface" import { IAuthenticationService } from "../../services/authentication.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface" -import { IUserSchema } from "@/src/entities/models/users/users.model" +import { IUserSchema, IUserSupabaseSchema } from "@/src/entities/models/users/users.model" import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model" @@ -11,7 +11,7 @@ export type IInviteUserUseCase = ReturnType export const inviteUserUseCase = ( instrumentationService: IInstrumentationService, usersRepository: IUsersRepository, -) => async (credential: ICredentialsInviteUserSchema): Promise => { +) => async (credential: ICredentialsInviteUserSchema): Promise => { return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" }, async () => { diff --git a/sigap-website/src/entities/errors/common.ts b/sigap-website/src/entities/errors/common.ts index 799eb86..f07ac1e 100644 --- a/sigap-website/src/entities/errors/common.ts +++ b/sigap-website/src/entities/errors/common.ts @@ -18,6 +18,12 @@ export class InputParseError extends Error { } } +export class AlreadyExistsError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +} + export class ServerActionError extends Error { code: string; diff --git a/sigap-website/src/entities/models/auth/session.model.ts b/sigap-website/src/entities/models/auth/session.model.ts index 0640b16..bebd641 100644 --- a/sigap-website/src/entities/models/auth/session.model.ts +++ b/sigap-website/src/entities/models/auth/session.model.ts @@ -5,7 +5,7 @@ export const SessionSchema = z.object({ user: UserSchema.pick({ id: true, email: true, - role: true, + roles_id: true, }), expiresAt: z.number().optional(), }); diff --git a/sigap-website/src/entities/models/permissions/create-permission.model.ts b/sigap-website/src/entities/models/permissions/create-permission.model.ts new file mode 100644 index 0000000..4a4e03a --- /dev/null +++ b/sigap-website/src/entities/models/permissions/create-permission.model.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export interface CreatePermissionDto { + action: string; + resource_id: string; + role_id: string; +} + +export const CreatePermissionSchema = z.object({ + action: z.string().min(1, { message: "Action is required" }), + resource_id: z.string().min(1, { message: "Resource ID is required" }), + role_id: z.string().min(1, { message: "Role ID is required" }), +}) + +export type ICreatePermissionSchema = z.infer; + +export const defaultICreatePermissionSchemaValues: ICreatePermissionSchema = { + action: "", + resource_id: "", + role_id: "", +} diff --git a/sigap-website/src/entities/models/permissions/delete-permissions.model.ts b/sigap-website/src/entities/models/permissions/delete-permissions.model.ts new file mode 100644 index 0000000..6fa1581 --- /dev/null +++ b/sigap-website/src/entities/models/permissions/delete-permissions.model.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const DeletePermissionsSchema = z.object({ + id: z.string().uuid(), +}) + +export type IDeletePermissionsSchema = z.infer + +export const defaultIDeletePermissionsSchemaValues: IDeletePermissionsSchema = { + id: "", +} + diff --git a/sigap-website/src/entities/models/permissions/permissions.model.ts b/sigap-website/src/entities/models/permissions/permissions.model.ts new file mode 100644 index 0000000..819bf7c --- /dev/null +++ b/sigap-website/src/entities/models/permissions/permissions.model.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +type permissions = { + id: string; + action: string; + role_id: string; + resource_id: string; + created_at: Date; + updated_at: Date; +} + +export const PermissionsSchema = z.object({ + id: z.string().uuid(), + action: z.string().min(1).max(255), + role_id: z.string().uuid(), + resource_id: z.string().uuid(), + created_at: z.date(), + updated_at: z.date() +}) + +export type IPermissionsSchema = z.infer; + diff --git a/sigap-website/src/entities/models/permissions/update-permission.model.ts b/sigap-website/src/entities/models/permissions/update-permission.model.ts new file mode 100644 index 0000000..f768d25 --- /dev/null +++ b/sigap-website/src/entities/models/permissions/update-permission.model.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const UpdatePermissionSchema = z.object({ + action: z.string().min(1, { message: "Action is required" }), + resource_id: z.string().min(1, { message: "Resource ID is required" }), + role_id: z.string().min(1, { message: "Role ID is required" }), +}) + +export type IUpdatePermissionSchema = z.infer; + +export const defaultIUpdatePermissionSchemaValues: IUpdatePermissionSchema = { + action: "", + resource_id: "", + role_id: "", +} \ No newline at end of file diff --git a/sigap-website/src/entities/models/resources/create-resources.model.ts b/sigap-website/src/entities/models/resources/create-resources.model.ts new file mode 100644 index 0000000..c55536b --- /dev/null +++ b/sigap-website/src/entities/models/resources/create-resources.model.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export interface CreateResourceDto { + name: string; + type?: string; + description?: string; + instance_role?: string; + relations?: string; + attributes?: Record; +} + +export const CreateResourceSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + type: z.string().optional(), + description: z.string().optional(), + instance_role: z.string().optional(), + relations: z.string().optional(), + attributes: z.record(z.any()).optional(), +}) + +export type ICreateResourceSchema = z.infer; + +export const defaultICreateResourceSchemaValues: ICreateResourceSchema = { + name: "", + type: undefined, + description: undefined, + instance_role: undefined, + relations: undefined, + attributes: {}, +} \ No newline at end of file diff --git a/sigap-website/src/entities/models/resources/resources.model.ts b/sigap-website/src/entities/models/resources/resources.model.ts new file mode 100644 index 0000000..5802c85 --- /dev/null +++ b/sigap-website/src/entities/models/resources/resources.model.ts @@ -0,0 +1,28 @@ +import { JsonValue } from "@prisma/client/runtime/library"; +import { z } from "zod"; + +type resources = { + id: string; + name: string; + type: string | null; + description: string | null; + instance_role: string | null; + relations: string | null; + attributes: JsonValue | null; + created_at: Date; + updated_at: Date; +} + +export const ResourcesSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1).max(255), + type: z.string().nullable(), + description: z.string().nullable(), + instance_role: z.string().nullable(), + relations: z.string().nullable(), + attributes: z.any().nullable(), + created_at: z.date(), + updated_at: z.date(), +}) + +export type IResourcesSchema = z.infer; \ No newline at end of file diff --git a/sigap-website/src/entities/models/resources/update-resources.model.ts b/sigap-website/src/entities/models/resources/update-resources.model.ts new file mode 100644 index 0000000..bd70d79 --- /dev/null +++ b/sigap-website/src/entities/models/resources/update-resources.model.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export interface UpdateResourceDto { + name?: string; + type?: string; + description?: string; + instance_role?: string; + relations?: string; + attributes?: Record; +} + +export const UpdateResourceSchema = z.object({ + name: z.string().optional(), + type: z.string().optional(), + description: z.string().optional(), + instance_role: z.string().optional(), + relations: z.string().optional(), + attributes: z.record(z.any()).optional(), +}) + +export type IUpdateResourceSchema = z.infer; + +export const defaultIUpdateResourceSchemaValues: IUpdateResourceSchema = { + name: undefined, + type: undefined, + description: undefined, + instance_role: undefined, + relations: undefined, + attributes: {}, +} \ No newline at end of file diff --git a/sigap-website/src/entities/models/roles/create-roles.model.ts b/sigap-website/src/entities/models/roles/create-roles.model.ts new file mode 100644 index 0000000..5075b8f --- /dev/null +++ b/sigap-website/src/entities/models/roles/create-roles.model.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export interface CreateRoleDto { + name: string; + description?: string; +} + +export const CreateRoleSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + description: z.string().optional(), +}) + +export type ICreateRoleSchema = z.infer; + +export const defaultICreateRoleSchemaValues: ICreateRoleSchema = { + name: "", + description: undefined, +} \ No newline at end of file diff --git a/sigap-website/src/entities/models/roles/roles.model.ts b/sigap-website/src/entities/models/roles/roles.model.ts new file mode 100644 index 0000000..90d81d2 --- /dev/null +++ b/sigap-website/src/entities/models/roles/roles.model.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const RoleSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + created_at: z.union([z.string(), z.date()]).nullable().optional(), + updated_at: z.union([z.string(), z.date()]).nullable().optional(), +}) + +export type IRolesSchema = z.infer; \ No newline at end of file diff --git a/sigap-website/src/entities/models/roles/update-roles.model.ts b/sigap-website/src/entities/models/roles/update-roles.model.ts new file mode 100644 index 0000000..bd9f91b --- /dev/null +++ b/sigap-website/src/entities/models/roles/update-roles.model.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export interface UpdateRoleDto { + name?: string; + description?: string; +} + +export const UpdateRoleSchema = z.object({ + name: z.string().optional(), + description: z.string().optional(), +}) + +export type IUpdateRoleSchema = z.infer; + +export const defaultIUpdateRoleSchemaValues: IUpdateRoleSchema = { + name: undefined, + description: undefined, +} \ No newline at end of file diff --git a/sigap-website/src/entities/models/users/update-user.model.ts b/sigap-website/src/entities/models/users/update-user.model.ts index 3010edd..125440b 100644 --- a/sigap-website/src/entities/models/users/update-user.model.ts +++ b/sigap-website/src/entities/models/users/update-user.model.ts @@ -4,7 +4,7 @@ export const UpdateUserSchema = z.object({ email: z.string().email().optional(), email_confirmed_at: z.boolean().optional(), encrypted_password: z.string().optional(), - role: z.enum(["user", "staff", "admin"]).optional(), + roles_id: z.string().optional(), // Sesuai dengan model di Prisma phone: z.string().optional(), phone_confirmed_at: z.boolean().optional(), invited_at: z.union([z.string(), z.date()]).optional(), @@ -34,7 +34,7 @@ export const defaultValueUpdateUserSchema: IUpdateUserSchema = { email: undefined, email_confirmed_at: undefined, encrypted_password: undefined, - role: "user", + roles_id: undefined, phone: undefined, phone_confirmed_at: undefined, invited_at: undefined, diff --git a/sigap-website/src/entities/models/users/users.model.ts b/sigap-website/src/entities/models/users/users.model.ts index c6cc234..e409efd 100644 --- a/sigap-website/src/entities/models/users/users.model.ts +++ b/sigap-website/src/entities/models/users/users.model.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { AuthError } from "@supabase/supabase-js"; +import { RoleSchema } from "../roles/roles.model"; const timestampSchema = z.union([z.string(), z.date()]).nullable(); @@ -37,7 +38,7 @@ const timestampSchema = z.union([z.string(), z.date()]).nullable(); export const UserSchema = z.object({ id: z.string(), - role: z.string().optional(), + roles_id: z.string().optional(), // Sesuaikan dengan field di Prisma email: z.string().email().optional(), email_confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), encrypted_password: z.string().nullable().optional(), @@ -64,10 +65,39 @@ export const UserSchema = z.object({ }) .nullable() .optional(), + role: RoleSchema.optional(), }); export type IUserSchema = z.infer; +export const UserSupabaseSchema = z.object({ + id: z.string(), + app_metadata: z.any().optional(), + user_metadata: z.any().optional(), + aud: z.string(), + confirmation_sent_at: z.union([z.string(), z.date()]).nullable().optional(), + recovery_sent_at: z.union([z.string(), z.date()]).nullable().optional(), + email_change_sent_at: z.union([z.string(), z.date()]).nullable().optional(), + new_email: z.string().nullable().optional(), + new_phone: z.string().nullable().optional(), + invited_at: z.union([z.string(), z.date()]).nullable().optional(), + action_link: z.string().nullable().optional(), + email: z.string().email().nullable().optional(), + phone: z.string().nullable().optional(), + created_at: z.union([z.string(), z.date()]), + confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), + email_confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), + phone_confirmed_at: z.union([z.string(), z.date()]).nullable().optional(), + last_sign_in_at: z.union([z.string(), z.date()]).nullable().optional(), + role: z.string().nullable().optional(), + updated_at: z.union([z.string(), z.date()]).nullable().optional(), + identities: z.array(z.any()).nullable().optional(), + is_anonymous: z.boolean().optional(), + factors: z.array(z.any()).nullable().optional(), +}) + +export type IUserSupabaseSchema = z.infer; + export const ProfileSchema = z.object({ id: z.string(), user_id: z.string(), diff --git a/sigap-website/src/infrastructure/repositories/permissions.repository.ts b/sigap-website/src/infrastructure/repositories/permissions.repository.ts new file mode 100644 index 0000000..cc84023 --- /dev/null +++ b/sigap-website/src/infrastructure/repositories/permissions.repository.ts @@ -0,0 +1,86 @@ +import db from '@/prisma/db'; +import { IPermissionsRepository } from '@/src/application/repositories/permissions.repository.interface'; +import { ICrashReporterService } from '@/src/application/services/crash-reporter.service.interface'; +import { IInstrumentationService } from '@/src/application/services/instrumentation.service.interface'; +import { ICreatePermissionSchema } from '@/src/entities/models/permissions/create-permission.model'; +import { IPermissionsSchema } from '@/src/entities/models/permissions/permissions.model'; +import { IUpdatePermissionSchema } from '@/src/entities/models/permissions/update-permission.model'; +import { PrismaClient, permissions } from '@prisma/client'; + +export class PermissionsRepository implements IPermissionsRepository { + constructor( + private readonly instrumentationService: IInstrumentationService, + private readonly crashReporterService: ICrashReporterService, + ) { } + + async create(data: ICreatePermissionSchema): Promise { + return db.permissions.create({ + data: { + action: data.action, + resource_id: data.resource_id, + role_id: data.role_id + } + }); + } + + async getById(id: string): Promise { + return db.permissions.findUnique({ + where: { id } + }); + } + + async getByRoleAndResource(roleId: string, resourceId: string): Promise { + return db.permissions.findMany({ + where: { + role_id: roleId, + resource_id: resourceId + } + }); + } + + async getByRole(roleId: string): Promise { + return db.permissions.findMany({ + where: { + role_id: roleId + }, + include: { + resource: true + } + }); + } + + async getAll(): Promise { + return db.permissions.findMany(); + } + + async update(id: string, data: IUpdatePermissionSchema): Promise { + return db.permissions.update({ + where: { id }, + data: { + action: data.action + } + }); + } + + async delete(id: string): Promise { + return db.permissions.delete({ + where: { id } + }); + } + + async checkPermission(role: string, action: string, resource: string): Promise { + const result = await db.permissions.findFirst({ + where: { + role: { + name: role + }, + action: action, + resource: { + name: resource + } + } + }); + + return !!result; + } +} \ No newline at end of file diff --git a/sigap-website/src/infrastructure/repositories/resources.repository.ts b/sigap-website/src/infrastructure/repositories/resources.repository.ts new file mode 100644 index 0000000..cf44152 --- /dev/null +++ b/sigap-website/src/infrastructure/repositories/resources.repository.ts @@ -0,0 +1,134 @@ +import db from "@/prisma/db"; +import { IResourcesRepository } from "@/src/application/repositories/resources.repository.interface"; +import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICreateResourceSchema } from "@/src/entities/models/resources/create-resources.model"; +import { IResourcesSchema } from "@/src/entities/models/resources/resources.model"; +import { resources } from "@prisma/client"; + +export class ResourcesRepository implements IResourcesRepository { + constructor( + private readonly instrumentationService: IInstrumentationService, + private readonly crashReporterService: ICrashReporterService, + ) { } + + async getById(id: string): Promise { + return this.instrumentationService.startSpan( + { name: "Find Resource By ID", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.findUnique({ + where: { id } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async getByName(name: string): Promise { + return this.instrumentationService.startSpan( + { name: "Find Resource By Name", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.findUnique({ + where: { name } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async getByType(type: string): Promise { + return this.instrumentationService.startSpan( + { name: "Find Resources By Type", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.findMany({ + where: { type } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async getAll(): Promise { + return this.instrumentationService.startSpan( + { name: "Find All Resources", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.findMany(); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async create(data: ICreateResourceSchema): Promise { + return this.instrumentationService.startSpan( + { name: "Create Resource", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.create({ + data: { + name: data.name, + description: data.description, + type: data.type, + } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async update(id: string, data: ICreateResourceSchema): Promise { + return this.instrumentationService.startSpan( + { name: "Update Resource", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.update({ + where: { id }, + data: { + name: data.name, + description: data.description, + type: data.type, + } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async delete(id: string): Promise { + return this.instrumentationService.startSpan( + { name: "Delete Resource", op: "function", attributes: {} }, + async () => { + try { + return await db.resources.delete({ + where: { id } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + +} \ No newline at end of file diff --git a/sigap-website/src/infrastructure/repositories/roles.repository.ts b/sigap-website/src/infrastructure/repositories/roles.repository.ts new file mode 100644 index 0000000..8ba5306 --- /dev/null +++ b/sigap-website/src/infrastructure/repositories/roles.repository.ts @@ -0,0 +1,115 @@ +import db from "@/prisma/db"; +import { IRolesRepository } from "@/src/application/repositories/roles.repository.interface"; +import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICreateRoleSchema } from "@/src/entities/models/roles/create-roles.model"; +import { IRolesSchema } from "@/src/entities/models/roles/roles.model"; +import { roles } from "@prisma/client"; + +export class RolesRepository implements IRolesRepository { + constructor( + private readonly instrumentationService: IInstrumentationService, + private readonly crashReporterService: ICrashReporterService, + ) { } + + async getById(id: string): Promise { + return this.instrumentationService.startSpan( + { name: "Find Role By ID", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.findUnique({ + where: { id } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async getByName(name: string): Promise { + return this.instrumentationService.startSpan( + { name: "Find Role By Name", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.findUnique({ + where: { name } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async getAll(): Promise { + return this.instrumentationService.startSpan( + { name: "Find All Roles", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.findMany(); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async create(data: ICreateRoleSchema): Promise { + return this.instrumentationService.startSpan( + { name: "Create Role", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.create({ + data: { + name: data.name, + description: data.description, + } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async update(id: string, data: ICreateRoleSchema): Promise { + return this.instrumentationService.startSpan( + { name: "Update Role", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.update({ + where: { id }, + data: { + name: data.name, + description: data.description, + } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } + + async delete(id: string): Promise { + return this.instrumentationService.startSpan( + { name: "Delete Role", op: "function", attributes: {} }, + async () => { + try { + return await db.roles.delete({ + where: { id } + }); + } catch (error) { + this.crashReporterService.report(error); + throw error; + } + } + ) + } +} \ No newline at end of file diff --git a/sigap-website/src/infrastructure/repositories/users.repository.ts b/sigap-website/src/infrastructure/repositories/users.repository.ts index 934f18c..75c8190 100644 --- a/sigap-website/src/infrastructure/repositories/users.repository.ts +++ b/sigap-website/src/infrastructure/repositories/users.repository.ts @@ -3,7 +3,7 @@ import { ICrashReporterService } from "@/src/application/services/crash-reporter import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createClient as createServerClient } from "@/app/_utils/supabase/server"; -import { IUserSchema } from "@/src/entities/models/users/users.model"; +import { IUserSchema, IUserSupabaseSchema } from "@/src/entities/models/users/users.model"; import { ITransaction } from "@/src/entities/models/transaction.interface"; import db from "@/prisma/db"; import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common"; @@ -15,6 +15,7 @@ import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"; import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"; import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"; +import { User } from "@supabase/supabase-js"; export class UsersRepository implements IUsersRepository { constructor( @@ -33,6 +34,7 @@ export class UsersRepository implements IUsersRepository { const query = db.users.findMany({ include: { profile: true, + role: true, }, }); @@ -69,6 +71,7 @@ export class UsersRepository implements IUsersRepository { }, include: { profile: true, + role: true, }, }) @@ -88,7 +91,6 @@ export class UsersRepository implements IUsersRepository { return { ...user, - id: credential.id, }; } catch (err) { this.crashReporterService.report(err); @@ -110,6 +112,7 @@ export class UsersRepository implements IUsersRepository { }, include: { profile: true, + role: true, }, }) @@ -149,6 +152,7 @@ export class UsersRepository implements IUsersRepository { }, include: { profile: true, + role: true, }, }) @@ -207,6 +211,7 @@ export class UsersRepository implements IUsersRepository { }, include: { profile: true, + role: true, }, }); @@ -215,11 +220,7 @@ export class UsersRepository implements IUsersRepository { } return { - ...user, - profile: { - user_id: userDetail.id, - ...userDetail.profile, - }, + ...userDetail }; } catch (err) { this.crashReporterService.report(err); @@ -228,7 +229,7 @@ export class UsersRepository implements IUsersRepository { }) } - async createUser(input: ICreateUserSchema, tx?: ITransaction): Promise { + async createUser(input: ICreateUserSchema, tx?: ITransaction): Promise { return await this.instrumentationService.startSpan({ name: "UsersRepository > createUser", }, async () => { @@ -241,10 +242,11 @@ export class UsersRepository implements IUsersRepository { const query = supabase.auth.admin.createUser({ email: input.email, password: input.password, - email_confirm: true, + email_confirm: input.email_confirm ?? true, + phone: input.phone, }) - const { data: { user } } = await this.instrumentationService.startSpan({ + const { data: { user }, error } = await this.instrumentationService.startSpan({ name: "UsersRepository > createUser > supabase.auth.admin.createUser", op: "db:query", attributes: { "system": "supabase.auth" }, @@ -254,7 +256,7 @@ export class UsersRepository implements IUsersRepository { } ) - if (!user) { + if (error || !user) { throw new DatabaseOperationError("Failed to create user"); } @@ -267,7 +269,7 @@ export class UsersRepository implements IUsersRepository { }) } - async inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise { + async inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise { return await this.instrumentationService.startSpan({ name: "UsersRepository > inviteUser", }, async () => { @@ -335,6 +337,7 @@ export class UsersRepository implements IUsersRepository { id: credential.id, }, include: { + role: true, profile: true, }, }) @@ -357,8 +360,12 @@ export class UsersRepository implements IUsersRepository { where: { id: credential.id, }, + include: { + profile: true, + role: true, + }, data: { - role: input.role || user.role, + roles_id: input.roles_id || user.roles_id, invited_at: input.invited_at || user.invited_at, confirmed_at: input.confirmed_at || user.confirmed_at, last_sign_in_at: input.last_sign_in_at || user.last_sign_in_at, @@ -377,9 +384,6 @@ export class UsersRepository implements IUsersRepository { }, }, }, - include: { - profile: true, - }, }) const updatedUser = await this.instrumentationService.startSpan({ diff --git a/sigap-website/src/infrastructure/services/authentication.service.ts b/sigap-website/src/infrastructure/services/authentication.service.ts index 90d4811..a7817b4 100644 --- a/sigap-website/src/infrastructure/services/authentication.service.ts +++ b/sigap-website/src/infrastructure/services/authentication.service.ts @@ -1,6 +1,7 @@ import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createClient } from "@/app/_utils/supabase/server"; import db from "@/prisma/db"; +import { IPermissionsRepository } from "@/src/application/repositories/permissions.repository.interface"; import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"; import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"; import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; @@ -19,6 +20,7 @@ export class AuthenticationService implements IAuthenticationService { private readonly usersRepository: IUsersRepository, private readonly instrumentationService: IInstrumentationService, private readonly crashReporterService: ICrashReporterService, + private readonly permissionRepository: IPermissionsRepository, private readonly supabaseAdmin = createAdminClient(), private readonly supabaseServer = createClient() ) { } @@ -274,4 +276,30 @@ export class AuthenticationService implements IAuthenticationService { } }) } + + async checkPermission(userId: string, action: string, resource: string): Promise { + return await this.instrumentationService.startSpan({ + name: "checkPermission Use Case", + }, async () => { + try { + + const user = await db.users.findUnique({ + where: { id: userId }, + include: { role: true } + }); + + if (!user) { + return false; + } + + const role = user.role.name; + + return await this.permissionRepository.checkPermission(role, action, resource); + + } catch (err) { + this.crashReporterService.report(err) + throw err + } + }) + } } \ No newline at end of file diff --git a/sigap-website/src/interface-adapters/controllers/auth/check-permissions.controller.ts b/sigap-website/src/interface-adapters/controllers/auth/check-permissions.controller.ts new file mode 100644 index 0000000..00cb9a1 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/auth/check-permissions.controller.ts @@ -0,0 +1,36 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICheckPermissionsUseCase } from "@/src/application/use-cases/auth/check-permissions.use-case"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + + +const checkPermissionInputSchema = z.object({ + userId: z.string().uuid("Please enter a valid user ID"), + action: z.string().nonempty("Please enter an action"), + resource: z.string().nonempty("Please enter a resource"), +}) + +export type ICheckPermissionsController = ReturnType + +export const checkPermissionsController = + ( + instrumentationService: IInstrumentationService, + checkPermissionUseCase: ICheckPermissionsUseCase + ) => + async (input: Partial>) => { + return await instrumentationService.startSpan({ name: "checkPermission Controller" }, + async () => { + const { data, error: inputParseError } = checkPermissionInputSchema.safeParse(input) + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }) + } + + return await checkPermissionUseCase( + data.userId, + data.action, + data.resource + ) + } + ) + } \ No newline at end of file diff --git a/sigap-website/src/interface-adapters/controllers/permissions/create-permission.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/create-permission.controller.ts new file mode 100644 index 0000000..0ab5fad --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/create-permission.controller.ts @@ -0,0 +1,38 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICreatePermissionUseCase } from "@/src/application/use-cases/permissions/create-permissions.use-case"; + +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { CreatePermissionSchema } from "@/src/entities/models/permissions/create-permission.model"; +import { z } from "zod"; + +const inputSchema = z.object({ + action: z.string().min(1, { message: "Action is required" }), + resource_id: z.string().min(1, { message: "Resource ID is required" }), + role_id: z.string().min(1, { message: "Role ID is required" }), +}) + +export type ICreatePermissionController = ReturnType; + +export const createPermissionController = ( + instrumentationService: IInstrumentationService, + createPermissionUseCase: ICreatePermissionUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: Partial>) => { + return await instrumentationService.startSpan({ name: "Create Permission Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to create a permission"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError('Invalid data', { cause: inputParseError }); + } + + return await createPermissionUseCase(data); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/delete-permission.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/delete-permission.controller.ts new file mode 100644 index 0000000..c01d0c6 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/delete-permission.controller.ts @@ -0,0 +1,23 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IDeletePermissionUseCase } from "@/src/application/use-cases/permissions/delete-permissions.use-case"; + +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; + +export type IDeletePermissionController = ReturnType; + +export const deletePermissionController = ( + instrumentationService: IInstrumentationService, + deletePermissionUseCase: IDeletePermissionUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string) => { + return await instrumentationService.startSpan({ name: "Delete Permission Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to delete a permission"); + } + + return await deletePermissionUseCase({ id }); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/get-all-permission.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/get-all-permission.controller.ts new file mode 100644 index 0000000..8c54fe3 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/get-all-permission.controller.ts @@ -0,0 +1,23 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetAllPermissionsUseCase } from "@/src/application/use-cases/permissions/get-all-permissions"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; + + +export type IGetAllPermissionsController = ReturnType; + +export const getAllPermissionsController = ( + instrumentationService: IInstrumentationService, + getAllPermissionsUseCase: IGetAllPermissionsUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async () => { + return await instrumentationService.startSpan({ name: "getAllPermissions Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to fetch permissions"); + } + + return await getAllPermissionsUseCase(); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-id.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-id.controller.ts new file mode 100644 index 0000000..8684f2d --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-id.controller.ts @@ -0,0 +1,34 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetPermissionByIdUseCase } from "@/src/application/use-cases/permissions/get-permissions-by-id.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + id: z.string().min(1, { message: "Permission ID is required" }), +}); + +export type IGetPermissionByIdController = ReturnType; + +export const getPermissionByIdController = ( + instrumentationService: IInstrumentationService, + getPermissionByIdUseCase: IGetPermissionByIdUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: z.infer) => { + return await instrumentationService.startSpan({ name: "Get Permission By Id Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to view a permission"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await getPermissionByIdUseCase(data.id); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role-and-resource.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role-and-resource.controller.ts new file mode 100644 index 0000000..a8b8ca7 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role-and-resource.controller.ts @@ -0,0 +1,35 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetPermissionByRoleAndResourcesUseCase } from "@/src/application/use-cases/permissions/get-permissions-by-role-and-resources.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + roleId: z.string(), + resourceId: z.string(), +}) + +export type IGetPermissionByRoleAndResourceController = ReturnType; + +export const getPermissionByRoleAndResourceController = ( + instrumentationService: IInstrumentationService, + getPermissionByRoleAndResourceUseCase: IGetPermissionByRoleAndResourcesUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: z.infer) => { + return await instrumentationService.startSpan({ name: "Get Permissions Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to view permissions"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await getPermissionByRoleAndResourceUseCase(data.roleId, data.resourceId); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role.controller.ts new file mode 100644 index 0000000..b219974 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/get-permission-by-role.controller.ts @@ -0,0 +1,23 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetPermissionByRoleUseCase } from "@/src/application/use-cases/permissions/get-permissions-by-role.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; + + +export type IGetPermissionByRoleController = ReturnType; + +export const getPermissionByRoleController = ( + instrumentationService: IInstrumentationService, + getPermissionByRoleUseCase: IGetPermissionByRoleUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (roleId: string) => { + return await instrumentationService.startSpan({ name: "Get Permission By Role Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to fetch permissions"); + } + + return await getPermissionByRoleUseCase(roleId); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/permissions/update-permission.controller.ts b/sigap-website/src/interface-adapters/controllers/permissions/update-permission.controller.ts new file mode 100644 index 0000000..e191cc8 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/permissions/update-permission.controller.ts @@ -0,0 +1,37 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IUpdatePermissionUseCase } from "@/src/application/use-cases/permissions/update-permissions.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { UpdatePermissionSchema } from "@/src/entities/models/permissions/update-permission.model"; +import { z } from "zod"; + +const inputSchema = z.object({ + action: z.string().min(1, { message: "Action is required" }), + resource_id: z.string().min(1, { message: "Resource ID is required" }), + role_id: z.string().min(1, { message: "Role ID is required" }), +}); + +export type IUpdatePermissionController = ReturnType; + +export const updatePermissionController = ( + instrumentationService: IInstrumentationService, + updatePermissionUseCase: IUpdatePermissionUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string, input: Partial>) => { + return await instrumentationService.startSpan({ name: "Update Permission Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to update a permission"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await updatePermissionUseCase(id, data); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/resources/create-resource.controller.ts b/sigap-website/src/interface-adapters/controllers/resources/create-resource.controller.ts new file mode 100644 index 0000000..a8903fa --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/resources/create-resource.controller.ts @@ -0,0 +1,36 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICreateResourceUseCase } from "@/src/application/use-cases/resources/create-resources.use-case"; + +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + name: z.string().min(1, { message: "Resource name is required" }), + description: z.string().optional(), +}); + +export type ICreateResourceController = ReturnType; + +export const createResourceController = ( + instrumentationService: IInstrumentationService, + createResourceUseCase: ICreateResourceUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: Partial>) => { + return await instrumentationService.startSpan({ name: "createResource Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to create a resource"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await createResourceUseCase(data); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/resources/delete-resource.controller.ts b/sigap-website/src/interface-adapters/controllers/resources/delete-resource.controller.ts new file mode 100644 index 0000000..54cbd7a --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/resources/delete-resource.controller.ts @@ -0,0 +1,22 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IDeleteResourceUseCase } from "@/src/application/use-cases/resources/delete-resource.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; + +export type IDeleteResourceController = ReturnType; + +export const deleteResourceController = ( + instrumentationService: IInstrumentationService, + deleteResourceUseCase: IDeleteResourceUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string) => { + return await instrumentationService.startSpan({ name: "deleteResource Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to delete a resource"); + } + + return await deleteResourceUseCase(id); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/resources/get-resource-by-id.controller.ts b/sigap-website/src/interface-adapters/controllers/resources/get-resource-by-id.controller.ts new file mode 100644 index 0000000..6cdd65b --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/resources/get-resource-by-id.controller.ts @@ -0,0 +1,34 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetResourceByIdUseCase } from "@/src/application/use-cases/resources/get-resource-by-id.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + id: z.string().min(1, { message: "Resource ID is required" }), +}); + +export type IGetResourceByIdController = ReturnType; + +export const getResourceByIdController = ( + instrumentationService: IInstrumentationService, + getResourceById: IGetResourceByIdUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: z.infer) => { + return await instrumentationService.startSpan({ name: "getResourceById Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to fetch a resource"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await getResourceById(data.id); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/resources/get-resources-by-type.controller.ts b/sigap-website/src/interface-adapters/controllers/resources/get-resources-by-type.controller.ts new file mode 100644 index 0000000..3a8c9d5 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/resources/get-resources-by-type.controller.ts @@ -0,0 +1,33 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetResourcesByTypeUseCase } from "@/src/application/use-cases/resources/get-resources-by-type.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { z } from "zod"; + +const inputSchema = z.object({ + type: z.string().nonempty("Type is required"), +}) + +export type IGetResourcesByTypeController = ReturnType; + +export const getResourcesByTypeController = ( + instrumentationService: IInstrumentationService, + getResourcesByTypeUseCase: IGetResourcesByTypeUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: z.infer) => { + return await instrumentationService.startSpan({ name: "getResourcesByType Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to fetch resources"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new Error("Invalid data", { cause: inputParseError }); + } + + return await getResourcesByTypeUseCase(data.type); + }); +}; \ No newline at end of file diff --git a/sigap-website/src/interface-adapters/controllers/resources/update-resource.controller.ts b/sigap-website/src/interface-adapters/controllers/resources/update-resource.controller.ts new file mode 100644 index 0000000..6da7e30 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/resources/update-resource.controller.ts @@ -0,0 +1,35 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IUpdateResourceUseCase } from "@/src/application/use-cases/resources/update-resource.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + name: z.string().min(1, { message: "Resource name is required" }), + description: z.string().optional(), +}); + +export type IUpdateResourceController = ReturnType; + +export const updateResourceController = ( + instrumentationService: IInstrumentationService, + updateResourceUseCase: IUpdateResourceUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string, input: Partial>) => { + return await instrumentationService.startSpan({ name: "updateResource Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to update a resource"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await updateResourceUseCase(id, data); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/roles/create-role.controller.ts b/sigap-website/src/interface-adapters/controllers/roles/create-role.controller.ts new file mode 100644 index 0000000..13d2d2a --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/roles/create-role.controller.ts @@ -0,0 +1,36 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICreateRoleUseCase } from "@/src/application/use-cases/roles/create-role.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; + +import { z } from "zod"; + +const inputSchema = z.object({ + name: z.string().min(1, { message: "Role name is required" }), + description: z.string().optional(), +}); + +export type ICreateRoleController = ReturnType; + +export const createRoleController = ( + instrumentationService: IInstrumentationService, + createRoleUseCase: ICreateRoleUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: Partial>) => { + return await instrumentationService.startSpan({ name: "createRole Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to create a role"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await createRoleUseCase(data); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/roles/delete-role.controller.ts b/sigap-website/src/interface-adapters/controllers/roles/delete-role.controller.ts new file mode 100644 index 0000000..51593e3 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/roles/delete-role.controller.ts @@ -0,0 +1,22 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IDeleteRoleUseCase } from "@/src/application/use-cases/roles/delete-role.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; + +export type IDeleteRoleController = ReturnType; + +export const deleteRoleController = ( + instrumentationService: IInstrumentationService, + deleteRoleUseCase: IDeleteRoleUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string) => { + return await instrumentationService.startSpan({ name: "deleteRole Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to delete a role"); + } + + return await deleteRoleUseCase(id); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/roles/get-role-by-id.controller.ts b/sigap-website/src/interface-adapters/controllers/roles/get-role-by-id.controller.ts new file mode 100644 index 0000000..ed9de59 --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/roles/get-role-by-id.controller.ts @@ -0,0 +1,36 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IGetRoleByIdUseCase } from "@/src/application/use-cases/roles/get-role-by-id.use-case"; + + +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; +import { z } from "zod"; + +const inputSchema = z.object({ + id: z.string() +}) + +export type IGetRoleByIdController = ReturnType; + +export const getRoleByIdController = ( + instrumentationService: IInstrumentationService, + getRoleById: IGetRoleByIdUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (input: Partial>) => { + return await instrumentationService.startSpan({ name: "getRole Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to fetch a role"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await getRoleById(data.id); + }); +}; diff --git a/sigap-website/src/interface-adapters/controllers/roles/update-role.controller.ts b/sigap-website/src/interface-adapters/controllers/roles/update-role.controller.ts new file mode 100644 index 0000000..279865b --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/roles/update-role.controller.ts @@ -0,0 +1,36 @@ +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { IUpdateRoleUseCase } from "@/src/application/use-cases/roles/update-role.use-case"; +import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"; +import { UnauthenticatedError } from "@/src/entities/errors/auth"; +import { InputParseError } from "@/src/entities/errors/common"; + +import { z } from "zod"; + +const inputSchema = z.object({ + name: z.string().min(1, { message: "Role name is required" }), + description: z.string().optional(), +}); + +export type IUpdateRoleController = ReturnType; + +export const updateRoleController = ( + instrumentationService: IInstrumentationService, + updateRoleUseCase: IUpdateRoleUseCase, + getCurrentUserUseCase: IGetCurrentUserUseCase +) => async (id: string, input: Partial>) => { + return await instrumentationService.startSpan({ name: "updateRole Controller" }, async () => { + const session = await getCurrentUserUseCase(); + + if (!session) { + throw new UnauthenticatedError("Must be logged in to update a role"); + } + + const { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await updateRoleUseCase(id, data); + }); +};