"feat: Added debug server and related components for improved development experience"

This commit is contained in:
2025-05-23 07:24:51 +02:00
parent d457a8d86b
commit 9f6219832c
189 changed files with 35730 additions and 133 deletions

View File

@@ -0,0 +1,29 @@
"use server";
import { lucia, validateRequest } from "@/server/auth";
import strings from "@/utils/strings";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
export async function logout(path?: string) {
const { session } = await validateRequest();
if (!session) {
return {
error: strings.ERROR.NO_SESSION,
};
}
try {
await lucia.invalidateSession(session.id);
} catch (error) {
return {
error: strings.ERROR.NO_SESSION,
};
}
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
revalidatePath(path ?? "/");
}

View File

@@ -0,0 +1,305 @@
"use server";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { printJobs, users } from "@/server/db/schema";
import { IS, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { type InferInsertModel, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function createPrintJob(printJob: InferInsertModel<typeof printJobs>) {
const { user } = await validateRequest();
if (guard(user, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
try {
const result = await db.insert(printJobs).values(printJob).returning({
jobId: printJobs.id,
});
return result[0].jobId;
} catch (error) {
return {
error: "Druckauftrag konnte nicht hinzugefügt werden.",
};
}
}
/* async function updatePrintJob(jobId: string, printJob: InferInsertModel<typeof printJobs>) {
const { user } = await validateRequest();
if (guard(user, is, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION
}
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, is, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION
}
}
await db.update(printJobs).set(printJob).where(eq(printJobs.id, jobId));
} */
export async function abortPrintJob(jobId: string, reason: string) {
const { user } = await validateRequest();
if (guard(user, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return {
error: "Druckauftrag nicht gefunden",
};
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get duration in minutes since startAt
const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);
await db
.update(printJobs)
.set({
aborted: true,
abortReason: reason,
durationInMinutes: duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag abgebrochen`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function earlyFinishPrintJob(jobId: string) {
const { user } = await validateRequest();
if (guard(user, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get duration in minutes since startAt
const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);
await db
.update(printJobs)
.set({
durationInMinutes: duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag vorzeitig abgeschlossen`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function extendPrintJob(jobId: string, minutes: number, hours: number) {
const { user } = await validateRequest();
if (guard(user, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
return {
error: strings.ERROR.PERMISSION,
};
}
const duration = minutes + hours * 60;
await db
.update(printJobs)
.set({
durationInMinutes: printJob.durationInMinutes + duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Verlängert um ${hours} Stunden und ${minutes} Minuten`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function updatePrintComments(jobId: string, comments: string) {
const { user } = await validateRequest();
if (guard(user, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS, UserRole.GUEST)) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
return {
error: strings.ERROR.PERMISSION,
};
}
await db
.update(printJobs)
.set({
comments,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}

View File

@@ -0,0 +1,130 @@
"use server";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { printers, users } from "@/server/db/schema";
import { IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { type InferInsertModel, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function createPrinter(printer: InferInsertModel<typeof printers>) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
if (!printer) {
return {
error: "Druckerdaten sind erforderlich.",
};
}
try {
await db.insert(printers).values(printer);
} catch (error) {
return {
error: "Drucker konnte nicht hinzugefügt werden.",
};
}
revalidatePath("/");
}
export async function updatePrinter(id: string, data: InferInsertModel<typeof printers>) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
if (!data) {
return {
error: "Druckerdaten sind erforderlich.",
};
}
try {
await db.update(printers).set(data).where(eq(printers.id, id));
} catch (error) {
return {
error: "Druckerdaten sind erforderlich.",
};
}
revalidatePath("/");
}
export async function deletePrinter(id: string) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
try {
await db.delete(printers).where(eq(printers.id, id));
} catch (error) {
if (error instanceof Error) {
return {
error: error.message,
};
}
return {
error: "Ein unbekannter Fehler ist aufgetreten.",
};
}
revalidatePath("/");
}
export async function getPrinters() {
return await db.query.printers.findMany({
with: {
printJobs: {
limit: 1,
orderBy: (printJobs, { desc }) => [desc(printJobs.startAt)],
},
},
});
}

View File

@@ -0,0 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
export async function revalidate() {
revalidatePath("/");
}

View File

@@ -0,0 +1,56 @@
"use server";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS, IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
/**
* Deletes a user from the database
* @param userId User ID to delete
* @param path Path to revalidate
*/
export async function deleteUser(userId: string, path?: string) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const targetUser = await db.query.users.findFirst({
where: eq(users.id, userId),
});
if (!targetUser) {
return {
error: "Benutzer nicht gefunden",
};
}
if (guard(targetUser, IS, UserRole.ADMIN)) {
return {
error: "Admins können nicht gelöscht werden.",
};
}
await db.delete(users).where(eq(users.id, userId));
revalidatePath(path ?? "/admin/users");
}

View File

@@ -0,0 +1,41 @@
import type { formSchema } from "@/app/admin/users/form";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import type { z } from "zod";
/**
* Updates a user in the database
* @param userId User ID to update
* @param data Updated user data
* @param path Path to revalidate
*/
export async function updateUser(userId: string, data: z.infer<typeof formSchema>, path?: string) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
await db.update(users).set(data).where(eq(users.id, userId));
revalidatePath(path ?? "/admin/users");
}

View File

@@ -0,0 +1,80 @@
"use server";
import type { formSchema } from "@/app/admin/users/form";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS, IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import type { z } from "zod";
/**
* @deprecated
*/
export async function updateUser(userId: string, data: z.infer<typeof formSchema>) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
await db.update(users).set(data).where(eq(users.id, userId));
revalidatePath("/admin/users");
}
/**
* @deprecated
*/
export async function deleteUser(userId: string) {
const { user } = await validateRequest();
if (guard(user, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const dbUser = await db.query.users.findFirst({
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
where: eq(users.id, user!.id),
});
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
return {
error: strings.ERROR.PERMISSION,
};
}
const targetUser = await db.query.users.findFirst({
where: eq(users.id, userId),
});
if (!targetUser) {
return { error: "Benutzer nicht gefunden" };
}
if (guard(targetUser, IS, UserRole.ADMIN)) {
return { error: "Kann keinen Admin löschen" };
}
await db.delete(users).where(eq(users.id, userId));
revalidatePath("/admin/users");
}

View File

@@ -0,0 +1,72 @@
import type { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { sessions, users } from "@/server/db/schema";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
import { Lucia, type RegisteredDatabaseUserAttributes, type Session } from "lucia";
import { cookies } from "next/headers";
import { cache } from "react";
//@ts-ignore
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
expires: false,
attributes: {
secure: process.env.NODE_ENV === "production",
},
},
getUserAttributes: (attributes) => {
return {
id: attributes.id,
username: attributes.username,
displayName: attributes.displayName,
email: attributes.email,
role: attributes.role,
};
},
});
export const validateRequest = cache(
async (): Promise<{ user: RegisteredDatabaseUserAttributes; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session?.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result as {
user: RegisteredDatabaseUserAttributes;
session: Session;
};
},
);
declare module "lucia" {
interface Register {
Lucia: typeof Lucia;
DatabaseUserAttributes: {
id: string;
github_id: number;
username: string;
displayName: string;
email: string;
role: UserRole;
};
}
}

View File

@@ -0,0 +1,54 @@
import { GitHub } from "arctic";
import { ALLOWED_CALLBACK_HOSTS, FRONTEND_URL, OAUTH_CALLBACK_URL } from "@/utils/api-config";
// Helper-Funktion, um die passende Callback-URL zu bestimmen
const getCallbackUrl = () => {
// Wenn eine spezifische OAuth-Callback-URL definiert ist, verwende diese
if (process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL) {
console.log("Verwende konfigurierte OAuth Callback URL:", process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL);
return process.env.NEXT_PUBLIC_OAUTH_CALLBACK_URL;
}
// Für spezifischen Unternehmens-Hostname
if (FRONTEND_URL.includes('corpintra.net') || FRONTEND_URL.includes('m040tbaraspi001')) {
const url = `http://m040tbaraspi001.de040.corpintra.net/auth/login/callback`;
console.log("Verwende Unternehmens-Hostname für OAuth Callback:", url);
return url;
}
// Fallback für lokale Entwicklung
console.log("Verwende Standard OAuth Callback URL:", OAUTH_CALLBACK_URL);
return OAUTH_CALLBACK_URL;
};
// Berechne die Callback-URL
export const USED_CALLBACK_URL = getCallbackUrl();
// Erstelle GitHub OAuth-Client mit expliziter Redirect-URI
export const github = new GitHub(
process.env.OAUTH_CLIENT_ID as string,
process.env.OAUTH_CLIENT_SECRET as string,
{
enterpriseDomain: "https://git.i.mercedes-benz.com",
redirectURI: USED_CALLBACK_URL,
}
);
// Hilfsfunktion zur Validierung von OAuth-Callbacks
export function isValidCallbackHost(url: string): boolean {
try {
const parsedUrl = new URL(url);
return ALLOWED_CALLBACK_HOSTS.some(host => parsedUrl.hostname === host ||
parsedUrl.hostname.includes(host));
} catch (e) {
console.error("Ungültige URL beim Validieren des Callback-Hosts:", url, e);
return false;
}
}
export interface GitHubUserResult {
id: number;
login: string;
name: string;
email: string;
}

View File

@@ -0,0 +1,28 @@
import type { RegisteredDatabaseUserAttributes } from "lucia";
export enum UserRole {
ADMIN = "admin",
USER = "user",
GUEST = "guest",
}
/**
* @deprecated
*/
export function hasRole(user: RegisteredDatabaseUserAttributes | null | undefined, role: UserRole) {
return user?.role === role;
}
/**
* @deprecated
*/
export function translateUserRole(role: UserRole) {
switch (role) {
case UserRole.ADMIN:
return "Administrator";
case UserRole.USER:
return "Benutzer";
case UserRole.GUEST:
return "Gast";
}
}