feat: add client-side validation utilities and debugging tools
Build and Push Docker Image / build (push) Successful in 1m23s
Build and Push Docker Image / build (push) Successful in 1m23s
- Implemented client-side validation functions for tax ID, VAT ID, IBAN, BIC, and website URL. - Added debug logging functionality to assist in development. - Created a comprehensive validation function for company form data. feat: initialize database with Prisma migrations - Added a server-side script to run Prisma migrations and check database health. - Ensured safe initialization of the database to prevent concurrent migrations. feat: comprehensive server-side error logging - Developed an error logging system that captures detailed error context, including request details and stack traces. - Implemented logging functions for different error types (route, action, database, API, startup). fix: validate user ID existence in audit logs - Updated the logging function to validate that the user ID exists in the database before logging actions. fix: update schemas for optional fields and validation - Modified schemas to allow for nullable fields and refined validation logic for tax ID, VAT ID, IBAN, and BIC. feat: enhance error boundary for better debugging - Improved error boundary to log detailed error information in development mode. - Added a debug panel to the main application layout for real-time error tracking. feat: implement company deletion functionality in admin routes - Added a new API route for deleting companies with appropriate logging. - Integrated delete confirmation in the admin interface for better user experience. fix: handle API errors gracefully - Wrapped API actions in try-catch blocks to log errors and return appropriate responses. feat: generate and save invoice PDFs - Implemented functionality to generate and save invoice PDFs upon status updates. - Added a new column in the database for storing the URL of the generated PDF. chore: update Docker image reference - Changed the Docker image reference to point to the new Git repository. chore: update package dependencies - Added @radix-ui/react-tooltip for enhanced UI components. - Updated package-lock.json to reflect new dependencies.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Error Logging Usage Examples
|
||||
*
|
||||
* Use these patterns in your routes to get better error debugging
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// PATTERN 1: In a loader (data fetching)
|
||||
// ============================================================================
|
||||
|
||||
import { json, type LoaderFunction } from "react-router";
|
||||
import { logRouteError } from "@/lib/error-logger.server";
|
||||
import prisma from "@/lib/prisma.server";
|
||||
|
||||
export const loader: LoaderFunction = async ({ request, params }) => {
|
||||
try {
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id: params.id },
|
||||
});
|
||||
|
||||
if (!company) {
|
||||
throw new Error(`Company not found: ${params.id}`);
|
||||
}
|
||||
|
||||
return json({ company });
|
||||
} catch (error) {
|
||||
logRouteError(error, {
|
||||
request,
|
||||
route: request.url,
|
||||
userId: params.userId, // if available
|
||||
metadata: {
|
||||
companyId: params.id,
|
||||
},
|
||||
});
|
||||
throw error; // Re-throw to let React Router handle it
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PATTERN 2: In an action (mutation/form submission)
|
||||
// ============================================================================
|
||||
|
||||
import { logActionError } from "@/lib/error-logger.server";
|
||||
|
||||
export const action: LoaderFunction = async ({ request, params }) => {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const result = await prisma.company.update({
|
||||
where: { id: params.id },
|
||||
data: { name: formData.get("name") },
|
||||
});
|
||||
return json({ success: true, result });
|
||||
} catch (error) {
|
||||
logActionError(error, {
|
||||
request,
|
||||
action: "UPDATE_COMPANY",
|
||||
metadata: {
|
||||
companyId: params.id,
|
||||
method: request.method,
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PATTERN 3: In an API route (POST/PUT/DELETE)
|
||||
// ============================================================================
|
||||
|
||||
import { logApiError } from "@/lib/error-logger.server";
|
||||
|
||||
export const action: LoaderFunction = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
// Validate
|
||||
if (!data.name) {
|
||||
return json(
|
||||
{ error: "Name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Process
|
||||
const result = await prisma.company.create({ data });
|
||||
return json({ success: true, result }, { status: 201 });
|
||||
} catch (error) {
|
||||
logApiError(error, {
|
||||
request,
|
||||
endpoint: "/api/companies",
|
||||
statusCode: 500,
|
||||
metadata: {
|
||||
method: request.method,
|
||||
},
|
||||
});
|
||||
return json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PATTERN 4: Database operations with error context
|
||||
// ============================================================================
|
||||
|
||||
import { logDatabaseError } from "@/lib/error-logger.server";
|
||||
|
||||
async function fetchCompanyWithUsers(companyId: string) {
|
||||
try {
|
||||
return await prisma.company.findUnique({
|
||||
where: { id: companyId },
|
||||
include: { users: true },
|
||||
});
|
||||
} catch (error) {
|
||||
logDatabaseError(error, "company.findUnique", {
|
||||
metadata: { companyId },
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OUTPUT EXAMPLE (Server Console)
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
[ROUTE_ERROR] | 2026-05-02T22:10:30.123Z | Company not found | route: /companies/123 | GET /companies/123 | user: user-id-456
|
||||
Stack at Object.loader (file:///app/routes/companies.$id.tsx:12:13)
|
||||
at process._tickCallback (internal/timers.ts:203:26)
|
||||
Metadata: {
|
||||
"companyId": "123"
|
||||
}
|
||||
================================================================================
|
||||
*/
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Client-side validation and debugging utilities
|
||||
* Mirrors server-side validation for real-time user feedback
|
||||
*/
|
||||
|
||||
// Debug mode - enable via localStorage: localStorage.setItem('DEBUG_MODE', 'true')
|
||||
export function isDebugMode(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
return localStorage.getItem("DEBUG_MODE") === "true";
|
||||
}
|
||||
|
||||
export function setDebugMode(enabled: boolean): void {
|
||||
if (typeof window === "undefined") return;
|
||||
if (enabled) {
|
||||
localStorage.setItem("DEBUG_MODE", "true");
|
||||
console.log("✅ DEBUG MODE ENABLED");
|
||||
} else {
|
||||
localStorage.removeItem("DEBUG_MODE");
|
||||
console.log("❌ DEBUG MODE DISABLED");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error to browser console (only in debug mode)
|
||||
*/
|
||||
export function debugLog(type: string, message: string, data?: unknown): void {
|
||||
if (!isDebugMode()) return;
|
||||
|
||||
const style = {
|
||||
error: "color: #ef4444; font-weight: bold;",
|
||||
warning: "color: #f97316; font-weight: bold;",
|
||||
info: "color: #3b82f6; font-weight: bold;",
|
||||
success: "color: #22c55e; font-weight: bold;",
|
||||
};
|
||||
|
||||
const typeStyle = style[type as keyof typeof style] || style.info;
|
||||
console.log(`%c[${type.toUpperCase()}] ${message}`, typeStyle, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error type
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tax ID (Steuernummer): 10 digits
|
||||
*/
|
||||
export function validateTaxId(val: string | null | undefined): ValidationError | null {
|
||||
if (!val || val === "") return null;
|
||||
if (!/^\d{10}$/.test(val)) {
|
||||
return { field: "taxId", message: "Steuernummer muss 10 Ziffern haben" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate VAT ID (USt-IdNr): DE + 9 digits
|
||||
*/
|
||||
export function validateVatId(val: string | null | undefined): ValidationError | null {
|
||||
if (!val || val === "") return null;
|
||||
if (!/^DE\d{9}$/.test(val)) {
|
||||
return { field: "vatId", message: "USt-IdNr. muss im Format DE + 9 Ziffern sein" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IBAN
|
||||
*/
|
||||
export function validateIban(val: string | null | undefined): ValidationError | null {
|
||||
if (!val || val === "") return null;
|
||||
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(val)) {
|
||||
return { field: "bankIban", message: "Ungültige IBAN" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BIC
|
||||
*/
|
||||
export function validateBic(val: string | null | undefined): ValidationError | null {
|
||||
if (!val || val === "") return null;
|
||||
if (!/^[A-Z0-9]{8,11}$/.test(val)) {
|
||||
return { field: "bankBic", message: "Ungültiger BIC" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate website URL
|
||||
*/
|
||||
export function validateWebsite(val: string | null | undefined): ValidationError | null {
|
||||
if (!val || val === "") return null;
|
||||
if (!/^https?:\/\//.test(val)) {
|
||||
return { field: "website", message: "Website muss mit http:// oder https:// beginnen" };
|
||||
}
|
||||
if (val.length > 255) {
|
||||
return { field: "website", message: "Website darf maximal 255 Zeichen sein" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate company form data
|
||||
*/
|
||||
export interface CompanyFormData {
|
||||
name: string;
|
||||
address: string;
|
||||
zip: string;
|
||||
city: string;
|
||||
taxId?: string;
|
||||
vatId?: string;
|
||||
website?: string;
|
||||
bankIban?: string;
|
||||
bankBic?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
legalForm?: string;
|
||||
country?: string;
|
||||
bankName?: string;
|
||||
}
|
||||
|
||||
export function validateCompanyForm(
|
||||
data: CompanyFormData
|
||||
): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// Required fields
|
||||
if (!data.name || data.name.trim() === "") {
|
||||
errors.push({ field: "name", message: "Firmenname erforderlich" });
|
||||
} else if (data.name.length > 255) {
|
||||
errors.push({ field: "name", message: "Firmenname darf maximal 255 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (!data.address || data.address.trim() === "") {
|
||||
errors.push({ field: "address", message: "Adresse erforderlich" });
|
||||
} else if (data.address.length > 500) {
|
||||
errors.push({ field: "address", message: "Adresse darf maximal 500 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (!data.zip || data.zip.trim() === "") {
|
||||
errors.push({ field: "zip", message: "PLZ erforderlich" });
|
||||
} else if (!/^[\d\s-]*$/.test(data.zip)) {
|
||||
errors.push({ field: "zip", message: "PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten" });
|
||||
} else if (data.zip.length > 20) {
|
||||
errors.push({ field: "zip", message: "PLZ darf maximal 20 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (!data.city || data.city.trim() === "") {
|
||||
errors.push({ field: "city", message: "Stadt erforderlich" });
|
||||
} else if (data.city.length > 100) {
|
||||
errors.push({ field: "city", message: "Stadt darf maximal 100 Zeichen sein" });
|
||||
}
|
||||
|
||||
// Optional fields with validation
|
||||
if (data.taxId) {
|
||||
const taxError = validateTaxId(data.taxId);
|
||||
if (taxError) errors.push(taxError);
|
||||
}
|
||||
|
||||
if (data.vatId) {
|
||||
const vatError = validateVatId(data.vatId);
|
||||
if (vatError) errors.push(vatError);
|
||||
}
|
||||
|
||||
if (data.website) {
|
||||
const webError = validateWebsite(data.website);
|
||||
if (webError) errors.push(webError);
|
||||
}
|
||||
|
||||
if (data.bankIban) {
|
||||
const ibanError = validateIban(data.bankIban);
|
||||
if (ibanError) errors.push(ibanError);
|
||||
}
|
||||
|
||||
if (data.bankBic) {
|
||||
const bicError = validateBic(data.bankBic);
|
||||
if (bicError) errors.push(bicError);
|
||||
}
|
||||
|
||||
if (data.email && data.email.trim()) {
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
errors.push({ field: "email", message: "Ungültige E-Mail-Adresse" });
|
||||
}
|
||||
}
|
||||
|
||||
if (data.phone && data.phone.length > 20) {
|
||||
errors.push({ field: "phone", message: "Telefonnummer darf maximal 20 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (data.legalForm && data.legalForm.length > 100) {
|
||||
errors.push({ field: "legalForm", message: "Rechtsform darf maximal 100 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (data.country && data.country.length > 2) {
|
||||
errors.push({ field: "country", message: "Ländercode darf maximal 2 Zeichen sein" });
|
||||
}
|
||||
|
||||
if (data.bankName && data.bankName.length > 255) {
|
||||
errors.push({ field: "bankName", message: "Bankname darf maximal 255 Zeichen sein" });
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
if (errors.length > 0) {
|
||||
debugLog("warning", `Validation failed: ${errors.length} error(s)`, errors);
|
||||
} else {
|
||||
debugLog("success", "Validation passed", data);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API response errors
|
||||
*/
|
||||
export function handleApiError(error: unknown, endpoint: string): void {
|
||||
debugLog("error", `API Error from ${endpoint}`, error);
|
||||
|
||||
if (error instanceof Response) {
|
||||
debugLog("error", `HTTP ${error.status}: ${error.statusText}`, error);
|
||||
} else if (error instanceof Error) {
|
||||
debugLog("error", error.message, error.stack);
|
||||
} else {
|
||||
debugLog("error", "Unknown error", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message for a specific field
|
||||
*/
|
||||
export function getFieldError(field: string, errors: ValidationError[]): string | null {
|
||||
const error = errors.find((e) => e.field === field);
|
||||
return error?.message || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field has error
|
||||
*/
|
||||
export function hasFieldError(field: string, errors: ValidationError[]): boolean {
|
||||
return errors.some((e) => e.field === field);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import prisma from "./prisma.server";
|
||||
|
||||
let initStarted = false;
|
||||
let initCompleted = false;
|
||||
|
||||
/**
|
||||
* Run Prisma migrations to bring database schema up to date
|
||||
*/
|
||||
async function runMigrations(): Promise<void> {
|
||||
try {
|
||||
console.log("[DB Init] Running Prisma migrations...");
|
||||
execSync("npx prisma migrate deploy", {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env },
|
||||
});
|
||||
console.log("[DB Init] ✓ Migrations completed successfully");
|
||||
} catch (error) {
|
||||
console.error("[DB Init] ✗ Migration failed:", error);
|
||||
throw new Error("Database migration failed. See logs above.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database: run all pending migrations
|
||||
* Safe to call multiple times (idempotent)
|
||||
*/
|
||||
export async function initializeDatabase(): Promise<void> {
|
||||
// Prevent concurrent initialization attempts
|
||||
if (initCompleted) return;
|
||||
if (initStarted) {
|
||||
// Wait for initialization to complete
|
||||
while (!initCompleted) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
initStarted = true;
|
||||
|
||||
try {
|
||||
await runMigrations();
|
||||
console.log("[DB Init] ✓ Database initialization complete");
|
||||
} catch (error) {
|
||||
console.error("[DB Init] Fatal error during database initialization:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
initCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check database health (non-blocking)
|
||||
*/
|
||||
export async function checkDatabaseHealth(): Promise<{
|
||||
connected: boolean;
|
||||
isEmpty: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const userCount = await prisma.user.count();
|
||||
return {
|
||||
connected: true,
|
||||
isEmpty: userCount === 0,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
connected: false,
|
||||
isEmpty: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Comprehensive server-side error logging for debugging
|
||||
* Captures errors with full context: stack traces, request details, environment
|
||||
*/
|
||||
|
||||
export interface ErrorContext {
|
||||
request?: Request;
|
||||
userId?: string | null;
|
||||
route?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ErrorLogEntry {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
context?: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
route?: string;
|
||||
userId?: string | null;
|
||||
ipAddress?: string | null;
|
||||
userAgent?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
environment: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message and stack from any error type
|
||||
*/
|
||||
function extractErrorInfo(error: unknown): { message: string; stack?: string } {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return { message: error };
|
||||
}
|
||||
|
||||
if (error && typeof error === "object") {
|
||||
const err = error as Record<string, unknown>;
|
||||
return {
|
||||
message: (err.message as string) || String(error),
|
||||
stack: (err.stack as string) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return { message: String(error) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract request context
|
||||
*/
|
||||
function extractRequestContext(
|
||||
request?: Request
|
||||
): ErrorLogEntry["context"] {
|
||||
if (!request) return {};
|
||||
|
||||
const url = new URL(request.url);
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for") ??
|
||||
request.headers.get("x-real-ip") ??
|
||||
null;
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
|
||||
return {
|
||||
method: request.method,
|
||||
url: url.pathname + url.search,
|
||||
ipAddress,
|
||||
userAgent: userAgent ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build comprehensive error log entry
|
||||
*/
|
||||
function buildErrorLogEntry(
|
||||
type: string,
|
||||
error: unknown,
|
||||
context?: ErrorContext
|
||||
): ErrorLogEntry {
|
||||
const { message, stack } = extractErrorInfo(error);
|
||||
const requestContext = extractRequestContext(context?.request);
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
message,
|
||||
stack,
|
||||
context: {
|
||||
...requestContext,
|
||||
route: context?.route,
|
||||
userId: context?.userId,
|
||||
metadata: context?.metadata,
|
||||
},
|
||||
environment: process.env.NODE_ENV || "development",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error log for console output
|
||||
*/
|
||||
function formatErrorLog(entry: ErrorLogEntry): string {
|
||||
const parts = [
|
||||
`[${entry.type}]`,
|
||||
`${entry.timestamp}`,
|
||||
entry.message,
|
||||
];
|
||||
|
||||
if (entry.context?.route) parts.push(`route: ${entry.context.route}`);
|
||||
if (entry.context?.method && entry.context?.url) {
|
||||
parts.push(`${entry.context.method} ${entry.context.url}`);
|
||||
}
|
||||
if (entry.context?.userId) parts.push(`user: ${entry.context.userId}`);
|
||||
if (entry.context?.ipAddress) parts.push(`ip: ${entry.context.ipAddress}`);
|
||||
|
||||
let output = parts.join(" | ");
|
||||
|
||||
if (entry.stack) {
|
||||
output += "\n" + entry.stack;
|
||||
}
|
||||
|
||||
if (entry.context?.metadata) {
|
||||
output += "\nMetadata: " + JSON.stringify(entry.context.metadata, null, 2);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a route/loader error
|
||||
*/
|
||||
export function logRouteError(
|
||||
error: unknown,
|
||||
context: ErrorContext & { route: string }
|
||||
): void {
|
||||
const entry = buildErrorLogEntry("ROUTE_ERROR", error, context);
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an action/mutation error
|
||||
*/
|
||||
export function logActionError(
|
||||
error: unknown,
|
||||
context: ErrorContext & { action: string }
|
||||
): void {
|
||||
const entry = buildErrorLogEntry("ACTION_ERROR", error, {
|
||||
...context,
|
||||
metadata: {
|
||||
action: context.action,
|
||||
...context.metadata,
|
||||
},
|
||||
});
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a database error
|
||||
*/
|
||||
export function logDatabaseError(
|
||||
error: unknown,
|
||||
operation: string,
|
||||
context?: Omit<ErrorContext, "metadata">
|
||||
): void {
|
||||
const entry = buildErrorLogEntry("DATABASE_ERROR", error, {
|
||||
...context,
|
||||
metadata: { operation },
|
||||
});
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an API error
|
||||
*/
|
||||
export function logApiError(
|
||||
error: unknown,
|
||||
context: ErrorContext & { endpoint: string; statusCode?: number }
|
||||
): void {
|
||||
const entry = buildErrorLogEntry("API_ERROR", error, {
|
||||
...context,
|
||||
metadata: {
|
||||
endpoint: context.endpoint,
|
||||
statusCode: context.statusCode || 500,
|
||||
...context.metadata,
|
||||
},
|
||||
});
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a server startup error
|
||||
*/
|
||||
export function logStartupError(error: unknown): void {
|
||||
const entry = buildErrorLogEntry("STARTUP_ERROR", error);
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error("🚨 CRITICAL: Server failed to start");
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a generic error with type
|
||||
*/
|
||||
export function logError(
|
||||
type: string,
|
||||
error: unknown,
|
||||
context?: ErrorContext
|
||||
): void {
|
||||
const entry = buildErrorLogEntry(type, error, context);
|
||||
console.error("\n" + "=".repeat(80));
|
||||
console.error(formatErrorLog(entry));
|
||||
console.error("=".repeat(80) + "\n");
|
||||
}
|
||||
@@ -47,9 +47,21 @@ export async function log({
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
// Validate that userId exists in the database if provided
|
||||
let validatedUserId = userId;
|
||||
if (userId) {
|
||||
const userExists = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!userExists) {
|
||||
validatedUserId = null; // User doesn't exist, log as anonymous
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: userId ?? null,
|
||||
userId: validatedUserId ?? null,
|
||||
action,
|
||||
entity: entity ?? null,
|
||||
entityId: entityId ?? null,
|
||||
|
||||
+24
-10
@@ -35,7 +35,7 @@ export const taxRateSchema = z
|
||||
export const ibanSchema = z
|
||||
.string()
|
||||
.refine(
|
||||
(iban) => /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(iban),
|
||||
(iban) => iban === "" || /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(iban),
|
||||
"Ungültige IBAN"
|
||||
);
|
||||
|
||||
@@ -44,14 +44,20 @@ export const ibanSchema = z
|
||||
*/
|
||||
export const taxIdSchema = z
|
||||
.string()
|
||||
.regex(/^\d{10}$/, "Steuernummer muss 10 Ziffern haben");
|
||||
.refine(
|
||||
(val) => val === "" || /^\d{10}$/.test(val),
|
||||
"Steuernummer muss 10 Ziffern haben"
|
||||
);
|
||||
|
||||
/**
|
||||
* VAT ID (Umsatzsteuer-IdNr): DE + 9 digits
|
||||
*/
|
||||
export const vatIdSchema = z
|
||||
.string()
|
||||
.regex(/^DE\d{9}$/, "USt-IdNr. muss im Format DE + 9 Ziffern sein");
|
||||
.refine(
|
||||
(val) => val === "" || /^DE\d{9}$/.test(val),
|
||||
"USt-IdNr. muss im Format DE + 9 Ziffern sein"
|
||||
);
|
||||
|
||||
// ===== Invoice Schemas =====
|
||||
|
||||
@@ -131,8 +137,8 @@ export const companySchema = z.object({
|
||||
.string()
|
||||
.max(100, "Rechtsform darf maximal 100 Zeichen sein")
|
||||
.optional(),
|
||||
taxId: taxIdSchema.optional(),
|
||||
vatId: vatIdSchema.optional(),
|
||||
taxId: taxIdSchema.nullable(),
|
||||
vatId: vatIdSchema.nullable(),
|
||||
address: z
|
||||
.string()
|
||||
.min(1, "Adresse erforderlich")
|
||||
@@ -162,14 +168,22 @@ export const companySchema = z.object({
|
||||
.optional(),
|
||||
website: z
|
||||
.string()
|
||||
.url("Ungültige URL")
|
||||
.refine(
|
||||
(val) => val === "" || /^https?:\/\//.test(val),
|
||||
"Website muss mit http:// oder https:// beginnen"
|
||||
)
|
||||
.max(255, "Website darf maximal 255 Zeichen sein")
|
||||
.optional(),
|
||||
bankIban: ibanSchema.optional(),
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
bankIban: ibanSchema.nullable(),
|
||||
bankBic: z
|
||||
.string()
|
||||
.regex(/^[A-Z0-9]{8,11}$/, "Ungültiger BIC")
|
||||
.optional(),
|
||||
.refine(
|
||||
(val) => val === "" || /^[A-Z0-9]{8,11}$/.test(val),
|
||||
"Ungültiger BIC"
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
bankName: z
|
||||
.string()
|
||||
.max(255, "Bankname darf maximal 255 Zeichen sein")
|
||||
|
||||
Reference in New Issue
Block a user