Kundenportal arbeiten

This commit is contained in:
2026-04-08 18:52:04 +02:00
parent d9e5df07bf
commit f125617af0
9 changed files with 1017 additions and 23 deletions

View File

@@ -11,6 +11,7 @@ import {
sql,
} from "drizzle-orm"
import { authProfiles } from "../../../db/schema";
import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions";
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
@@ -18,6 +19,9 @@ import { diffObjects } from "../../utils/diff";
import { recalculateServicePricesForTenant } from "../../modules/service-price-recalculation.service";
import { decrypt, encrypt } from "../../utils/crypt";
const PORTAL_ALLOWED_RESOURCES = new Set(["customers", "contracts", "createddocuments"])
const PORTAL_VISIBLE_DOCUMENT_TYPES = ["invoices", "advanceInvoices", "cancellationInvoices"]
// -------------------------------------------------------------
// SQL Suche auf mehreren Feldern (Haupttabelle + Relationen)
// -------------------------------------------------------------
@@ -130,6 +134,65 @@ function applyResourceWhereFilters(resource: string, table: any, whereCond: any)
return whereCond
}
async function getPortalCustomerId(server: FastifyInstance, req: any) {
const tenantId = req.user?.tenant_id
const userId = req.user?.user_id
if (!tenantId || !userId) return null
const [profile] = await server.db
.select({ customer_for_portal: authProfiles.customer_for_portal })
.from(authProfiles)
.where(and(
eq(authProfiles.tenant_id, tenantId),
eq(authProfiles.user_id, userId)
))
.limit(1)
return profile?.customer_for_portal || null
}
function applyPortalScope(resource: string, table: any, whereCond: any, portalCustomerId: number | null) {
if (!portalCustomerId) return whereCond
if (!PORTAL_ALLOWED_RESOURCES.has(resource)) {
return null
}
if (resource === "customers") {
return and(whereCond, eq(table.id, portalCustomerId))
}
if (resource === "contracts") {
return and(whereCond, eq(table.customer, portalCustomerId))
}
if (resource === "createddocuments") {
return and(
whereCond,
eq(table.customer, portalCustomerId),
eq(table.availableInPortal, true),
inArray(table.type, PORTAL_VISIBLE_DOCUMENT_TYPES)
)
}
return whereCond
}
function sanitizePortalCustomerUpdate(payload: Record<string, any>) {
const nextInfoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
return {
name: payload.name,
firstname: payload.firstname,
lastname: payload.lastname,
salutation: payload.salutation,
title: payload.title,
nameAddition: payload.nameAddition,
infoData: nextInfoData,
}
}
function getTenantColumn(resource: string, table: any) {
const config = resourceConfig[resource]
const tenantKey = config?.tenantKey || "tenant"
@@ -271,11 +334,16 @@ export default async function resourceRoutes(server: FastifyInstance) {
if (!config) {
return reply.code(404).send({ error: "Unknown resource" })
}
const portalCustomerId = await getPortalCustomerId(server, req)
if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
return reply.code(403).send({ error: "Forbidden" })
}
const table = config.table
const tenantColumn = getTenantColumn(resource, table)
let whereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined
whereCond = applyResourceWhereFilters(resource, table, whereCond)
whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)
let q = server.db.select().from(table).$dynamic()
const searchCols: any[] = (config.searchColumns || []).map(c => table[c])
@@ -380,6 +448,10 @@ export default async function resourceRoutes(server: FastifyInstance) {
if (!config) {
return reply.code(404).send({ error: "Unknown resource" });
}
const portalCustomerId = await getPortalCustomerId(server, req)
if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
return reply.code(403).send({ error: "Forbidden" })
}
const table = config.table;
const { queryConfig } = req;
@@ -389,6 +461,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
const tenantColumn = getTenantColumn(resource, table);
let whereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined;
whereCond = applyResourceWhereFilters(resource, table, whereCond)
whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)
const searchCols: any[] = (config.searchColumns || []).map(c => table[c]);
const debugSearchColumnNames: string[] = [...(config.searchColumns || [])];
const parsedFilters: Array<{ key: string; value: any }> = []
@@ -489,6 +562,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
}
let distinctWhereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined
distinctWhereCond = applyResourceWhereFilters(resource, table, distinctWhereCond)
distinctWhereCond = applyPortalScope(resource, table, distinctWhereCond, portalCustomerId)
if (search) {
const searchCond = buildSearchCondition(searchCols, search.trim())
@@ -570,10 +644,15 @@ export default async function resourceRoutes(server: FastifyInstance) {
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { resource, no_relations } = req.params as { resource: string, no_relations?: boolean }
const portalCustomerId = await getPortalCustomerId(server, req)
if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
return reply.code(403).send({ error: "Forbidden" })
}
const table = resourceConfig[resource].table
let whereCond: any = and(eq(table.id, id), eq(table.tenant, tenantId))
whereCond = applyResourceWhereFilters(resource, table, whereCond)
whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)
const projRows = await server.db
.select()
@@ -624,6 +703,10 @@ export default async function resourceRoutes(server: FastifyInstance) {
try {
if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" });
const { resource } = req.params as { resource: string };
const portalCustomerId = await getPortalCustomerId(server, req)
if (portalCustomerId) {
return reply.code(403).send({ error: "Forbidden" })
}
if (resource === "accounts") {
return reply.code(403).send({ error: "Accounts are read-only" })
}
@@ -703,8 +786,12 @@ export default async function resourceRoutes(server: FastifyInstance) {
const body = req.body as Record<string, any>
const tenantId = req.user?.tenant_id
const userId = req.user?.user_id
const portalCustomerId = await getPortalCustomerId(server, req)
if (!tenantId || !userId) return reply.code(401).send({ error: "Unauthorized" })
if (portalCustomerId && resource !== "customers") {
return reply.code(403).send({ error: "Forbidden" })
}
const table = resourceConfig[resource].table
const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }
@@ -712,13 +799,25 @@ export default async function resourceRoutes(server: FastifyInstance) {
const [oldRecord] = await server.db
.select()
.from(table)
.where(and(eq(table.id, id), eq(table.tenant, tenantId)))
.where(applyPortalScope(resource, table, and(eq(table.id, id), eq(table.tenant, tenantId)), portalCustomerId))
.limit(1)
if (!oldRecord) {
return reply.code(404).send({ error: "Resource not found" })
}
let data: Record<string, any> = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
//@ts-ignore
delete data.updatedBy; delete data.updatedAt;
if (portalCustomerId) {
data = {
...sanitizePortalCustomerUpdate(data),
updated_at: data.updated_at,
updated_by: data.updated_by,
}
}
if (resource === "members") {
data = normalizeMemberPayload(data)
const validationError = validateMemberPayload(data)