refactor: consolidate permission checking logic and update related components

This commit is contained in:
vergiLgood1 2025-04-12 13:56:49 +07:00
parent 31e2fe590a
commit 0c663753f6
14 changed files with 130 additions and 153 deletions

View File

@ -15,10 +15,10 @@ import { IUserSchema } from "@/src/entities/models/users/users.model"
import { useUserActionsHandler } from "../../../_handlers/actions/use-user-actions" import { useUserActionsHandler } from "../../../_handlers/actions/use-user-actions"
import { BanUserDialog } from "../../dialogs/ban-user-dialog" import { BanUserDialog } from "../../dialogs/ban-user-dialog"
import { useCreateUserColumn } from "../../../_handlers/use-create-user-column" import { useCreateUserColumn } from "../../../_handlers/use-create-user-column"
import { useCheckPermissionsHandler } from "@/app/(pages)/(auth)/_handlers/use-check-permissions"
import { useCheckPermissionsNewQuery } from "@/app/(pages)/(auth)/_queries/mutations"
import { useGetCurrentUserQuery } from "../../../_queries/queries" import { useGetCurrentUserQuery } from "../../../_queries/queries"
import { Badge } from "@/app/_components/ui/badge" import { Badge } from "@/app/_components/ui/badge"
import { useCheckPermissionsQuery } from "@/app/(pages)/(auth)/_queries/queries"
interface ActionsCellProps { interface ActionsCellProps {
user: IUserSchema user: IUserSchema
@ -43,12 +43,12 @@ export const ActionsCell: React.FC<ActionsCellProps> = ({ user, onUpdate }) => {
setSelectedUser, setSelectedUser,
} = useCreateUserColumn() } = useCreateUserColumn()
const { data: currentUser, isPending } = useGetCurrentUserQuery() const { data: currentUser } = useGetCurrentUserQuery()
if (!currentUser) return <Badge variant={"destructive"}>user not found</Badge> if (!currentUser) return <Badge variant={"destructive"}>user not found</Badge>
let { data: isAllowedToDelete } = useCheckPermissionsNewQuery(currentUser.email, "delete", "users") let { data: isAllowedToDelete } = useCheckPermissionsQuery(currentUser.email, "delete", "users")
let { data: isAllowedToUpdate } = useCheckPermissionsNewQuery(currentUser.email, "update", "users") let { data: isAllowedToUpdate } = useCheckPermissionsQuery(currentUser.email, "update", "users")
return ( return (
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
@ -76,7 +76,7 @@ export const ActionsCell: React.FC<ActionsCellProps> = ({ user, onUpdate }) => {
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{isAllowedToUpdate && user.banned_until != null && ( {isAllowedToUpdate && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
if (user.banned_until != null) { if (user.banned_until != null) {

View File

@ -1,14 +0,0 @@
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 {
checkPermissions: handleCheckPermissions,
isPending,
}
}

View File

@ -1,5 +1,5 @@
import { useMutation, useQueries, useQuery } from "@tanstack/react-query" import { useMutation, useQueries, useQuery } from "@tanstack/react-query"
import { checkPermissionNew, checkPermissions, sendMagicLink, sendPasswordRecovery, signInPasswordless, signInWithPassword, signOut, verifyOtp } from "../action" import { checkPermissions, sendMagicLink, sendPasswordRecovery, signInPasswordless, signInWithPassword, signOut, verifyOtp } from "../action"
export const useSignInPasswordlessMutation = () => { export const useSignInPasswordlessMutation = () => {
return useMutation({ return useMutation({
@ -42,17 +42,3 @@ export const useVerifyOtpMutation = () => {
mutationFn: async (formData: FormData) => await verifyOtp(formData), mutationFn: async (formData: FormData) => await verifyOtp(formData),
}) })
} }
export const useCheckPermissionsMutation = () => {
return useMutation({
mutationKey: ["check-permissions"],
mutationFn: async ({ userId, action, resource }: { userId: string; action: string; resource: string }) => await checkPermissions(userId, action, resource),
})
}
export const useCheckPermissionsNewQuery = (email: string, action: string, resource: string) => {
return useQuery({
queryKey: ["check-permissions", email, action, resource],
queryFn: async () => await checkPermissionNew(email, action, resource),
})
}

View File

@ -0,0 +1,9 @@
import { useQuery } from "@tanstack/react-query"
import { checkPermissions } from "../action"
export const useCheckPermissionsQuery = (email: string, action: string, resource: string) => {
return useQuery({
queryKey: ["check-permissions", email, action, resource],
queryFn: async () => await checkPermissions(email, action, resource),
})
}

View File

@ -229,82 +229,42 @@ export async function sendPasswordRecovery(email: string) {
}) })
} }
export async function checkPermissions(userId: string, action: string, resource: string) { export async function checkPermissions(email: string, action: string, resource: string) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("checkPermissions", {
recordResponse: true
}, async () => {
try {
const user = await db.users.findUnique({
where: { id: userId },
include: {
role: true
}
})
console.log("Checking permissions for user:", user?.role.name, "action:", action, "resource:", resource)
const permission = await db.permissions.findFirst({
where: {
role_id: user?.role.id,
action: action,
resource: {
name: resource
}
}
})
return !!permission
} 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.",
}
}
})
}
export async function checkPermissionNew(email: string, action: string, resource: string) {
const instrumentationService = getInjection("IInstrumentationService") const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("checkPermissionNew", { return await instrumentationService.instrumentServerAction("checkPermissionNew", {
recordResponse: true recordResponse: true
}, async () => { }, async () => {
try { try {
const user = await db.users.findUnique({ // const user = await db.users.findUnique({
where: { email }, // where: { email },
include: { role: true } // include: { role: true }
}) // })
if (!user) { // if (!user) {
return { error: "User not found" } // return { error: "User not found" }
} // }
console.log("Checking permissions for user:", user?.role.name, "action:", action, "resource:", resource) // console.log("Checking permissions for user:", user?.role.name, "action:", action, "resource:", resource)
const permission = await db.permissions.findFirst({ // const permission = await db.permissions.findFirst({
where: { // where: {
role_id: user.role.id, // role_id: user.role.id,
action: action, // action: action,
resource: { // resource: {
name: resource // name: resource
} // }
} // }
}) // })
if (!permission) { // if (!permission) {
return false // return false
} // }
return !!permission // return !!permission
const checkPermissionsController = getInjection("ICheckPermissionsController")
return await checkPermissionsController({ email, action, resource })
} catch (err) { } catch (err) {
if (err instanceof InputParseError) { if (err instanceof InputParseError) {
@ -315,7 +275,7 @@ export async function checkPermissionNew(email: string, action: string, resource
crashReporterService.report(err) crashReporterService.report(err)
return { return {
error: "An error occurred during permissions check. Please try again later.", error: err instanceof Error ? err.message : "An unknown error occurred",
} }
} }
}) })

View File

@ -17,6 +17,8 @@ import { sendMagicLinkController } from '@/src/interface-adapters/controllers/au
import { sendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller'; import { sendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
import { signInWithPasswordUseCase } from '@/src/application/use-cases/auth/sign-in-with-password.use-case'; import { signInWithPasswordUseCase } from '@/src/application/use-cases/auth/sign-in-with-password.use-case';
import { signInWithPasswordController } from '@/src/interface-adapters/controllers/auth/sign-in-with-password.controller'; import { signInWithPasswordController } from '@/src/interface-adapters/controllers/auth/sign-in-with-password.controller';
import { checkPermissionsUseCase } from '@/src/application/use-cases/auth/check-permissions.use-case';
import { checkPermissionsController } from '@/src/interface-adapters/controllers/auth/check-permissions.controller';
export function createAuthenticationModule() { export function createAuthenticationModule() {
const authenticationModule = createModule(); const authenticationModule = createModule();
@ -28,15 +30,19 @@ export function createAuthenticationModule() {
authenticationModule authenticationModule
.bind(DI_SYMBOLS.IAuthenticationService) .bind(DI_SYMBOLS.IAuthenticationService)
.toClass(AuthenticationService, [ .toClass(AuthenticationService, [
DI_SYMBOLS.IUsersRepository,
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICrashReporterService,
DI_SYMBOLS.IPermissionsRepository,
DI_SYMBOLS.IUsersRepository,
]); ]);
} else { } else {
authenticationModule authenticationModule
.bind(DI_SYMBOLS.IAuthenticationService) .bind(DI_SYMBOLS.IAuthenticationService)
.toClass(AuthenticationService, [ .toClass(AuthenticationService, [
DI_SYMBOLS.IUsersRepository,
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICrashReporterService,
DI_SYMBOLS.IPermissionsRepository,
DI_SYMBOLS.IUsersRepository,
]); ]);
} }
@ -98,7 +104,7 @@ export function createAuthenticationModule() {
authenticationModule authenticationModule
.bind(DI_SYMBOLS.ICheckPermissionsUseCase) .bind(DI_SYMBOLS.ICheckPermissionsUseCase)
.toHigherOrderFunction(signUpUseCase, [ .toHigherOrderFunction(checkPermissionsUseCase, [
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IAuthenticationService, DI_SYMBOLS.IAuthenticationService,
]); ]);
@ -150,9 +156,9 @@ export function createAuthenticationModule() {
authenticationModule authenticationModule
.bind(DI_SYMBOLS.ICheckPermissionsController) .bind(DI_SYMBOLS.ICheckPermissionsController)
.toHigherOrderFunction(signUpUseCase, [ .toHigherOrderFunction(checkPermissionsController, [
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IAuthenticationService, DI_SYMBOLS.ICheckPermissionsUseCase,
DI_SYMBOLS.IUsersRepository, DI_SYMBOLS.IUsersRepository,
]); ]);

View File

@ -13,10 +13,27 @@ import { getPermissionByIdController } from '@/src/interface-adapters/controller
import { getPermissionByRoleController } from '@/src/interface-adapters/controllers/permissions/get-permission-by-role.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 { updatePermissionController } from '@/src/interface-adapters/controllers/permissions/update-permission.controller';
import { deletePermissionController } from '@/src/interface-adapters/controllers/permissions/delete-permission.controller'; import { deletePermissionController } from '@/src/interface-adapters/controllers/permissions/delete-permission.controller';
import { PermissionsRepository } from '@/src/infrastructure/repositories/permissions.repository';
export function createPermissionsModule() { export function createPermissionsModule() {
const permissionsModule = createModule(); const permissionsModule = createModule();
if (process.env.NODE_ENV === 'test') {
permissionsModule
.bind(DI_SYMBOLS.IPermissionsRepository)
.toClass(PermissionsRepository, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICrashReporterService,
]);
} else {
permissionsModule
.bind(DI_SYMBOLS.IPermissionsRepository)
.toClass(PermissionsRepository, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICrashReporterService,
]);
}
// Use Cases // Use Cases
permissionsModule permissionsModule
.bind(DI_SYMBOLS.ICreatePermissionUseCase) .bind(DI_SYMBOLS.ICreatePermissionUseCase)

View File

@ -11,10 +11,23 @@ import { getResourceByIdController } from '@/src/interface-adapters/controllers/
import { getResourcesByTypeController } from '@/src/interface-adapters/controllers/resources/get-resources-by-type.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 { updateResourceController } from '@/src/interface-adapters/controllers/resources/update-resource.controller';
import { deleteResourceController } from '@/src/interface-adapters/controllers/resources/delete-resource.controller'; import { deleteResourceController } from '@/src/interface-adapters/controllers/resources/delete-resource.controller';
import { ResourcesRepository } from '@/src/infrastructure/repositories/resources.repository';
export function createResourcesModule() { export function createResourcesModule() {
const resourcesModule = createModule(); const resourcesModule = createModule();
if (process.env.NODE_ENV === 'test') {
// resourcesModule
// .bind(DI_SYMBOLS.IResourcesRepository)
// .toClass(MockResourcesRepository, [DI_SYMBOLS.IInstrumentationService]);
} else {
resourcesModule
.bind(DI_SYMBOLS.IResourcesRepository)
.toClass(ResourcesRepository, [
DI_SYMBOLS.IInstrumentationService,
]);
}
// Use Cases // Use Cases
resourcesModule resourcesModule
.bind(DI_SYMBOLS.ICreateResourceUseCase) .bind(DI_SYMBOLS.ICreateResourceUseCase)

View File

@ -9,10 +9,23 @@ import { createRoleController } from '@/src/interface-adapters/controllers/roles
import { getRoleByIdController } from '@/src/interface-adapters/controllers/roles/get-role-by-id.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 { updateRoleController } from '@/src/interface-adapters/controllers/roles/update-role.controller';
import { deleteRoleController } from '@/src/interface-adapters/controllers/roles/delete-role.controller'; import { deleteRoleController } from '@/src/interface-adapters/controllers/roles/delete-role.controller';
import { RolesRepository } from '@/src/infrastructure/repositories/roles.repository';
export function createRolesModule() { export function createRolesModule() {
const rolesModule = createModule(); const rolesModule = createModule();
if (process.env.NODE_ENV === 'test') {
// rolesModule
// .bind(DI_SYMBOLS.IRolesRepository)
// .toClass(MockRolesRepository, [DI_SYMBOLS.IInstrumentationService]);
} else {
rolesModule
.bind(DI_SYMBOLS.IRolesRepository)
.toClass(RolesRepository, [
DI_SYMBOLS.IInstrumentationService,
]);
}
// Use Cases // Use Cases
rolesModule rolesModule
.bind(DI_SYMBOLS.ICreateRoleUseCase) .bind(DI_SYMBOLS.ICreateRoleUseCase)

View File

@ -17,5 +17,5 @@ export interface IAuthenticationService {
sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void> sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void>
sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void> sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void>
verifyOtp(credentials: IVerifyOtpSchema): Promise<void> verifyOtp(credentials: IVerifyOtpSchema): Promise<void>
checkPermission(userId: string, action: string, resource: string): Promise<boolean>; checkPermission(email: string, action: string, resource: string): Promise<boolean>;
} }

View File

@ -1,21 +1,19 @@
import { IInstrumentationService } from "../../services/instrumentation.service.interface"; import { IInstrumentationService } from "../../services/instrumentation.service.interface";
import { NotFoundError } from "@/src/entities/errors/common";
import { IAuthenticationService } from "../../services/authentication.service.interface"; import { IAuthenticationService } from "../../services/authentication.service.interface";
import { IUsersRepository } from "../../repositories/users.repository.interface";
export type ICheckPermissionsUseCase = ReturnType<typeof checkPermissionsUseCase>; export type ICheckPermissionsUseCase = ReturnType<typeof checkPermissionsUseCase>;
export const checkPermissionsUseCase = ( export const checkPermissionsUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService, authenticationService: IAuthenticationService,
) => async (userId: string, action: string, resource: string): Promise<boolean> => { ) => async (email: string, action: string, resource: string): Promise<boolean> => {
return await instrumentationService.startSpan({ name: "Check Permission Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "Check Permission Use Case", op: "function" },
async () => { async () => {
const permission = await authenticationService.checkPermission(userId, action, resource); const permission = await authenticationService.checkPermission(email, action, resource);
if (!permission) { if (!permission) {
throw new NotFoundError("Permission not found"); return false;
} }
return permission; return permission;

View File

@ -73,19 +73,22 @@ export class PermissionsRepository implements IPermissionsRepository {
async () => { async () => {
try { try {
const result = await db.permissions.findFirst({ const permission = await db.permissions.findFirst({
where: { where: {
role: { role: { name: role },
name: role action,
}, resource: { name: resource }
action: action,
resource: {
name: resource
} }
} })
});
if (!permission) {
return false
}
console.log("Permission", permission)
return true
return !!result;
} catch (err) { } catch (err) {
this.crashReporterService.report(err); this.crashReporterService.report(err);
throw err; throw err;

View File

@ -17,10 +17,9 @@ import { IUserSchema } from "@/src/entities/models/users/users.model";
export class AuthenticationService implements IAuthenticationService { export class AuthenticationService implements IAuthenticationService {
constructor( constructor(
private readonly usersRepository: IUsersRepository,
private readonly instrumentationService: IInstrumentationService, private readonly instrumentationService: IInstrumentationService,
private readonly crashReporterService: ICrashReporterService, private readonly crashReporterService: ICrashReporterService,
private readonly permissionRepository: IPermissionsRepository, private readonly permissionsRepository: IPermissionsRepository,
private readonly supabaseAdmin = createAdminClient(), private readonly supabaseAdmin = createAdminClient(),
private readonly supabaseServer = createClient() private readonly supabaseServer = createClient()
) { } ) { }
@ -277,35 +276,22 @@ export class AuthenticationService implements IAuthenticationService {
}) })
} }
async checkPermission(userId: string, action: string, resource: string): Promise<boolean> { async checkPermission(email: string, action: string, resource: string): Promise<boolean> {
return await this.instrumentationService.startSpan({ return await this.instrumentationService.startSpan({
name: "", name: "Check Permission Use Case",
}, async () => { }, async () => {
try { try {
const user = await db.users.findUnique({
where: { email },
include: { role: true }
});
// const user = await db.users.findUnique({ if (!user) {
// where: { id: userId }, return false;
// include: { role: true } }
// });
// if (!user) { return await this.permissionsRepository.checkPermission(user.role.name, action, resource);
// return false;
// }
// const role = user.role.name;
// const permission = await db.permissions.findFirst({
// where: {
// role: { name: role },
// action,
// resource: {
// name: resource
// }
// }
// })
return true
} catch (err) { } catch (err) {
this.crashReporterService.report(err) this.crashReporterService.report(err)

View File

@ -6,7 +6,7 @@ import { z } from "zod";
const checkPermissionInputSchema = z.object({ const checkPermissionInputSchema = z.object({
userId: z.string().uuid("Please enter a valid user ID"), email: z.string().email("Please enter a valid email"),
action: z.string().nonempty("Please enter an action"), action: z.string().nonempty("Please enter an action"),
resource: z.string().nonempty("Please enter a resource"), resource: z.string().nonempty("Please enter a resource"),
}) })
@ -23,11 +23,11 @@ export const checkPermissionsController =
return await instrumentationService.startSpan({ name: "checkPermission Controller" }, return await instrumentationService.startSpan({ name: "checkPermission Controller" },
async () => { async () => {
// const session = await usersRpository.getCurrentUser() const session = await usersRpository.getCurrentUser()
// if (!session) { if (!session) {
// throw new InputParseError("User not found") throw new InputParseError("User not found")
// } }
const { data, error: inputParseError } = checkPermissionInputSchema.safeParse(input) const { data, error: inputParseError } = checkPermissionInputSchema.safeParse(input)
@ -36,7 +36,7 @@ export const checkPermissionsController =
} }
return await checkPermissionUseCase( return await checkPermissionUseCase(
data.userId, data.email,
data.action, data.action,
data.resource data.resource
) )