Kundenportal arbeiten
This commit is contained in:
@@ -4,6 +4,7 @@ import { and, eq, inArray, isNull } from "drizzle-orm";
|
||||
import {
|
||||
authTenantUsers,
|
||||
authProfiles,
|
||||
customers,
|
||||
authRoles,
|
||||
authUserRoles,
|
||||
authUsers,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
tenants,
|
||||
} from "../../db/schema";
|
||||
import { generateRandomPassword, hashPassword } from "../utils/password";
|
||||
import { sendMail } from "../utils/mailer";
|
||||
|
||||
export default async function adminRoutes(server: FastifyInstance) {
|
||||
const deriveNameFromEmail = (email: string) => {
|
||||
@@ -255,6 +257,33 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
const ensurePortalRoleForTenant = async (tenantId: number, createdBy: string) => {
|
||||
const existingRoles = await server.db
|
||||
.select({
|
||||
id: authRoles.id,
|
||||
name: authRoles.name,
|
||||
})
|
||||
.from(authRoles)
|
||||
.where(eq(authRoles.tenant_id, tenantId));
|
||||
|
||||
const portalRole = existingRoles.find((role) => role.name === "Kundenportal");
|
||||
if (portalRole) return portalRole.id;
|
||||
|
||||
const [createdRole] = await server.db
|
||||
.insert(authRoles)
|
||||
.values({
|
||||
name: "Kundenportal",
|
||||
description: "Automatisch angelegte Rolle für eingeladene Kundenportal-Benutzer",
|
||||
tenant_id: tenantId,
|
||||
created_by: createdBy,
|
||||
})
|
||||
.returning({
|
||||
id: authRoles.id,
|
||||
});
|
||||
|
||||
return createdRole.id;
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /admin/overview
|
||||
// -------------------------------------------------------------
|
||||
@@ -422,6 +451,233 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
server.post("/admin/customers/:customerId/invite-portal-user", async (req, reply) => {
|
||||
try {
|
||||
const currentUser = await requireAdmin(req, reply);
|
||||
if (!currentUser) return;
|
||||
|
||||
const tenantId = Number(req.user?.tenant_id);
|
||||
const { customerId } = req.params as { customerId: string };
|
||||
|
||||
if (!tenantId) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const [tenantRecord] = await server.db
|
||||
.select({
|
||||
id: tenants.id,
|
||||
name: tenants.name,
|
||||
portalDomain: tenants.portalDomain,
|
||||
})
|
||||
.from(tenants)
|
||||
.where(eq(tenants.id, tenantId))
|
||||
.limit(1);
|
||||
|
||||
const [customerRecord] = await server.db
|
||||
.select()
|
||||
.from(customers)
|
||||
.where(and(eq(customers.id, Number(customerId)), eq(customers.tenant, tenantId)))
|
||||
.limit(1);
|
||||
|
||||
if (!customerRecord) {
|
||||
return reply.code(404).send({ error: "Customer not found" });
|
||||
}
|
||||
|
||||
const customerInfo = customerRecord.infoData && typeof customerRecord.infoData === "object" ? customerRecord.infoData as Record<string, any> : {};
|
||||
const email = String(customerInfo.email || customerInfo.invoiceEmail || "").trim().toLowerCase();
|
||||
|
||||
if (!email) {
|
||||
return reply.code(400).send({ error: "Customer has no email address" });
|
||||
}
|
||||
|
||||
const generatedPassword = generateRandomPassword(14);
|
||||
const passwordHash = await hashPassword(generatedPassword);
|
||||
|
||||
const [existingUser] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
is_admin: authUsers.is_admin,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.email, email))
|
||||
.limit(1);
|
||||
|
||||
const derivedName = deriveNameFromEmail(email);
|
||||
const firstName = customerRecord.firstname?.trim() || derivedName.first_name;
|
||||
const lastName = customerRecord.lastname?.trim() || derivedName.last_name;
|
||||
|
||||
let userId = existingUser?.id || null;
|
||||
let createdNewUser = false;
|
||||
|
||||
if (existingUser) {
|
||||
const [existingProfile] = await server.db
|
||||
.select({
|
||||
id: authProfiles.id,
|
||||
customer_for_portal: authProfiles.customer_for_portal,
|
||||
})
|
||||
.from(authProfiles)
|
||||
.where(and(
|
||||
eq(authProfiles.user_id, existingUser.id),
|
||||
eq(authProfiles.tenant_id, tenantId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.is_admin) {
|
||||
return reply.code(409).send({ error: "Email address is already used by an admin user" });
|
||||
}
|
||||
|
||||
if (!existingProfile) {
|
||||
return reply.code(409).send({ error: "Email address is already used by another user" });
|
||||
}
|
||||
|
||||
if (existingProfile.customer_for_portal && existingProfile.customer_for_portal !== customerRecord.id) {
|
||||
return reply.code(409).send({ error: "Email address is already assigned to another portal customer" });
|
||||
}
|
||||
|
||||
await server.db
|
||||
.update(authUsers)
|
||||
.set({
|
||||
passwordHash,
|
||||
must_change_password: true,
|
||||
multiTenant: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(authUsers.id, existingUser.id));
|
||||
|
||||
userId = existingUser.id;
|
||||
} else {
|
||||
const [createdUser] = await server.db
|
||||
.insert(authUsers)
|
||||
.values({
|
||||
email,
|
||||
passwordHash,
|
||||
is_admin: false,
|
||||
multiTenant: false,
|
||||
must_change_password: true,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning({
|
||||
id: authUsers.id,
|
||||
});
|
||||
|
||||
userId = createdUser.id;
|
||||
createdNewUser = true;
|
||||
}
|
||||
|
||||
const portalRoleId = await ensurePortalRoleForTenant(tenantId, currentUser.id);
|
||||
|
||||
const existingMemberships = await server.db
|
||||
.select()
|
||||
.from(authTenantUsers)
|
||||
.where(and(
|
||||
eq(authTenantUsers.user_id, userId!),
|
||||
eq(authTenantUsers.tenant_id, tenantId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!existingMemberships.length) {
|
||||
await server.db
|
||||
.insert(authTenantUsers)
|
||||
.values({
|
||||
tenant_id: tenantId,
|
||||
user_id: userId!,
|
||||
created_by: currentUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
const existingPortalRoleAssignment = await server.db
|
||||
.select()
|
||||
.from(authUserRoles)
|
||||
.where(and(
|
||||
eq(authUserRoles.user_id, userId!),
|
||||
eq(authUserRoles.tenant_id, tenantId),
|
||||
eq(authUserRoles.role_id, portalRoleId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!existingPortalRoleAssignment.length) {
|
||||
await server.db
|
||||
.insert(authUserRoles)
|
||||
.values({
|
||||
user_id: userId!,
|
||||
tenant_id: tenantId,
|
||||
role_id: portalRoleId,
|
||||
created_by: currentUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
const [existingTenantProfile] = await server.db
|
||||
.select({
|
||||
id: authProfiles.id,
|
||||
user_id: authProfiles.user_id,
|
||||
customer_for_portal: authProfiles.customer_for_portal,
|
||||
})
|
||||
.from(authProfiles)
|
||||
.where(and(
|
||||
eq(authProfiles.user_id, userId!),
|
||||
eq(authProfiles.tenant_id, tenantId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existingTenantProfile) {
|
||||
await server.db
|
||||
.update(authProfiles)
|
||||
.set({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
customer_for_portal: customerRecord.id,
|
||||
active: true,
|
||||
})
|
||||
.where(eq(authProfiles.id, existingTenantProfile.id));
|
||||
} else {
|
||||
await server.db
|
||||
.insert(authProfiles)
|
||||
.values({
|
||||
user_id: userId!,
|
||||
tenant_id: tenantId,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
customer_for_portal: customerRecord.id,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
const portalUrl = tenantRecord?.portalDomain ? `https://${tenantRecord.portalDomain}/login` : null;
|
||||
|
||||
const mailResult = await sendMail(
|
||||
email,
|
||||
`FEDEO | Einladung ins Kundenportal`,
|
||||
`
|
||||
<p>Hallo${customerRecord.name ? ` ${customerRecord.name}` : ""},</p>
|
||||
<p>für Sie wurde ein Zugang zum FEDEO Kundenportal eingerichtet.</p>
|
||||
<p><strong>E-Mail:</strong> ${email}</p>
|
||||
<p><strong>Initialpasswort:</strong> ${generatedPassword}</p>
|
||||
<p>Bitte ändern Sie dieses Passwort direkt nach dem ersten Login.</p>
|
||||
${portalUrl ? `<p><strong>Login:</strong> <a href="${portalUrl}">${portalUrl}</a></p>` : ""}
|
||||
<p>Viele Grüße<br>${tenantRecord?.name || "FEDEO"}</p>
|
||||
`
|
||||
);
|
||||
|
||||
if (!mailResult.success) {
|
||||
return reply.code(500).send({ error: "Invitation email could not be sent" });
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
createdNewUser,
|
||||
email,
|
||||
initialPassword: generatedPassword,
|
||||
portalUrl,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("ERROR /admin/customers/:customerId/invite-portal-user:", err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POST /admin/tenants
|
||||
// -------------------------------------------------------------
|
||||
|
||||
@@ -10,7 +10,9 @@ import { secrets } from "../utils/secrets"
|
||||
import { saveFile } from "../utils/files"
|
||||
|
||||
import { eq, inArray } from "drizzle-orm"
|
||||
import { and } from "drizzle-orm"
|
||||
import {
|
||||
authProfiles,
|
||||
files,
|
||||
createddocuments,
|
||||
customers
|
||||
@@ -18,6 +20,55 @@ import {
|
||||
|
||||
|
||||
export default async function fileRoutes(server: FastifyInstance) {
|
||||
const getPortalCustomerId = async (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
|
||||
}
|
||||
|
||||
const loadSingleFileForRequest = async (req: any, id: string) => {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return null
|
||||
|
||||
const portalCustomerId = await getPortalCustomerId(req)
|
||||
|
||||
if (!portalCustomerId) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(and(eq(files.id, id), eq(files.tenant, tenantId)))
|
||||
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
const rows = await server.db
|
||||
.select({
|
||||
file: files,
|
||||
})
|
||||
.from(files)
|
||||
.leftJoin(createddocuments, eq(files.createddocument, createddocuments.id))
|
||||
.where(and(
|
||||
eq(files.id, id),
|
||||
eq(files.tenant, tenantId),
|
||||
eq(createddocuments.customer, portalCustomerId),
|
||||
eq(createddocuments.availableInPortal, true)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return rows[0]?.file || null
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MULTIPART INIT
|
||||
@@ -80,12 +131,7 @@ export default async function fileRoutes(server: FastifyInstance) {
|
||||
|
||||
// 🔹 EINZELNE DATEI
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
const file = rows[0]
|
||||
const file = await loadSingleFileForRequest(req, id)
|
||||
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
return file
|
||||
@@ -135,12 +181,7 @@ export default async function fileRoutes(server: FastifyInstance) {
|
||||
// 1️⃣ SINGLE DOWNLOAD
|
||||
// -------------------------------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
const file = rows[0]
|
||||
const file = await loadSingleFileForRequest(req, id)
|
||||
if (!file) return reply.code(404).send({ error: "File not found" })
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
@@ -217,12 +258,7 @@ export default async function fileRoutes(server: FastifyInstance) {
|
||||
// SINGLE FILE PRESIGNED URL
|
||||
// -------------------------------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
const file = rows[0]
|
||||
const file = await loadSingleFileForRequest(req, id)
|
||||
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const url = await getSignedUrl(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user