Optimize role
This commit is contained in:
parent
9ba63c2ec9
commit
caab669d51
|
|
@ -1,16 +0,0 @@
|
|||
import BaseError from "@/BaseError";
|
||||
|
||||
export enum AuthErrorCode {
|
||||
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
|
||||
EMPTY_USER_HASH = "EMPTY_USER_HASH",
|
||||
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
||||
INVALID_JWT_TOKEN = "INVALID_JWT_TOKEN",
|
||||
JWT_SECRET_EMPTY = "JWT_SECRET_NOT_EMPTY",
|
||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||
}
|
||||
|
||||
export default class AuthError extends BaseError {
|
||||
constructor(errorCode: AuthErrorCode, {statusCode = 500, message, data}: Partial<{statusCode: number, message: string, data: object}> = {}) {
|
||||
super(message, errorCode, statusCode, data);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
"use server"
|
||||
import { z } from "zod";
|
||||
import prisma from "@/db";
|
||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
||||
import BaseError, { BaseErrorCodes } from "@/BaseError";
|
||||
import { createJwtToken, hashPassword } from "../authUtils";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Interface for the schema of a new user.
|
||||
*/
|
||||
interface CreateUserSchema {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema for creating a user.
|
||||
*/
|
||||
const createUserSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
passwordConfirmation: z.string().optional(),
|
||||
}).refine(
|
||||
(data) => data.password === data.passwordConfirmation,
|
||||
{
|
||||
message: "Password confirmation must match the password",
|
||||
path: ["passwordConfirmation"],
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates a new user in the system.
|
||||
*
|
||||
* @param formData - The form data containing user details.
|
||||
* @returns An object indicating the result of the operation.
|
||||
*/
|
||||
export default async function createUser(formData: FormData){
|
||||
//TODO: Add Throttling
|
||||
//TODO: Add validation check if the user is already logged in
|
||||
|
||||
try {
|
||||
const parsedData = {
|
||||
email: formData.get("email")?.toString() ?? '',
|
||||
name: formData.get("name")?.toString() ?? '',
|
||||
password: formData.get("password")?.toString() ?? '',
|
||||
passwordConfirmation: formData.get("passwordConfirmation")?.toString()
|
||||
};
|
||||
const validatedFields = createUserSchema.safeParse(parsedData);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: "",
|
||||
errors: validatedFields.error.flatten().fieldErrors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: validatedFields.data.email },
|
||||
});
|
||||
|
||||
if (existingUser){
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: "",
|
||||
errors: {
|
||||
email: ["Email already exists"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: validatedFields.data.name,
|
||||
email: validatedFields.data.email,
|
||||
passwordHash: await hashPassword(validatedFields.data.password),
|
||||
},
|
||||
});
|
||||
|
||||
const token = createJwtToken({ id: user.id });
|
||||
cookies().set("token", token);
|
||||
} catch (e: unknown) {
|
||||
// Handle unexpected errors
|
||||
console.error(e)
|
||||
//@ts-ignore
|
||||
console.log(e.message)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: "An unexpected error occurred on the server. Please try again or contact the administrator.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"use server"
|
||||
|
||||
import { cookies } from "next/headers"
|
||||
import "server-only"
|
||||
import { decodeJwtToken, getUserFromToken } from "../authUtils";
|
||||
import prisma from "@/db";
|
||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
||||
import logout from "./logout";
|
||||
|
||||
export default async function getUser(){
|
||||
try {
|
||||
const token = cookies().get('token');
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
const user = await getUserFromToken(token.value);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
name: user.name ?? "",
|
||||
email: user.email ?? "",
|
||||
photoUrl: user.photoProfile?.path ?? null
|
||||
}
|
||||
} catch (e: unknown){
|
||||
if (e instanceof AuthError && e.errorCode === AuthErrorCode.INVALID_JWT_TOKEN){
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"use server"
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import getUser from "./getUser"
|
||||
|
||||
export default async function guestOnly(){
|
||||
|
||||
const user = await getUser();
|
||||
|
||||
if (user){
|
||||
redirect("dashboard")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"use server"
|
||||
|
||||
import { cookies } from "next/headers"
|
||||
import { redirect } from "next/navigation";
|
||||
import "server-only"
|
||||
|
||||
/**
|
||||
* Handles user logout by deleting the authentication token and redirecting to the login page.
|
||||
* This function is intended to be used on the server side.
|
||||
*
|
||||
* @returns A promise that resolves when the logout process is complete.
|
||||
*/
|
||||
export default async function logout(){
|
||||
cookies().delete("token");
|
||||
redirect("/login")
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
"use server";
|
||||
import prisma from "@/db";
|
||||
import { User } from "@prisma/client";
|
||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
||||
import { comparePassword, createJwtToken } from "../authUtils";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import BaseError from "@/BaseError";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
/**
|
||||
* Handles the sign-in process for a user.
|
||||
*
|
||||
* This function validates a user's credentials (email and password), checks against the database,
|
||||
* and on successful validation, redirects the user to the dashboard and sets a cookie with a JWT token.
|
||||
* If the validation fails at any stage, it throws a custom AuthError.
|
||||
*
|
||||
* @param prevState - The previous state of the application, not currently used.
|
||||
* @param rawFormData - The raw form data containing the user's email and password.
|
||||
* @returns A promise that resolves to a redirect to the dashboard on successful authentication,
|
||||
* or an object containing error details on failure.
|
||||
* @throws {AuthError} - Specific authentication error based on the failure stage.
|
||||
*/
|
||||
export default async function signIn(prevState: any, rawFormData: FormData) {
|
||||
//TODO: Add Throttling
|
||||
//TODO: Add validation check if the user is already logged in
|
||||
try {
|
||||
const formData = {
|
||||
email: rawFormData.get("email") as string,
|
||||
password: rawFormData.get("password") as string,
|
||||
};
|
||||
|
||||
// Retrieve user from the database by email
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: formData.email },
|
||||
});
|
||||
|
||||
// Throw if user not found
|
||||
if (!user) throw new AuthError(AuthErrorCode.EMAIL_NOT_FOUND);
|
||||
|
||||
// Throw if user has no password hash
|
||||
// TODO: Add check if the user uses another provider
|
||||
if (!user.passwordHash)
|
||||
throw new AuthError(AuthErrorCode.EMPTY_USER_HASH);
|
||||
|
||||
// Compare the provided password with the user's stored password hash
|
||||
const isMatch = await comparePassword(
|
||||
formData.password,
|
||||
user.passwordHash
|
||||
);
|
||||
if (!isMatch) throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS);
|
||||
|
||||
//Set cookie
|
||||
//TODO: Auth: Add expiry
|
||||
const token = createJwtToken({ id: user.id });
|
||||
|
||||
cookies().set("token", token);
|
||||
} catch (e: unknown) {
|
||||
// Custom error handling for authentication errors
|
||||
if (e instanceof BaseError) {
|
||||
// Specific error handling for known authentication errors
|
||||
switch (e.errorCode) {
|
||||
case AuthErrorCode.EMAIL_NOT_FOUND:
|
||||
case AuthErrorCode.INVALID_CREDENTIALS:
|
||||
return {
|
||||
errors: {
|
||||
message:
|
||||
"Email/Password combination is incorrect. Please try again.",
|
||||
},
|
||||
};
|
||||
default:
|
||||
// Handle other types of authentication errors
|
||||
return {
|
||||
errors: {
|
||||
message: e.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generic error handling for unexpected server errors
|
||||
return {
|
||||
errors: {
|
||||
message:
|
||||
"An unexpected error occurred on the server. Please try again or contact the administrator.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import jwt, { SignOptions, JwtPayload } from "jsonwebtoken";
|
||||
import { User } from "@prisma/client";
|
||||
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.
|
||||
*
|
||||
* @deprecated
|
||||
* @param password - The plain text password to hash.
|
||||
* @returns The hashed password.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, authConfig.saltRounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a plain text password with a hashed password.
|
||||
*
|
||||
* @param password - The plain text password to compare.
|
||||
* @param hash - The hashed password to compare against.
|
||||
* @returns True if the passwords match, false otherwise.
|
||||
*/
|
||||
export async function comparePassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWT token based on user claims.
|
||||
*
|
||||
* @param userClaims - The user claims to encode in the JWT.
|
||||
* @param options - Optional signing options.
|
||||
* @returns The generated JWT token.
|
||||
*/
|
||||
export function createJwtToken(
|
||||
userClaims: UserClaims,
|
||||
options?: SignOptions
|
||||
): string {
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
|
||||
return jwt.sign(userClaims, secret, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a JWT token and retrieves the payload.
|
||||
*
|
||||
* @param token - The JWT token to decode.
|
||||
* @returns The decoded payload.
|
||||
*/
|
||||
export function decodeJwtToken(token: string): JwtPayload | string {
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
|
||||
|
||||
try {
|
||||
return jwt.verify(token, secret) as JwtPayload;
|
||||
} catch (error) {
|
||||
throw new AuthError(AuthErrorCode.INVALID_JWT_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
roles: true,
|
||||
directPermissions: true
|
||||
},
|
||||
where: {
|
||||
id: decodedToken.id,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
})
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import getCurrentUser from "./getCurrentUser";
|
||||
import "server-only";
|
||||
|
||||
/**
|
||||
* Checks if the current user has the specified permissions.
|
||||
*
|
||||
* @param permission - The specific permission to check. If it's "guest-only", the function returns true if the user is not authenticated. If it's "authenticated-only", it returns true if the user is authenticated. For other permissions, it checks against the user's roles and direct permissions.
|
||||
* @param currentUser - Optional. The current user object. If not provided, the function retrieves the current user.
|
||||
* @returns true if the user has the required permission, otherwise false.
|
||||
*/
|
||||
export default async function checkPermission(
|
||||
permission?: "guest-only" | "authenticated-only" | (string & {}),
|
||||
currentUser?: Awaited<ReturnType<typeof getCurrentUser>>
|
||||
): Promise<boolean> {
|
||||
// Allow if no specific permission is required.
|
||||
if (!permission) return true;
|
||||
|
||||
// Retrieve current user if not provided.
|
||||
const user = currentUser ?? (await getCurrentUser());
|
||||
|
||||
// Handle non-authenticated users.
|
||||
if (!user) {
|
||||
return permission === "guest-only";
|
||||
}
|
||||
|
||||
// Allow authenticated users if the permission is 'authenticated-only'.
|
||||
if (permission === "authenticated-only") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Short-circuit for super-admin role to allow all permissions.
|
||||
if (user.roles.some((role) => role.code === "super-admin")) return true;
|
||||
|
||||
// Aggregate all role codes and direct permissions into a set for efficient lookup.
|
||||
const permissions = new Set<string>([
|
||||
...user.roles.map((role) => role.code),
|
||||
...user.directPermissions.map((dp) => dp.code),
|
||||
]);
|
||||
|
||||
// Check if the user has the required permission.
|
||||
return permissions.has(permission);
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { cache } from "react"
|
||||
import "server-only"
|
||||
import { getUserFromToken } from "../authUtils"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Return the user if found, otherwise return null
|
||||
return user ? user : null;
|
||||
}
|
||||
|
||||
export default getCurrentUser;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import authConfig from "../../../config/auth";
|
||||
|
||||
/**
|
||||
* Hashes a plain text password using bcrypt.
|
||||
*
|
||||
* @param password - The plain text password to hash.
|
||||
* @returns The hashed password.
|
||||
*/
|
||||
export default async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, authConfig.saltRounds);
|
||||
}
|
||||
7
src/_features/auth/types/UserClaims.d.ts
vendored
7
src/_features/auth/types/UserClaims.d.ts
vendored
|
|
@ -1,7 +0,0 @@
|
|||
import { User } from "@prisma/client"
|
||||
|
||||
type UserClaims = {
|
||||
id: User["id"]
|
||||
}
|
||||
|
||||
export default UserClaims
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./DashboardTable";
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import DashboardTable from "./DashboardTable";
|
||||
|
||||
export { DashboardTable };
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// Use TypeScript enum for error codes to provide better autocompletion and error handling
|
||||
export const DashboardErrorCodes = [
|
||||
"UNAUTHORIZED",
|
||||
"NOT_FOUND",
|
||||
"UNKNOWN_ERROR",
|
||||
"INVALID_FORM_DATA",
|
||||
] as const;
|
||||
|
||||
interface ErrorOptions {
|
||||
message?: string,
|
||||
errorCode?: typeof DashboardErrorCodes[number] | 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?: 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;
|
||||
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a structured error response object.
|
||||
*/
|
||||
getErrorReponseObject(){
|
||||
return {
|
||||
success: false,
|
||||
dashboardError: true,
|
||||
error: {
|
||||
message: `${this.message}`,
|
||||
errorCode: this.errorCode,
|
||||
errors: this.formErrors ?? undefined
|
||||
}
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import { handleCatch, unauthorized } from "../../errors/DashboardError";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export default async function deletePermission(id: string): Promise<ServerResponse> {
|
||||
try {
|
||||
if (!(await checkPermission("permission.delete"))) return unauthorized();
|
||||
const permission = await prisma.permission.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
|
||||
revalidatePath(".")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "The permission has been deleted successfully",
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
return handleCatch(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
"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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
|
||||
export default async function getPermissionById(id: string) {
|
||||
if (!(await checkPermission("permission.read"))) unauthorized();
|
||||
|
||||
const permission = await prisma.permission.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
code: true,
|
||||
description: true,
|
||||
id: true,
|
||||
isActive: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Permission not found",
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Permission fetched successfully",
|
||||
data: permission,
|
||||
} as const;
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import permissionFormDataSchema, { PermissionFormData } from "../../../../modules/permission/formSchemas/PermissionFormData";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Upserts a permission based on the provided PermissionFormData.
|
||||
* If the permission already exists (determined by `id`), it updates the permission; otherwise, it creates a new permission.
|
||||
* Authorization checks are performed based on whether it's a create or update operation.
|
||||
*
|
||||
* @param data - The data for creating or updating the permission.
|
||||
* @returns An object containing the success status, message, and any errors.
|
||||
*/
|
||||
export default async function upsertPermission(
|
||||
data: PermissionFormData
|
||||
): Promise<ServerResponse> {
|
||||
try {
|
||||
const isInsert = !data.id;
|
||||
|
||||
// Authorization check
|
||||
const permissionType = isInsert ? "permission.create" : "permission.update";
|
||||
if (!(await checkPermission(permissionType))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
// Validate form data
|
||||
const validatedFields = permissionFormDataSchema.safeParse(data);
|
||||
if (!validatedFields.success) {
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
||||
})
|
||||
}
|
||||
const permissionData = {
|
||||
code: validatedFields.data.code,
|
||||
description: validatedFields.data.description,
|
||||
name: validatedFields.data.name,
|
||||
isActive: validatedFields.data.isActive,
|
||||
};
|
||||
|
||||
// Database operation
|
||||
if (isInsert) {
|
||||
if (await prisma.permission.findFirst({
|
||||
where: {
|
||||
code: permissionData.code
|
||||
}
|
||||
})){
|
||||
throw new DashboardError({
|
||||
errorCode: "INVALID_FORM_DATA",
|
||||
formErrors: {
|
||||
code: "The code is already exists"
|
||||
}
|
||||
})
|
||||
}
|
||||
await prisma.permission.create({ data: permissionData });
|
||||
} else {
|
||||
await prisma.permission.update({
|
||||
where: { id: validatedFields.data.id! },
|
||||
data: permissionData,
|
||||
});
|
||||
}
|
||||
|
||||
// Revalidate the cache
|
||||
revalidatePath(".");
|
||||
|
||||
// Return success message
|
||||
return {
|
||||
success: true,
|
||||
message: `Permission ${validatedFields.data.name} has been successfully ${
|
||||
isInsert ? "created" : "updated"
|
||||
}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return handleCatch(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { unauthorized } from "@/BaseError";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import "server-only";
|
||||
|
||||
/**
|
||||
* Retrieves all permissions along with the count of associated permissions and users.
|
||||
* Authorization check is performed for the operation.
|
||||
*
|
||||
* @returns An array of permission objects each including details and counts of related permissions and users.
|
||||
*/
|
||||
export default async function getPermissions() {
|
||||
// Authorization check
|
||||
if (!(await checkPermission("permissions.readAll"))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch permissions from the database
|
||||
const permissions = await prisma.permission.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
roles: true,
|
||||
directUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Transform the data into the desired format
|
||||
return permissions.map(
|
||||
({ id, code, name, description, isActive, _count }) => ({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
roleCount: _count.roles,
|
||||
//User count counts only direct user
|
||||
userCount: _count.directUsers,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error retrieving permissions", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./DeleteModal";
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import DashboardError from "@/features/dashboard/errors/DashboardError";
|
||||
import getPermissionById from "@/features/dashboard/permissions/actions/getPermissionById";
|
||||
import upsertPermission from "@/features/dashboard/permissions/actions/upsertPermission";
|
||||
import permissionFormDataSchema, {
|
||||
PermissionFormData,
|
||||
} from "@/features/dashboard/permissions/formSchemas/PermissionFormData";
|
||||
import withErrorHandling from "@/features/dashboard/utils/withServerAction";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
Stack,
|
||||
Switch,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Button,
|
||||
ScrollArea,
|
||||
Checkbox,
|
||||
Skeleton,
|
||||
Fieldset,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { TbDeviceFloppy } from "react-icons/tb";
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
readonly?: boolean;
|
||||
id?: string;
|
||||
opened: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for rendering a modal with a form to create or edit a permission.
|
||||
*
|
||||
* @param props - The props for the component.
|
||||
* @returns The rendered element.
|
||||
*/
|
||||
export default function FormModal(props: ModalProps) {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const form = useForm<PermissionFormData>({
|
||||
initialValues: {
|
||||
code: "",
|
||||
description: "",
|
||||
id: "",
|
||||
isActive: false,
|
||||
name: "",
|
||||
},
|
||||
validate: zodResolver(permissionFormDataSchema),
|
||||
validateInputOnChange: false,
|
||||
onValuesChange: (values) => {
|
||||
console.log(values);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches permission data by ID and populates the form if the modal is opened and an ID is provided.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!props.opened || !props.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFetching(true);
|
||||
getPermissionById(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,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
//TODO: Handle error
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
setFetching(false);
|
||||
});
|
||||
}, [props.opened, props.id]);
|
||||
|
||||
const closeModal = () => {
|
||||
form.reset()
|
||||
props.onClose ? props.onClose() : router.replace("?");
|
||||
};
|
||||
|
||||
const handleSubmit = (values: PermissionFormData) => {
|
||||
setSubmitting(true);
|
||||
withErrorHandling(() => upsertPermission(values))
|
||||
.then((response) => {
|
||||
showNotification(response.message!, "success");
|
||||
closeModal();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof DashboardError) {
|
||||
if (e.errorCode === "INVALID_FORM_DATA") {
|
||||
form.setErrors(e.formErrors ?? {});
|
||||
} else {
|
||||
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(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={props.opened}
|
||||
onClose={closeModal}
|
||||
title={props.title}
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
size="xl"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack mt="sm" gap="lg" px="lg">
|
||||
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
|
||||
{/* ID */}
|
||||
{form.values.id ? (
|
||||
<TextInput
|
||||
label="ID"
|
||||
readOnly
|
||||
variant="filled"
|
||||
{...form.getInputProps("id")}
|
||||
/>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{/* Code */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<TextInput
|
||||
data-autofocus
|
||||
label="Code"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Name */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<TextInput
|
||||
label="Name"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Description */}
|
||||
<Skeleton visible={isFetching}>
|
||||
<Textarea
|
||||
label="Description"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
<Skeleton visible={isFetching}>
|
||||
<Checkbox
|
||||
label="Active"
|
||||
labelPosition="right"
|
||||
{...form.getInputProps("isActive", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{!props.readonly && (
|
||||
<Button
|
||||
variant="filled"
|
||||
leftSection={<TbDeviceFloppy size={20} />}
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./FormModal";
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import DeleteModal from "./DeleteModal";
|
||||
import FormModal from "./FormModal";
|
||||
|
||||
export { FormModal, DeleteModal };
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
"use client";
|
||||
import { Table, Text, Flex, Button, Center } from "@mantine/core";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import React, { useState } from "react";
|
||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import { PermissionFormData } from "@/features/dashboard/permissions/formSchemas/PermissionFormData";
|
||||
import { string } from "zod";
|
||||
import { DashboardTable } from "@/features/dashboard/components";
|
||||
import getPermissions from "../../data/getPermissions";
|
||||
import FormModal, { ModalProps } from "../../modals/FormModal/FormModal";
|
||||
import DeleteModal, { DeleteModalProps } from "../../modals/DeleteModal/DeleteModal";
|
||||
import createColumns from "./_columns";
|
||||
|
||||
interface Props {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
permissionData: Awaited<ReturnType<typeof getPermissions>>;
|
||||
}
|
||||
|
||||
export default function PermissionsTable(props: Props) {
|
||||
const [modalProps, setModalProps] = useState<ModalProps>({
|
||||
opened: false,
|
||||
title: "",
|
||||
});
|
||||
|
||||
const [deleteModalProps, setDeleteModalProps] = useState<
|
||||
Omit<DeleteModalProps, "onClose">
|
||||
>({
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: props.permissionData,
|
||||
columns: createColumns({
|
||||
permissions: props.permissions,
|
||||
actions: {
|
||||
detail: (id: string) => openFormModal("detail", id),
|
||||
edit: (id: string) => openFormModal("edit", id),
|
||||
delete: (id: string, name: string) => openDeleteModal(id, name),
|
||||
},
|
||||
}),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
defaultColumn: {
|
||||
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
||||
},
|
||||
});
|
||||
|
||||
const openFormModal = (type: "create" | "edit" | "detail", id?: string) => {
|
||||
const openCreateModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Create new permission",
|
||||
});
|
||||
};
|
||||
|
||||
const openDetailModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Permission detail",
|
||||
readonly: true,
|
||||
});
|
||||
};
|
||||
|
||||
const openEditModal = () => {
|
||||
setModalProps({
|
||||
id,
|
||||
opened: true,
|
||||
title: "Edit permission",
|
||||
});
|
||||
};
|
||||
|
||||
type === "create"
|
||||
? openCreateModal()
|
||||
: type === "detail"
|
||||
? openDetailModal()
|
||||
: openEditModal();
|
||||
};
|
||||
|
||||
const openDeleteModal = (id: string, name: string) => {
|
||||
setDeleteModalProps({
|
||||
data: {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalProps({
|
||||
id: "",
|
||||
opened: false,
|
||||
title: "",
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Add view when data is empty
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="flex-end">
|
||||
{props.permissions.create && (
|
||||
<Button
|
||||
leftSection={<TbPlus />}
|
||||
onClick={() => openFormModal("create")}
|
||||
>
|
||||
New Permission
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<DashboardTable table={table} />
|
||||
|
||||
<FormModal {...modalProps} onClose={closeModal} />
|
||||
<DeleteModal
|
||||
{...deleteModalProps}
|
||||
onClose={() => setDeleteModalProps({})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Badge, Flex } from "@mantine/core";
|
||||
import createActionButtons from "@/features/dashboard/utils/createActionButtons";
|
||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
|
||||
|
||||
export interface PermissionRow {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
roleCount: number;
|
||||
userCount: number;
|
||||
}
|
||||
|
||||
interface ColumnOptions {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
actions: {
|
||||
detail: (id: string) => void;
|
||||
edit: (id: string) => void;
|
||||
delete: (id: string, name: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const createColumns = (options: ColumnOptions) => {
|
||||
const columnHelper = createColumnHelper<PermissionRow>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("id", {
|
||||
id: "sequence",
|
||||
header: "#",
|
||||
cell: (props) => props.row.index + 1,
|
||||
}),
|
||||
|
||||
columnHelper.accessor("code", {
|
||||
header: "Code",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("isActive", {
|
||||
header: "Status",
|
||||
cell: (props) => {
|
||||
props.getValue() ? (
|
||||
<Badge color="green">Enabled</Badge>
|
||||
) : (
|
||||
<Badge color="orange">Disabled</Badge>
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.accessor("roleCount", {
|
||||
header: "Roles",
|
||||
}),
|
||||
|
||||
columnHelper.accessor("userCount", {
|
||||
header: "Users",
|
||||
}),
|
||||
|
||||
columnHelper.display({
|
||||
id: "Actions",
|
||||
header: "Actions",
|
||||
cell: (props) => (
|
||||
<Flex gap="xs">
|
||||
{createActionButtons([
|
||||
{
|
||||
label: "Detail",
|
||||
permission: options.permissions.read,
|
||||
action: () =>
|
||||
options.actions.detail(props.row.original.id),
|
||||
color: "green",
|
||||
icon: <TbEye />,
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
permission: options.permissions.update,
|
||||
action: () =>
|
||||
options.actions.edit(props.row.original.id),
|
||||
color: "yellow",
|
||||
icon: <TbPencil />,
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
permission: options.permissions.delete,
|
||||
action: () =>
|
||||
options.actions.delete(
|
||||
props.row.original.id,
|
||||
props.row.original.name
|
||||
),
|
||||
color: "red",
|
||||
icon: <TbTrash />,
|
||||
},
|
||||
])}
|
||||
</Flex>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export default createColumns;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./PermissionTable"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import PermissionTable from "./PermissionTable";
|
||||
|
||||
export { PermissionTable };
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import { handleCatch, unauthorized } from "../../errors/DashboardError";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export default async function deleteRole(id: string): Promise<ServerResponse> {
|
||||
try {
|
||||
if (!(await checkPermission("role.delete"))) return unauthorized();
|
||||
const role = await prisma.role.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
|
||||
revalidatePath(".")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "The role has been deleted successfully",
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
return handleCatch(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./DeleteModal";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./FormModal"
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import FormModal from "./FormModal";
|
||||
import DeleteModal from "./DeleteModal";
|
||||
|
||||
export { DeleteModal, FormModal };
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import checkMultiplePermissions from "@/modules/dashboard/services/checkMultiplePermissions";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
import getAllRoles from "@/modules/role/actions/getAllRoles";
|
||||
import RolesTable from "@/modules/role/tables/RolesTable/RolesTable";
|
||||
import { Card, Stack, Title } from "@mantine/core";
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
import RolesTable from "./_tables/RolesTable/RolesTable";
|
||||
import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
|
||||
import { unauthorized } from "@/features/dashboard/errors/DashboardError";
|
||||
|
||||
interface Props {
|
||||
searchParams: {
|
||||
|
|
@ -19,7 +19,7 @@ export const metadata: Metadata = {
|
|||
title: "Roles - Dashboard",
|
||||
};
|
||||
|
||||
export default async function RolesPage({ searchParams }: Props) {
|
||||
export default async function RolesPage() {
|
||||
const permissions = await checkMultiplePermissions({
|
||||
create: "role.create",
|
||||
readAll: "role.readAll",
|
||||
|
|
@ -30,7 +30,7 @@ export default async function RolesPage({ searchParams }: Props) {
|
|||
|
||||
if (!permissions.readAll) unauthorized()
|
||||
|
||||
const roles = await getRoles();
|
||||
const roles = await getAllRoles();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
|
|
|
|||
29
src/modules/role/actions/deleteRole.ts
Normal file
29
src/modules/role/actions/deleteRole.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use server";
|
||||
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function deleteRole(
|
||||
id: string
|
||||
): Promise<ServerResponseAction> {
|
||||
try {
|
||||
if (!(await checkPermission("roles.delete"))) return unauthorized();
|
||||
const role = await prisma.role.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
revalidatePath(".");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "The role has been deleted successfully",
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
return handleCatch(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
"use server"
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
import "server-only";
|
||||
import { unauthorized } from "@/features/dashboard/errors/DashboardError";
|
||||
|
||||
/**
|
||||
* Retrieves all roles along with the count of associated permissions and users.
|
||||
|
|
@ -9,10 +10,10 @@ import { unauthorized } from "@/features/dashboard/errors/DashboardError";
|
|||
*
|
||||
* @returns An array of role objects each including details and counts of related permissions and users.
|
||||
*/
|
||||
export default async function getRoles() {
|
||||
export default async function getAllRoles() {
|
||||
// Authorization check
|
||||
if (!await checkPermission("roles.getAll")) {
|
||||
return unauthorized();
|
||||
unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
"use server";
|
||||
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import { handleCatch, notFound, unauthorized } from "../../errors/DashboardError";
|
||||
import ServerResponse from "@/types/Action";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
|
||||
type RoleData = {
|
||||
id: string;
|
||||
|
|
@ -18,10 +19,10 @@ type RoleData = {
|
|||
}[]
|
||||
}
|
||||
|
||||
export default async function getRoleById(id: string): Promise<ServerResponse<RoleData>>{
|
||||
export default async function getRoleById(id: string): Promise<ServerResponseAction<RoleData>>{
|
||||
try{
|
||||
|
||||
if (!(await checkPermission("role.read"))) return unauthorized();
|
||||
if (!(await checkPermission("roles.read"))) return unauthorized();
|
||||
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { id },
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
"use server";
|
||||
|
||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
|
||||
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 ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||
|
||||
/**
|
||||
* Upserts a role based on the provided RoleFormData.
|
||||
|
|
@ -21,7 +20,7 @@ import DashboardError, {
|
|||
*/
|
||||
export default async function upsertRole(
|
||||
data: RoleFormData
|
||||
): Promise<ServerResponse> {
|
||||
): Promise<ServerResponseAction> {
|
||||
try {
|
||||
const isInsert = !data.id;
|
||||
|
||||
|
|
@ -2,24 +2,16 @@
|
|||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import deleteRole from "@/features/dashboard/roles/actions/deleteRole";
|
||||
import withErrorHandling from "@/features/dashboard/utils/withServerAction";
|
||||
import { error } from "console";
|
||||
import DashboardError from "@/features/dashboard/errors/DashboardError";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import withServerAction from "@/modules/dashboard/utils/withServerAction";
|
||||
import deleteRole from "../actions/deleteRole";
|
||||
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||
|
||||
export interface DeleteModalProps {
|
||||
data?: {
|
||||
|
|
@ -48,7 +40,7 @@ export default function DeleteModal(props: DeleteModalProps) {
|
|||
if (!props.data?.id) return;
|
||||
setSubmitting(true);
|
||||
|
||||
withErrorHandling(() => deleteRole(props.data!.id))
|
||||
withServerAction(deleteRole, props.data!.id)
|
||||
.then((response) => {
|
||||
showNotification(
|
||||
response.message ?? "Role deleted successfully"
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
/* 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, {
|
||||
RoleFormData,
|
||||
} from "@/features/dashboard/roles/formSchemas/RoleFormData";
|
||||
import withErrorHandling from "@/features/dashboard/utils/withServerAction";
|
||||
import { showNotification } from "@/utils/notifications";
|
||||
import {
|
||||
Flex,
|
||||
|
|
@ -28,6 +20,12 @@ import { useRouter } from "next/navigation";
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TbDeviceFloppy } from "react-icons/tb";
|
||||
import { string } from "zod";
|
||||
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
|
||||
import getAllPermissions from "@/modules/permission/actions/getAllPermissions";
|
||||
import withServerAction from "@/modules/dashboard/utils/withServerAction";
|
||||
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||
import getRoleById from "../actions/getRoleById";
|
||||
import upsertRole from "../actions/upsertRole";
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
|
|
@ -71,7 +69,7 @@ export default function FormModal(props: ModalProps) {
|
|||
//Fetch Permissions
|
||||
useEffect(() => {
|
||||
setFetching(true);
|
||||
withErrorHandling(getAllPermissions)
|
||||
withServerAction(getAllPermissions)
|
||||
.then((response) => {
|
||||
setAllPermissions(response.data);
|
||||
})
|
||||
|
|
@ -100,7 +98,7 @@ export default function FormModal(props: ModalProps) {
|
|||
}
|
||||
|
||||
setFetching(true);
|
||||
withErrorHandling(getRoleById, props.id)
|
||||
withServerAction(getRoleById, props.id)
|
||||
.then((response) => {
|
||||
const data = response.data;
|
||||
form.setValues({
|
||||
|
|
@ -137,7 +135,7 @@ export default function FormModal(props: ModalProps) {
|
|||
|
||||
const handleSubmit = (values: RoleFormData) => {
|
||||
setSubmitting(true);
|
||||
withErrorHandling(upsertRole, values)
|
||||
withServerAction(upsertRole, values)
|
||||
.then((response) => {
|
||||
showNotification(response.message!, "success");
|
||||
closeModal();
|
||||
|
|
@ -1,26 +1,18 @@
|
|||
"use client";
|
||||
import { Table, Text, Flex, Button, Center } from "@mantine/core";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
|
||||
import { Text, Flex, Button } from "@mantine/core";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import React, { useState } from "react";
|
||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||
import createColumns from "./columns";
|
||||
import FormModal from "../../_modals/FormModal";
|
||||
import { ModalProps } from "../../_modals/FormModal/FormModal";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormData";
|
||||
import { string } from "zod";
|
||||
import { DeleteModal } from "../../_modals";
|
||||
import { DeleteModalProps } from "../../_modals/DeleteModal/DeleteModal";
|
||||
import { DashboardTable } from "@/features/dashboard/components";
|
||||
import getAllRoles from "../../actions/getAllRoles";
|
||||
import FormModal, { ModalProps } from "../../modals/FormModal";
|
||||
import DeleteModal, { DeleteModalProps } from "../../modals/DeleteModal";
|
||||
import createColumns from "./columns";
|
||||
import DashboardTable from "@/modules/dashboard/components/DashboardTable";
|
||||
|
||||
interface Props {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
roles: Awaited<ReturnType<typeof getRoles>>;
|
||||
roles: Awaited<ReturnType<typeof getAllRoles>>;
|
||||
}
|
||||
|
||||
export default function RolesTable(props: Props) {
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||
import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormData";
|
||||
import createActionButtons from "@/features/dashboard/utils/createActionButtons";
|
||||
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
|
||||
import createActionButtons from "@/modules/dashboard/utils/createActionButton";
|
||||
import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import Link from "next/link";
|
||||
Loading…
Reference in New Issue
Block a user