mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:41:16 +00:00
fix issues
This commit is contained in:
parent
11f5be2d72
commit
3af15a262a
6 changed files with 124 additions and 34 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue