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