Files
Webklar-Kundenbereich/server/services/customerAdmin.js
Kenso Grimm cf4b3ce4d1 chore: aktuellen Live-Stand sichern (Admin-API, Services, Auth-Erweiterungen)
Sichert den bisher uncommitteten Produktionsstand des Kundenbereich-Servers,
damit kuenftige Deploys (git reset --hard) nichts mehr verwerfen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:24:53 +00:00

259 lines
7.5 KiB
JavaScript
Raw Blame History

import { config } from '../config.js'
import {
createAppwriteUser,
createDocument,
deleteDocument,
getCustomerByEmail,
getDocument,
getPortalAccessByCustomerId,
listDocuments,
updateAppwriteUserEmail,
updateAppwriteUserPassword,
deleteAppwriteUser,
updateDocument,
Query,
} from './appwriteAdmin.js'
const PORTAL_PASSWORD_PREFIX = '__portalPassword__:'
function readPortalPassword(customer) {
if (customer?.portalPassword) return customer.portalPassword
const notes = customer?.notes || ''
const match = notes.match(/^__portalPassword__:([^\n]+)/)
return match ? match[1] : ''
}
function notesWithPortalPassword(existingNotes, password) {
const stripped = (existingNotes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim()
if (!password) return stripped || ''
const line = `${PORTAL_PASSWORD_PREFIX}${password}`
return stripped ? `${line}\n${stripped}` : line
}
function sanitizeCustomer(customer, { portalPassword } = {}) {
const storedPassword = portalPassword || readPortalPassword(customer)
return {
$id: customer.$id,
code: customer.code || '',
name: customer.name || '',
location: customer.location || '',
email: customer.email || '',
phone: customer.phone || '',
companyName: customer.companyName || '',
portalAccessEnabled: Boolean(customer.portalAccessEnabled),
appwriteUserId: customer.appwriteUserId || '',
customerStatus: customer.customerStatus || '',
portalPassword: storedPassword || '',
}
}
async function ensurePortalAccess(customerId, appwriteUserId) {
const now = new Date().toISOString()
let portalAccess = await getPortalAccessByCustomerId(customerId)
if (portalAccess) {
portalAccess = await updateDocument(config.collections.customerPortalAccess, portalAccess.$id, {
enabled: true,
appwriteUserId,
passwordSet: true,
})
} else {
portalAccess = await createDocument(config.collections.customerPortalAccess, {
customerId,
enabled: true,
appwriteUserId,
passwordSet: true,
})
}
return portalAccess
}
export async function createCustomerWithPortalAccess(payload) {
const {
code = '',
name,
location = '',
email,
phone = '',
password,
} = payload || {}
if (!name?.trim()) {
const error = new Error('Name ist erforderlich')
error.status = 400
throw error
}
if (!email?.trim()) {
const error = new Error('E-Mail ist erforderlich')
error.status = 400
throw error
}
if (!password || password.length < 8) {
const error = new Error('Passwort muss mindestens 8 Zeichen haben')
error.status = 400
throw error
}
const existing = await getCustomerByEmail(email.trim())
if (existing) {
const error = new Error('Ein Kunde mit dieser E-Mail existiert bereits')
error.status = 409
throw error
}
const appwriteUser = await createAppwriteUser({
email: email.trim(),
password,
name: name.trim(),
})
const now = new Date().toISOString()
let customer
try {
customer = await createDocument(config.collections.customers, {
code: code.trim(),
name: name.trim(),
location: location.trim(),
email: email.trim(),
phone: phone.trim(),
notes: notesWithPortalPassword('', password),
portalAccessEnabled: true,
appwriteUserId: appwriteUser.$id,
customerStatus: 'active',
createdAt: now,
updatedAt: now,
})
await ensurePortalAccess(customer.$id, appwriteUser.$id)
} catch (err) {
try {
await deleteAppwriteUser(appwriteUser.$id)
} catch {
/* ignore rollback failure */
}
throw err
}
return {
customer: sanitizeCustomer(customer, { portalPassword: password }),
}
}
export async function updateCustomerWithPortalAccess(customerId, payload) {
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
if (!customer) {
const error = new Error('Kunde nicht gefunden')
error.status = 404
throw error
}
const {
code,
name,
location,
email,
phone,
password,
} = payload || {}
const nextEmail = (email ?? customer.email ?? '').trim()
if (!nextEmail) {
const error = new Error('E-Mail ist erforderlich')
error.status = 400
throw error
}
if (password && password.length < 8) {
const error = new Error('Passwort muss mindestens 8 Zeichen haben')
error.status = 400
throw error
}
const normalizedPassword =
typeof password === 'string' && password.trim().length > 0 ? password : undefined
if (nextEmail.toLowerCase() !== (customer.email || '').trim().toLowerCase()) {
const existing = await getCustomerByEmail(nextEmail)
if (existing && existing.$id !== customerId) {
const error = new Error('Ein anderer Kunde nutzt diese E-Mail bereits')
error.status = 409
throw error
}
}
let appwriteUserId = customer.appwriteUserId || ''
let portalPassword
if (!appwriteUserId && nextEmail && normalizedPassword) {
const appwriteUser = await createAppwriteUser({
email: nextEmail,
password: normalizedPassword,
name: (name ?? customer.name ?? nextEmail).trim(),
})
appwriteUserId = appwriteUser.$id
portalPassword = normalizedPassword
} else if (appwriteUserId) {
if (nextEmail && nextEmail !== customer.email) {
await updateAppwriteUserEmail(appwriteUserId, nextEmail)
}
if (normalizedPassword) {
await updateAppwriteUserPassword(appwriteUserId, normalizedPassword)
portalPassword = normalizedPassword
}
} else if (normalizedPassword) {
const error = new Error('Portal-Login fehlt <20> bitte E-Mail setzen und Passwort erneut speichern')
error.status = 400
throw error
}
const now = new Date().toISOString()
const nextPortalPassword =
normalizedPassword || readPortalPassword(customer) || undefined
const updated = await updateDocument(config.collections.customers, customerId, {
...(code !== undefined ? { code: String(code).trim() } : {}),
...(name !== undefined ? { name: String(name).trim() } : {}),
...(location !== undefined ? { location: String(location).trim() } : {}),
email: nextEmail,
...(phone !== undefined ? { phone: String(phone).trim() } : {}),
...(nextPortalPassword
? { notes: notesWithPortalPassword(customer.notes, nextPortalPassword) }
: {}),
...(appwriteUserId ? { appwriteUserId, portalAccessEnabled: true, customerStatus: 'active' } : {}),
updatedAt: now,
})
if (appwriteUserId) {
await ensurePortalAccess(customerId, appwriteUserId)
}
return {
customer: sanitizeCustomer(updated, portalPassword ? { portalPassword } : {}),
}
}
export async function listCustomersForAdmin() {
const customers = await listDocuments(config.collections.customers, [Query.orderAsc('name')])
return customers.map((customer) => sanitizeCustomer(customer))
}
export async function deleteCustomerWithPortalAccess(customerId) {
const customer = await getDocument(config.collections.customers, customerId)
.catch(() => null)
if (!customer) {
const error = new Error('Kunde nicht gefunden')
error.status = 404
throw error
}
const portalAccess = await getPortalAccessByCustomerId(customerId)
if (portalAccess) {
await deleteDocument(
config.collections.customerPortalAccess,
portalAccess.$id
)
}
await deleteDocument(config.collections.customers, customerId)
if (customer.appwriteUserId) {
await deleteAppwriteUser(customer.appwriteUserId).catch(() => {})
}
return { success: true }
}