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>
This commit is contained in:
258
server/services/customerAdmin.js
Normal file
258
server/services/customerAdmin.js
Normal file
@@ -0,0 +1,258 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user