feat: HTTPS-TLS-Sync nach Deploy, Projekte ohne Subdomain, portalPassword-Feld

- previewDeploy.syncPreviewTls(): schreibt Traefik-Router (Host-Regeln) fuer alle
  Preview-Subdomains nach preview-hosts.yml, damit Let's Encrypt Zertifikate
  ausstellt. Laeuft nach jedem Deploy und beim Serverstart (ersetzt das nie
  aufgerufene bash-Script sync-preview-tls.sh).
- createProjectFromTemplate: Subdomain optional. Ohne Subdomain wird nur das
  Gitea-Repo angelegt (projectType 'repo', Status 'created'), kein Deploy.
- customerAdmin: Portal-Passwort liegt jetzt im Feld customers.portalPassword
  statt als __portalPassword__-Prefix in den Notizen; Legacy-Notizen werden
  beim naechsten Update bereinigt. CRM-Stufe (lead/customer) wird beim Setzen
  des Portal-Passworts nicht mehr auf 'active' ueberschrieben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:34:23 +00:00
parent cf4b3ce4d1
commit cc57cf887a
6 changed files with 124 additions and 30 deletions

View File

@@ -14,20 +14,16 @@ import {
Query,
} from './appwriteAdmin.js'
const PORTAL_PASSWORD_PREFIX = '__portalPassword__:'
function readPortalPassword(customer) {
if (customer?.portalPassword) return customer.portalPassword
// Fallback: Altbestand, bei dem das Passwort noch in den Notizen lag
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 notesWithoutPortalPassword(notes) {
return (notes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim()
}
function sanitizeCustomer(customer, { portalPassword } = {}) {
@@ -115,7 +111,7 @@ export async function createCustomerWithPortalAccess(payload) {
location: location.trim(),
email: email.trim(),
phone: phone.trim(),
notes: notesWithPortalPassword('', password),
portalPassword: password,
portalAccessEnabled: true,
appwriteUserId: appwriteUser.$id,
customerStatus: 'active',
@@ -206,6 +202,9 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
const now = new Date().toISOString()
const nextPortalPassword =
normalizedPassword || readPortalPassword(customer) || undefined
// Legacy-Prefix aus den Notizen entfernen, Passwort liegt jetzt im eigenen Feld
const cleanedNotes = notesWithoutPortalPassword(customer.notes)
const notesChanged = cleanedNotes !== (customer.notes || '')
const updated = await updateDocument(config.collections.customers, customerId, {
...(code !== undefined ? { code: String(code).trim() } : {}),
@@ -213,10 +212,16 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
...(location !== undefined ? { location: String(location).trim() } : {}),
email: nextEmail,
...(phone !== undefined ? { phone: String(phone).trim() } : {}),
...(nextPortalPassword
? { notes: notesWithPortalPassword(customer.notes, nextPortalPassword) }
...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}),
...(notesChanged ? { notes: cleanedNotes } : {}),
// CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer
...(appwriteUserId
? {
appwriteUserId,
portalAccessEnabled: true,
...(customer.customerStatus ? {} : { customerStatus: 'active' }),
}
: {}),
...(appwriteUserId ? { appwriteUserId, portalAccessEnabled: true, customerStatus: 'active' } : {}),
updatedAt: now,
})