Add role selection for user
This commit is contained in:
parent
8e7de074c1
commit
f81a02f5e4
|
|
@ -28,7 +28,7 @@ export default async function RolesPage({ searchParams }: Props) {
|
|||
});
|
||||
|
||||
const res = await getAllPermissions();
|
||||
if (!res.success) throw new Error();
|
||||
if (!res.success) throw new Error("Error while fetch permission");
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
|
|
|
|||
|
|
@ -30,13 +30,14 @@ export default async function RolesPage() {
|
|||
|
||||
if (!permissions.readAll) unauthorized()
|
||||
|
||||
const roles = await getAllRoles();
|
||||
const res = await getAllRoles();
|
||||
if (!res.success) throw new Error("Error while fetch roles");
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={1}>Roles</Title>
|
||||
<Card>
|
||||
<RolesTable permissions={permissions} roles={roles} />
|
||||
<RolesTable permissions={permissions} roles={res.data} />
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default function DashboardLayout(props: Props) {
|
|||
{/* Navbar */}
|
||||
<AppNavbar />
|
||||
|
||||
<AppShell.Main className="bg-slate-100">
|
||||
<AppShell.Main className="bg-slate-100" styles={{main: {backgroundColor: "rgb(241 245 249)"}}}>
|
||||
{props.children}
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
"use server"
|
||||
"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 "server-only";
|
||||
import Role from "../types/Role";
|
||||
|
||||
/**
|
||||
* Retrieves all roles along with the count of associated permissions and users.
|
||||
|
|
@ -10,37 +13,45 @@ import "server-only";
|
|||
*
|
||||
* @returns An array of role objects each including details and counts of related permissions and users.
|
||||
*/
|
||||
export default async function getAllRoles() {
|
||||
// Authorization check
|
||||
if (!await checkPermission("roles.getAll")) {
|
||||
unauthorized();
|
||||
}
|
||||
export default async function getAllRoles(): Promise<
|
||||
ServerResponseAction<Role[]>
|
||||
> {
|
||||
try {
|
||||
// Authorization check
|
||||
if (!(await checkPermission("roles.getAll"))) {
|
||||
unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch roles from the database
|
||||
const roles = await prisma.role.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
permissions: true,
|
||||
users: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Fetch roles from the database
|
||||
const roles = await prisma.role.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
permissions: true,
|
||||
users: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Transform the data into the desired format
|
||||
return roles.map(({ id, code, name, description, isActive, _count }) => ({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
permissionCount: _count.permissions,
|
||||
userCount: _count.users
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error retrieving roles', error);
|
||||
throw error;
|
||||
}
|
||||
// Transform the data into the desired format
|
||||
const result = roles.map(
|
||||
({ id, code, name, description, isActive, _count }) => ({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
permissionCount: _count.permissions,
|
||||
userCount: _count.users,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
}
|
||||
} catch (error) {
|
||||
return handleCatch(error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import FormModal, { ModalProps } from "../../modals/FormModal";
|
|||
import DeleteModal, { DeleteModalProps } from "../../modals/DeleteModal";
|
||||
import createColumns from "./columns";
|
||||
import DashboardTable from "@/modules/dashboard/components/DashboardTable";
|
||||
import Role from "../../types/Role";
|
||||
|
||||
interface Props {
|
||||
permissions: Partial<CrudPermissions>;
|
||||
roles: Awaited<ReturnType<typeof getAllRoles>>;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
export default function RolesTable(props: Props) {
|
||||
|
|
|
|||
9
src/modules/role/types/Role.d.ts
vendored
Normal file
9
src/modules/role/types/Role.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export default interface Role {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
permissionCount: number;
|
||||
userCount: number;
|
||||
}
|
||||
|
|
@ -6,27 +6,32 @@ import "server-only";
|
|||
const getAllUsers = async () => {
|
||||
if (!(await checkPermission("users.readAll"))) unauthorized();
|
||||
|
||||
try {
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
photoProfile: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = users.map((user) => ({
|
||||
...user,
|
||||
photoUrl: user.photoProfile ?? null,
|
||||
photoProfile: undefined,
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (e){
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
photoProfile: true,
|
||||
name: true,
|
||||
roles: {
|
||||
select: {
|
||||
name: true,
|
||||
code: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = users.map((user) => ({
|
||||
...user,
|
||||
photoUrl: user.photoProfile ?? null,
|
||||
photoProfile: undefined,
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export default getAllUsers;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use server"
|
||||
"use server";
|
||||
import "server-only";
|
||||
import prisma from "@/db";
|
||||
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||
|
|
@ -6,11 +6,15 @@ import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
|||
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||
|
||||
type UserData = {
|
||||
id: string,
|
||||
email: string,
|
||||
name: string,
|
||||
photoProfileUrl: string
|
||||
}
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
photoProfileUrl: string;
|
||||
roles: {
|
||||
code: string,
|
||||
name: string
|
||||
}[]
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves detailed information of a user by their ID.
|
||||
|
|
@ -18,7 +22,9 @@ type UserData = {
|
|||
* @param id The unique identifier of the user.
|
||||
* @returns The user's detailed information or an error response.
|
||||
*/
|
||||
export default async function getUserDetailById(id: string): Promise<ServerResponseAction<UserData>> {
|
||||
export default async function getUserDetailById(
|
||||
id: string
|
||||
): Promise<ServerResponseAction<UserData>> {
|
||||
// Check user permission
|
||||
if (!checkPermission("users.read")) return unauthorized();
|
||||
|
||||
|
|
@ -30,6 +36,12 @@ export default async function getUserDetailById(id: string): Promise<ServerRespo
|
|||
email: true,
|
||||
name: true,
|
||||
photoProfile: true,
|
||||
roles: {
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -46,6 +58,7 @@ export default async function getUserDetailById(id: string): Promise<ServerRespo
|
|||
email: user.email ?? "",
|
||||
name: user.name ?? "",
|
||||
photoProfileUrl: user.photoProfile ?? "",
|
||||
roles: user.roles
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -49,8 +49,20 @@ export default async function upsertUser(
|
|||
photoProfile: validatedFields.data.photoProfileUrl ?? "",
|
||||
email: validatedFields.data.email,
|
||||
};
|
||||
|
||||
const passwordHash = await hashPassword(validatedFields.data.password!);
|
||||
|
||||
const roles = await prisma.role.findMany({
|
||||
where: {
|
||||
code: {
|
||||
in: validatedFields.data.roles,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true, // Only select the id field
|
||||
},
|
||||
});
|
||||
|
||||
// Database operation
|
||||
if (isInsert) {
|
||||
if (
|
||||
|
|
@ -67,11 +79,24 @@ export default async function upsertUser(
|
|||
},
|
||||
});
|
||||
}
|
||||
await prisma.user.create({ data: { ...userData, passwordHash } });
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
...userData,
|
||||
passwordHash,
|
||||
roles: {
|
||||
connect: roles.map((role) => ({ id: role.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { id: validatedFields.data.id! },
|
||||
data: userData,
|
||||
data: {
|
||||
...userData,
|
||||
roles: {
|
||||
set: roles.map((role) => ({ id: role.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,19 @@ export interface UserFormData {
|
|||
name: string;
|
||||
photoProfileUrl: string;
|
||||
email: string;
|
||||
password: string | undefined
|
||||
password: string | undefined;
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
const userFormDataSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
photoProfileUrl: z.union([z.string(), z.null()]),
|
||||
photoProfileUrl: z.union([z.string().url(), z.null(), z.string()]),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8).optional(),
|
||||
});
|
||||
|
||||
password: z.string().optional(),
|
||||
roles: z.array(z.string())
|
||||
}).refine((data) => data.id || data.password || data.password!.length >= 8, {
|
||||
message: "Password is required and must be at least 8 characters long if id is empty",
|
||||
path: ["password"],
|
||||
});
|
||||
export default userFormDataSchema;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
Center,
|
||||
Avatar,
|
||||
PasswordInput,
|
||||
MultiSelect,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -29,6 +30,8 @@ import withServerAction from "@/modules/dashboard/utils/withServerAction";
|
|||
import upsertUser from "../actions/upsertUser";
|
||||
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||
import stringToColorHex from "@/core/utils/stringToColorHex";
|
||||
import getAllRoles from "@/modules/role/actions/getAllRoles";
|
||||
import Role from "@/modules/role/types/Role";
|
||||
|
||||
export interface ModalProps {
|
||||
title: string;
|
||||
|
|
@ -49,6 +52,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
|
||||
const form = useForm<UserFormData>({
|
||||
initialValues: {
|
||||
|
|
@ -57,6 +61,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
name: "",
|
||||
photoProfileUrl: "",
|
||||
password: "",
|
||||
roles: [],
|
||||
},
|
||||
validate: zodResolver(userFormDataSchema),
|
||||
validateInputOnChange: false,
|
||||
|
|
@ -80,6 +85,7 @@ export default function UserFormModal(props: ModalProps) {
|
|||
id: data.id,
|
||||
name: data.name,
|
||||
photoProfileUrl: data.photoProfileUrl,
|
||||
roles: data.roles.map(role => role.code)
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -92,6 +98,17 @@ export default function UserFormModal(props: ModalProps) {
|
|||
});
|
||||
}, [props.opened, props.id]);
|
||||
|
||||
// Fetch Roles
|
||||
useEffect(() => {
|
||||
withServerAction(getAllRoles)
|
||||
.then((response) => {
|
||||
setRoles(response.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeModal = () => {
|
||||
form.reset();
|
||||
props.onClose ? props.onClose() : router.replace("?");
|
||||
|
|
@ -190,6 +207,19 @@ export default function UserFormModal(props: ModalProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Role */}
|
||||
<MultiSelect
|
||||
label="Roles"
|
||||
readOnly={props.readonly}
|
||||
disabled={isSubmitting}
|
||||
value={form.values.roles}
|
||||
onChange={(values) =>
|
||||
form.setFieldValue("roles", values)
|
||||
}
|
||||
data={roles.map((role) => role.code)}
|
||||
error={form.errors.roles}
|
||||
/>
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
export default config
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user