Add role selection for user

This commit is contained in:
sianida26 2024-02-14 17:03:57 +07:00
parent 8e7de074c1
commit f81a02f5e4
12 changed files with 184 additions and 90 deletions

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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
View File

@ -0,0 +1,9 @@
export default interface Role {
id: string;
code: string;
name: string;
description: string;
isActive: boolean;
permissionCount: number;
userCount: number;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 })),
},
},
});
}

View File

@ -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;

View File

@ -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

View File

@ -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;