Added perission check and permission management
This commit is contained in:
parent
152d444067
commit
7800fb471c
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import DashboardError from "@/features/dashboard/errors/DashboardError";
|
||||
import getAllPermissions from "@/features/dashboard/permissions/actions/getAllPermissions";
|
||||
import getRoleById from "@/features/dashboard/roles/actions/getRoleById";
|
||||
import upsertRole from "@/features/dashboard/roles/actions/upsertRole";
|
||||
import roleFormDataSchema, {
|
||||
|
|
@ -20,11 +21,13 @@ import {
|
|||
Skeleton,
|
||||
Fieldset,
|
||||
Alert,
|
||||
Chip,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TbDeviceFloppy } from "react-icons/tb";
|
||||
import { string } from "zod";
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
|
|
@ -45,6 +48,9 @@ export default function FormModal(props: ModalProps) {
|
|||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [allPermissions, setAllPermissions] = useState<
|
||||
{ code: string; name: string }[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const form = useForm<RoleFormData>({
|
||||
initialValues: {
|
||||
|
|
@ -53,6 +59,7 @@ export default function FormModal(props: ModalProps) {
|
|||
id: "",
|
||||
isActive: false,
|
||||
name: "",
|
||||
permissions: [],
|
||||
},
|
||||
validate: zodResolver(roleFormDataSchema),
|
||||
validateInputOnChange: false,
|
||||
|
|
@ -61,6 +68,29 @@ export default function FormModal(props: ModalProps) {
|
|||
},
|
||||
});
|
||||
|
||||
//Fetch Permissions
|
||||
useEffect(() => {
|
||||
setFetching(true);
|
||||
withErrorHandling(getAllPermissions)
|
||||
.then((response) => {
|
||||
setAllPermissions(response.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof DashboardError) {
|
||||
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
|
||||
} else if (e instanceof Error) {
|
||||
setErrorMessage(`ERROR: ${e.message}`);
|
||||
} else {
|
||||
setErrorMessage(
|
||||
`Unkown error is occured. Please contact administrator`
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setFetching(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fetches role data by ID and populates the form if the modal is opened and an ID is provided.
|
||||
*/
|
||||
|
|
@ -70,22 +100,30 @@ export default function FormModal(props: ModalProps) {
|
|||
}
|
||||
|
||||
setFetching(true);
|
||||
getRoleById(props.id)
|
||||
withErrorHandling(getRoleById, props.id)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
const data = response.data;
|
||||
form.setValues({
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
id: data.id,
|
||||
isActive: data.isActive,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
const data = response.data;
|
||||
form.setValues({
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
id: data.id,
|
||||
isActive: data.isActive,
|
||||
name: data.name,
|
||||
permissions: data.permissions.map(
|
||||
(permission) => permission.code
|
||||
),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
//TODO: Handle error
|
||||
console.log(e);
|
||||
if (e instanceof DashboardError) {
|
||||
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
|
||||
} else if (e instanceof Error) {
|
||||
setErrorMessage(`ERROR: ${e.message}`);
|
||||
} else {
|
||||
setErrorMessage(
|
||||
`Unkown error is occured. Please contact administrator`
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setFetching(false);
|
||||
|
|
@ -94,11 +132,12 @@ export default function FormModal(props: ModalProps) {
|
|||
|
||||
const closeModal = () => {
|
||||
props.onClose ? props.onClose() : router.replace("?");
|
||||
form.reset();
|
||||
};
|
||||
|
||||
const handleSubmit = (values: RoleFormData) => {
|
||||
setSubmitting(true);
|
||||
withErrorHandling(() => upsertRole(values))
|
||||
withErrorHandling(upsertRole, values)
|
||||
.then((response) => {
|
||||
showNotification(response.message!, "success");
|
||||
closeModal();
|
||||
|
|
@ -187,6 +226,30 @@ export default function FormModal(props: ModalProps) {
|
|||
/>
|
||||
</Skeleton>
|
||||
|
||||
<Fieldset legend="Permissions">
|
||||
<Chip.Group
|
||||
multiple
|
||||
value={form.values.permissions}
|
||||
onChange={(values) =>
|
||||
!props.readonly &&
|
||||
form.setFieldValue("permissions", values)
|
||||
}
|
||||
>
|
||||
<Flex wrap="wrap" gap="md">
|
||||
{allPermissions?.map((permission) => (
|
||||
<div key={permission.code}>
|
||||
<Chip
|
||||
disabled={isSubmitting}
|
||||
value={permission.code}
|
||||
>
|
||||
{permission.code}
|
||||
</Chip>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</Chip.Group>
|
||||
</Fieldset>
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import prisma from "@/db";
|
|||
import AuthError, { AuthErrorCode } from "./AuthError";
|
||||
import authConfig from "@/config/auth";
|
||||
import UserClaims from "./types/UserClaims";
|
||||
import { cache } from "react";
|
||||
import BaseError from "@/BaseError";
|
||||
|
||||
/**
|
||||
* Hashes a plain text password using bcrypt.
|
||||
|
|
@ -64,22 +66,31 @@ export function decodeJwtToken(token: string): JwtPayload | string {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getUserFromToken(token: string) {
|
||||
const decodedToken = decodeJwtToken(token) as {
|
||||
id: string;
|
||||
iat: number;
|
||||
};
|
||||
/**
|
||||
* Retrieves user data from the database based on the provided JWT token.
|
||||
*
|
||||
* This function decodes the JWT token to extract the user ID, and then queries the database using Prisma
|
||||
* to fetch the user's details, including the profile photo, roles, and direct permissions.
|
||||
*
|
||||
* @param token - The JWT token containing the user's ID.
|
||||
* @returns The user's data if the user exists, or null if no user is found.
|
||||
* Throws an error if the token is invalid or the database query fails.
|
||||
*/
|
||||
export const getUserFromToken = cache(async (token: string) => {
|
||||
// Decode the JWT token to extract the user ID
|
||||
const decodedToken = decodeJwtToken(token) as { id: string; iat: number; };
|
||||
|
||||
// Fetch the user from the database
|
||||
const user = await prisma.user.findFirst({
|
||||
include:{
|
||||
photoProfile: true,
|
||||
include: {
|
||||
photoProfile: true,
|
||||
roles: true,
|
||||
directPermissions: true
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: decodedToken.id,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,15 +3,26 @@ import "server-only"
|
|||
import { getUserFromToken } from "../authUtils"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
const getCurrentUser = cache(async () => {
|
||||
/**
|
||||
* Retrieves the current user based on the JWT token stored in cookies.
|
||||
* This function is intended to run on the server side in a Next.js application.
|
||||
* It reads the JWT token from the cookies, decodes it to get the user ID,
|
||||
* and then fetches the corresponding user data from the database.
|
||||
*
|
||||
* @returns The current user's data if the user is authenticated and found in the database, otherwise null.
|
||||
*/
|
||||
const getCurrentUser = async () => {
|
||||
// Retrieve the token from cookies
|
||||
const token = cookies().get("token")?.value;
|
||||
|
||||
// If no token is found, return null (no current user)
|
||||
if(!token) return null;
|
||||
|
||||
// Use the token to get the user from the database
|
||||
const user = await getUserFromToken(token);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return user;
|
||||
})
|
||||
// Return the user if found, otherwise return null
|
||||
return user ? user : null;
|
||||
}
|
||||
|
||||
export default getCurrentUser;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// Use TypeScript enum for error codes to provide better autocompletion and error handling
|
||||
export const DashboardErrorCodes = [
|
||||
"UNAUTHORIZED",
|
||||
"NOT_FOUND",
|
||||
|
|
@ -10,22 +11,26 @@ export const DashboardErrorCodes = [
|
|||
interface ErrorOptions {
|
||||
message?: string,
|
||||
errorCode?: typeof DashboardErrorCodes[number] | string & {},
|
||||
formErrors?: {[k: string]: string}
|
||||
formErrors?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class for handling errors specific to the dashboard application.
|
||||
*/
|
||||
export default class DashboardError extends Error {
|
||||
public readonly errorCode: typeof DashboardErrorCodes[number] | string & {};
|
||||
public readonly formErrors?: {[k: string]: string}
|
||||
// public readonly data: object;
|
||||
public readonly formErrors?: Record<string, string>
|
||||
|
||||
constructor(options: ErrorOptions) {
|
||||
super(options.message ?? "Undetermined Error"); // Pass message to the Error parent class
|
||||
this.errorCode = options.errorCode ?? "UNKNOWN_ERROR";
|
||||
this.formErrors = options.formErrors;
|
||||
// this.data = data;
|
||||
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a structured error response object.
|
||||
*/
|
||||
getErrorReponseObject(){
|
||||
return {
|
||||
success: false,
|
||||
|
|
@ -35,10 +40,14 @@ export default class DashboardError extends Error {
|
|||
errorCode: this.errorCode,
|
||||
errors: this.formErrors ?? undefined
|
||||
}
|
||||
} as const
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions and converts them into a structured error response.
|
||||
* @param e The caught error or exception.
|
||||
*/
|
||||
export const handleCatch = (e: unknown) => {
|
||||
if (e instanceof DashboardError){
|
||||
return e.getErrorReponseObject()
|
||||
|
|
@ -65,9 +74,23 @@ export const handleCatch = (e: unknown) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a 'UNAUTHORIZED' DashboardError.
|
||||
*/
|
||||
export const unauthorized = () => {
|
||||
throw new DashboardError({
|
||||
errorCode: "UNAUTHORIZED",
|
||||
message: "You are unauthorized to do this action"
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a 'NOT_FOUND' DashboardError with a custom or default message.
|
||||
* @param message Optional custom message for the error.
|
||||
*/
|
||||
export const notFound = ({message}: {message?: string}) => {
|
||||
throw new DashboardError({
|
||||
errorCode: "NOT_FOUND",
|
||||
message: message ?? "The requested data could not be located. It may have been deleted or relocated. Please verify the information or try a different request."
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"use server"
|
||||
|
||||
import checkPermission from "@/features/auth/tools/checkPermission"
|
||||
import { handleCatch, notFound, unauthorized } from "../../errors/DashboardError"
|
||||
import ServerResponse from "@/types/Action";
|
||||
|
||||
type PermissionData = {
|
||||
code: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active permissions from the database if the user has the 'permissions.readAll' permission.
|
||||
*
|
||||
* @returns A structured server response containing the list of permissions or an error message.
|
||||
*/
|
||||
export default async function getAllPermissions(): Promise<ServerResponse<PermissionData[]>>{
|
||||
try {
|
||||
// Check if the user has the required permission
|
||||
if (!await checkPermission("permissions.readAll")) return unauthorized()
|
||||
|
||||
// Fetch active permissions from the database
|
||||
const permissions = await prisma?.permission.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
}
|
||||
});
|
||||
|
||||
// If no permissions are found, throw a custom 'not found' error
|
||||
if (!permissions || permissions.length === 0) {
|
||||
return notFound({ message: "No active permissions found." });
|
||||
}
|
||||
|
||||
// Return the permissions in a structured server response
|
||||
return {
|
||||
success: true,
|
||||
message: "Permissions fetched",
|
||||
data: permissions
|
||||
}
|
||||
} catch (e){
|
||||
return handleCatch(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import "server-only";
|
|||
*/
|
||||
export default async function getPermissions() {
|
||||
// Authorization check
|
||||
if (!(await checkPermission("permissions.getAll"))) {
|
||||
if (!(await checkPermission("permissions.readAll"))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +1,56 @@
|
|||
"use server";
|
||||
|
||||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import { handleCatch, notFound, unauthorized } from "../../errors/DashboardError";
|
||||
import ServerResponse from "@/types/Action";
|
||||
|
||||
export default async function getRoleById(id: string) {
|
||||
if (!(await checkPermission("role.read"))) unauthorized();
|
||||
type RoleData = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
permissions: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
code: true,
|
||||
description: true,
|
||||
id: true,
|
||||
isActive: true,
|
||||
name: true,
|
||||
permissions: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
export default async function getRoleById(id: string): Promise<ServerResponse<RoleData>>{
|
||||
try{
|
||||
|
||||
if (!(await checkPermission("role.read"))) return unauthorized();
|
||||
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
code: true,
|
||||
description: true,
|
||||
id: true,
|
||||
isActive: true,
|
||||
name: true,
|
||||
permissions: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new Error("Permission not found")
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Role not found",
|
||||
} as const;
|
||||
success: true,
|
||||
message: "Role fetched successfully",
|
||||
data: role,
|
||||
};
|
||||
} catch (e){
|
||||
return handleCatch(e)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Role fetched successfully",
|
||||
data: role,
|
||||
} as const;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
|||
import prisma from "@/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import DashboardError, { handleCatch, unauthorized } from "../../errors/DashboardError";
|
||||
import DashboardError, {
|
||||
handleCatch,
|
||||
unauthorized,
|
||||
} from "../../errors/DashboardError";
|
||||
|
||||
/**
|
||||
* Upserts a role based on the provided RoleFormData.
|
||||
|
|
@ -32,9 +35,11 @@ export default async function upsertRole(
|
|||
const validatedFields = roleFormDataSchema.safeParse(data);
|
||||
if (!validatedFields.success) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
||||
})
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(
|
||||
validatedFields.error.flatten().fieldErrors
|
||||
),
|
||||
});
|
||||
}
|
||||
const roleData = {
|
||||
code: validatedFields.data.code,
|
||||
|
|
@ -43,25 +48,38 @@ export default async function upsertRole(
|
|||
isActive: validatedFields.data.isActive,
|
||||
};
|
||||
|
||||
const permissionIds = validatedFields.data.permissions.map(
|
||||
(permission) => ({ code: permission })
|
||||
);
|
||||
|
||||
// Database operation
|
||||
if (isInsert) {
|
||||
if (await prisma.role.findFirst({
|
||||
where: {
|
||||
code: roleData.code
|
||||
}
|
||||
})){
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
code: "The code is already exists"
|
||||
}
|
||||
})
|
||||
}
|
||||
await prisma.role.create({ data: roleData });
|
||||
if (
|
||||
await prisma.role.findFirst({
|
||||
where: {
|
||||
code: roleData.code,
|
||||
},
|
||||
})
|
||||
) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
code: "The code is already exists",
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.role.create({
|
||||
data: {
|
||||
...roleData,
|
||||
permissions: {
|
||||
connect: permissionIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.role.update({
|
||||
where: { id: validatedFields.data.id! },
|
||||
data: roleData,
|
||||
data: { ...roleData, permissions: { connect: permissionIds } },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +94,6 @@ export default async function upsertRole(
|
|||
}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleCatch(error)
|
||||
return handleCatch(error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import "server-only";
|
||||
import { unauthorized } from "@/features/dashboard/errors/DashboardError";
|
||||
|
||||
/**
|
||||
* Retrieves all roles along with the count of associated permissions and users.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface RoleFormData {
|
|||
code: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
const roleFormDataSchema = z.object({
|
||||
|
|
@ -14,6 +15,7 @@ const roleFormDataSchema = z.object({
|
|||
code: z.string().min(1),
|
||||
description: z.string(),
|
||||
isActive: z.boolean(),
|
||||
permissions: z.array(z.string()).optional().default([]),
|
||||
})
|
||||
|
||||
export default roleFormDataSchema;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import ServerResponse from "@/types/Action";
|
||||
import ServerResponse, { SuccessResponse } from "@/types/Action";
|
||||
import DashboardError from "../errors/DashboardError";
|
||||
|
||||
async function withErrorHandling<T extends ServerResponse>(
|
||||
asyncFunction: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const result = await asyncFunction();
|
||||
if (result.success) {
|
||||
/**
|
||||
* A higher-order function that wraps an async function and provides structured error handling.
|
||||
* If the wrapped function returns a successful response, it's returned directly.
|
||||
* If an error occurs, it throws a DashboardError for dashboard-related errors or a generic Error otherwise.
|
||||
*
|
||||
* @param asyncFunction - The async function to wrap.
|
||||
* @param args - The arguments to pass to the async function.
|
||||
* @returns The successful response from the async function.
|
||||
* @throws DashboardError for dashboard-related errors or Error for other errors.
|
||||
*/
|
||||
async function withErrorHandling<T, Args extends unknown[] = []>(
|
||||
asyncFunction: (...args: Args) => Promise<ServerResponse<T>>,
|
||||
...args: Args
|
||||
){
|
||||
const result = await asyncFunction(...args);
|
||||
if (result.success === true) {
|
||||
return result;
|
||||
} else {
|
||||
if (result.dashboardError && result.error) {
|
||||
|
|
|
|||
34
src/types/Action.d.ts
vendored
34
src/types/Action.d.ts
vendored
|
|
@ -1,18 +1,22 @@
|
|||
export type ErrorResponse = {
|
||||
success: false;
|
||||
dashboardError?: boolean;
|
||||
error?: {
|
||||
message?: string;
|
||||
errorCode?: string;
|
||||
errors?: { [k: string]: string };
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type SuccessResponse<T = undefined> = T extends undefined ? {success: true; message?: string} : {
|
||||
success: true;
|
||||
message?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
type ServerResponse<T = undefined> =
|
||||
| {
|
||||
success: false;
|
||||
dashboardError?: boolean;
|
||||
error?: {
|
||||
message?: string;
|
||||
errorCode?: string;
|
||||
errors?: { [k: string]: string };
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
message?: string;
|
||||
data?: T;
|
||||
};
|
||||
| ErrorResponse
|
||||
| SuccessResponse<T>
|
||||
|
||||
export default ServerResponse;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user