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(); const res = await getAllPermissions();
if (!res.success) throw new Error(); if (!res.success) throw new Error("Error while fetch permission");
return ( return (
<Stack> <Stack>

View File

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

View File

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

View File

@ -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<
// Authorization check ServerResponseAction<Role[]>
if (!await checkPermission("roles.getAll")) { > {
unauthorized(); try {
} // Authorization check
if (!(await checkPermission("roles.getAll"))) {
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, ({ id, code, name, description, isActive, _count }) => ({
code, id,
name, code,
description, name,
isActive, description,
permissionCount: _count.permissions, isActive,
userCount: _count.users permissionCount: _count.permissions,
})); userCount: _count.users,
} catch (error) { })
console.error('Error retrieving roles', error); );
throw error;
} 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 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
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 () => { 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,
const result = users.map((user) => ({ },
...user, },
photoUrl: user.photoProfile ?? null, },
photoProfile: undefined, });
}));
const result = users.map((user) => ({
return result; ...user,
} catch (e){ photoUrl: user.photoProfile ?? null,
throw e; photoProfile: undefined,
} }));
return result;
} catch (e) {
throw e;
}
}; };
export default getAllUsers; export default getAllUsers;

View File

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

View File

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

View File

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

View File

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

View File

@ -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))', corePlugins: { preflight: false },
'gradient-conic': plugins: [],
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', };
}, export default config;
},
},
plugins: [],
}
export default config