fix issues

This commit is contained in:
Rajpreet0 2026-03-24 16:11:20 +01:00
parent 11f5be2d72
commit 3af15a262a
6 changed files with 124 additions and 34 deletions

View file

@ -1,15 +1,18 @@
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
import { requireManagerOrPermission, isEmployee } from "@/src/lib/auth/getUser";
import EmployeeCreateView from "@/src/modules/employee/ui/views/employee-create-view";
import { getMyEmployeeRoles } from "@/src/modules/employee/actions/employee-actions";
const EmployeeCreate = async () => {
await requireManagerOrPermission((p) => p.employees.create);
const userData = await requireManagerOrPermission((p) => p.employees.create);
// Lade alle bereits verwendeten custom Rollen
const rolesResult = await getMyEmployeeRoles();
const customRoles = rolesResult.success ? rolesResult.roles : [];
return <EmployeeCreateView customRoles={customRoles} />;
// Employees können nur Rechte vergeben, die sie selbst besitzen
const callerPermissions = isEmployee(userData) ? userData.permissions : null;
return <EmployeeCreateView customRoles={customRoles} callerPermissions={callerPermissions} />;
};
export default EmployeeCreate;

View file

@ -306,7 +306,7 @@ export function isEmployee(userData: AuthUserData): userData is EmployeeData {
*/
export async function requireManagerOrPermission(
check: (permissions: EmployeeData["permissions"]) => boolean
) {
): Promise<ManagerData | EmployeeData> {
const userData = await requireAuthWithData();
if (isManager(userData)) return userData;
@ -315,4 +315,5 @@ export async function requireManagerOrPermission(
const { redirect } = await import("next/navigation");
redirect("/dashboard");
throw new Error("unreachable");
}

View file

@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { getMyCourses } from "../../actions/course-actions";
import { useDeleteCourse } from "../../hooks/useDeleteCourse";
import { getAllRooms } from "@/src/modules/rooms/actions/room-actions";
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
import { Button } from "@/src/components/ui/button";
import { toast } from "sonner";
import { useRouter, useSearchParams } from "next/navigation";
@ -88,7 +88,7 @@ const CourseListView = ({ canCreate, canEdit, canDelete }: CourseListViewProps)
// Load rooms once on mount
useEffect(() => {
const loadRooms = async () => {
const result = await getAllRooms();
const result = await getMyRooms();
if (result.success) {
const map: Record<string, string> = {};
result.rooms.forEach((room) => {

View file

@ -27,6 +27,31 @@ function generateOnboardingToken(): string {
return randomBytes(32).toString("hex");
}
type PermissionGroup = { view: boolean; create: boolean; edit: boolean; delete: boolean };
type PermissionsObj = {
employees: PermissionGroup;
courses: PermissionGroup;
rooms: PermissionGroup;
};
/**
* Begrenzt die zu speichernden Berechtigungen auf das, was der Aufrufer selbst besitzt.
* Verhindert Privilege-Escalation: ein Employee kann nur Rechte vergeben, die er selbst hat.
*/
function clampPermissions(requested: PermissionsObj, ceiling: PermissionsObj): PermissionsObj {
const clamp = (req: PermissionGroup, ceil: PermissionGroup): PermissionGroup => ({
view: req.view && ceil.view,
create: req.create && ceil.create,
edit: req.edit && ceil.edit,
delete: req.delete && ceil.delete,
});
return {
employees: clamp(requested.employees, ceiling.employees),
courses: clamp(requested.courses, ceiling.courses),
rooms: clamp(requested.rooms, ceiling.rooms),
};
}
/**
* Erstellt einen neuen Mitarbeiter in der Datenbank
*
@ -105,13 +130,18 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
managerId = selfRecord?.createdBy ?? userData.id;
}
// Schritt 8: Mitarbeiter in der Datenbank erstellen
// Schritt 8: Berechtigungen auf Caller-Ceiling begrenzen (Privilege-Escalation verhindern)
const finalPermissions = isEmployee(userData)
? clampPermissions(validatedData.permissions, userData.permissions)
: validatedData.permissions;
// Schritt 9: Mitarbeiter in der Datenbank erstellen
const employee = await prisma.employee.create({
data: {
email: validatedData.email,
roles: validatedData.roles,
locations: validatedData.locations || [],
permissions: validatedData.permissions,
permissions: finalPermissions,
status: status,
isOnboarded: false,
onboardingToken,
@ -387,14 +417,19 @@ export async function updateEmployee(
shouldSendEmail = true; // E-Mail nur senden wenn neuer Token generiert wurde
}
// Schritt 9: Mitarbeiter in der Datenbank aktualisieren
// Schritt 9: Berechtigungen auf Caller-Ceiling begrenzen (Privilege-Escalation verhindern)
const finalPermissions = isEmployee(userData)
? clampPermissions(validatedData.permissions, userData.permissions)
: validatedData.permissions;
// Schritt 10: Mitarbeiter in der Datenbank aktualisieren
await prisma.employee.update({
where: { id: employeeId },
data: {
email: validatedData.email,
roles: validatedData.roles,
locations: validatedData.locations || [],
permissions: validatedData.permissions,
permissions: finalPermissions,
status,
onboardingToken,
onboardingTokenExpiry,

View file

@ -42,11 +42,18 @@ type PermissionsState = {
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
};
type CallerPermissions = {
employees: { view: boolean; create: boolean; edit: boolean; delete: boolean };
courses: { view: boolean; create: boolean; edit: boolean; delete: boolean };
rooms: { view: boolean; create: boolean; edit: boolean; delete: boolean };
} | null;
interface EmployeeCreateViewProps {
customRoles: Array<{ value: string; label: string }>;
callerPermissions: CallerPermissions;
}
const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
const EmployeeCreateView = ({ customRoles, callerPermissions }: EmployeeCreateViewProps) => {
const router = useRouter();
const [employeeMail, setEmplyoeeMail] = useState("");
const [isMultipleLocation, setIsMultipleLocation] = useState(false);
@ -115,29 +122,39 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
const handlePublish = async () => {
setIsSubmitting(true);
setErrors({});
const result = await createEmployee(buildData(), "published");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) setErrors(result.fieldErrors);
try {
const result = await createEmployee(buildData(), "published");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) setErrors(result.fieldErrors);
}
} catch {
toast.error("Ein unerwarteter Fehler ist aufgetreten");
} finally {
setIsSubmitting(false);
}
setIsSubmitting(false);
};
const handleSaveDraft = async () => {
setIsSubmitting(true);
setErrors({});
const result = await createEmployee(buildData(), "draft");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) setErrors(result.fieldErrors);
try {
const result = await createEmployee(buildData(), "draft");
if (result.success) {
toast.success(result.message);
router.push("/employee");
} else {
toast.error(result.error || "Ein Fehler ist aufgetreten");
if (result.fieldErrors) setErrors(result.fieldErrors);
}
} catch {
toast.error("Ein unerwarteter Fehler ist aufgetreten");
} finally {
setIsSubmitting(false);
}
setIsSubmitting(false);
};
return (
@ -211,14 +228,34 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
id="all-permissions"
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
checked={PERMISSION_GROUPS.every((g) =>
PERMISSION_ACTIONS.every((a) => permissionsState[g.key][a.key])
PERMISSION_ACTIONS.every((a) => {
if (callerPermissions && !callerPermissions[g.key][a.key]) return true;
return permissionsState[g.key][a.key];
})
)}
onCheckedChange={(checked) => {
const value = checked === true;
setPermissionsState({
employees: { view: value, create: value, edit: value, delete: value },
courses: { view: value, create: value, edit: value, delete: value },
rooms: { view: value, create: value, edit: value, delete: value },
employees: {
view: value && (callerPermissions ? callerPermissions.employees.view : true),
create:
value && (callerPermissions ? callerPermissions.employees.create : true),
edit: value && (callerPermissions ? callerPermissions.employees.edit : true),
delete:
value && (callerPermissions ? callerPermissions.employees.delete : true),
},
courses: {
view: value && (callerPermissions ? callerPermissions.courses.view : true),
create: value && (callerPermissions ? callerPermissions.courses.create : true),
edit: value && (callerPermissions ? callerPermissions.courses.edit : true),
delete: value && (callerPermissions ? callerPermissions.courses.delete : true),
},
rooms: {
view: value && (callerPermissions ? callerPermissions.rooms.view : true),
create: value && (callerPermissions ? callerPermissions.rooms.create : true),
edit: value && (callerPermissions ? callerPermissions.rooms.edit : true),
delete: value && (callerPermissions ? callerPermissions.rooms.delete : true),
},
});
}}
/>
@ -233,17 +270,23 @@ const EmployeeCreateView = ({ customRoles }: EmployeeCreateViewProps) => {
<div className="grid grid-cols-4 gap-2">
{PERMISSION_ACTIONS.map((action) => {
const id = `${group.key}-${action.key}`;
const callerHas =
callerPermissions === null || callerPermissions[group.key][action.key];
return (
<div key={id} className="grid grid-cols-[auto_1fr] items-center gap-2">
<Checkbox
id={id}
className="shadow-none h-5 w-5 bg-white border-blue cursor-pointer"
checked={permissionsState[group.key][action.key]}
disabled={!callerHas}
onCheckedChange={(checked) =>
handlePermissionChange(group.key, action.key, checked === true)
}
/>
<Label htmlFor={id} className="cursor-pointer text-blue text-sm">
<Label
htmlFor={id}
className={`text-blue text-sm ${callerHas ? "cursor-pointer" : "cursor-not-allowed opacity-40"}`}
>
{action.label}
</Label>
</div>

View file

@ -24,12 +24,20 @@ const RoomsListItem: React.FC<RoomsListItemProps> = ({
</div>
<div className="flex gap-2">
{showDeleteIcon && (
<button onClick={onDelete} className="p-2 rounded-md hover:bg-red-100 transition">
<button
aria-label={`Raum ${roomName} löschen`}
onClick={onDelete}
className="p-2 rounded-md hover:bg-red-100 transition"
>
<Trash2 size={20} className="text-red-600" />
</button>
)}
{onEdit && (
<button onClick={onEdit} className="p-2 rounded-md hover:bg-gray-100 transition">
<button
aria-label={`Raum ${roomName} bearbeiten`}
onClick={onEdit}
className="p-2 rounded-md hover:bg-gray-100 transition"
>
<Pen size={20} className="text-gray-700" />
</button>
)}