Portal-Login-URL als App-Setting (statt nur PUBLIC_URL-Env)

Bugfix: in der "Zugangsdaten versenden"-Mail stand bisher
http://localhost:5173/portal/login als Login-Link, wenn die
PUBLIC_URL-Env nicht gesetzt war – Kunden klickten auf einen
toten Link.

Neue Einstellung "portalLoginUrl" unter Einstellungen → Kundenportal.
Wenn gepflegt, wird sie als Basis-URL für:
  - Portal-Zugangsdaten-Mail (Login-Link)
  - Passwort-Reset-Link
verwendet. Reihenfolge: AppSetting → PUBLIC_URL-Env → localhost-Default.

Backend: getPublicUrl() jetzt async, liest erst aus AppSetting,
fällt auf Env zurück. Trailing-Slash-Bereinigung im Backend
(damit Links nicht doppelt-Slash bekommen) und im Frontend
(damit der gespeicherte Wert sauber ist).

Frontend: neue Card "Portal-Login-URL" oberhalb der Support-
Anfragen-Card in PortalSettings.tsx. Input + Save-Button +
http(s)://-Schema-Validierung + Erfolgs-Toast.

Live-verifiziert: PUT setzt 'https://crm.beispiel.de', Backend-
getPublicUrl liefert 'https://crm.beispiel.de/portal/login'
statt localhost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 12:49:55 +02:00
parent ee4ca9df07
commit 2d4e4cdcc7
3 changed files with 79 additions and 5 deletions
+55 -1
View File
@@ -1,9 +1,12 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import toast from 'react-hot-toast';
import { appSettingsApi } from '../../services/api';
import Card from '../../components/ui/Card';
import { ArrowLeft, Globe, MessageSquare } from 'lucide-react';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
import { ArrowLeft, Globe, MessageSquare, Link as LinkIcon } from 'lucide-react';
export default function PortalSettings() {
const queryClient = useQueryClient();
@@ -14,10 +17,12 @@ export default function PortalSettings() {
});
const [customerSupportEnabled, setCustomerSupportEnabled] = useState(false);
const [portalLoginUrl, setPortalLoginUrl] = useState('');
useEffect(() => {
if (settingsData?.data) {
setCustomerSupportEnabled(settingsData.data.customerSupportTicketsEnabled === 'true');
setPortalLoginUrl(settingsData.data.portalLoginUrl || '');
}
}, [settingsData]);
@@ -27,6 +32,9 @@ export default function PortalSettings() {
queryClient.invalidateQueries({ queryKey: ['app-settings'] });
queryClient.invalidateQueries({ queryKey: ['app-settings-public'] });
},
onError: (err: Error) => {
toast.error(err.message || 'Speichern fehlgeschlagen');
},
});
const handleSupportToggle = (enabled: boolean) => {
@@ -34,6 +42,18 @@ export default function PortalSettings() {
updateMutation.mutate({ customerSupportTicketsEnabled: enabled ? 'true' : 'false' });
};
const handleSavePortalLoginUrl = () => {
const trimmed = portalLoginUrl.trim().replace(/\/+$/, '');
if (trimmed && !/^https?:\/\//i.test(trimmed)) {
toast.error('URL muss mit http:// oder https:// beginnen');
return;
}
updateMutation.mutate(
{ portalLoginUrl: trimmed },
{ onSuccess: () => toast.success('Portal-Login-URL gespeichert') },
);
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@@ -54,6 +74,40 @@ export default function PortalSettings() {
</div>
</div>
<Card title="Portal-Login-URL" className="mb-6">
<div className="flex items-start gap-3">
<LinkIcon className="w-5 h-5 text-gray-500 mt-2" />
<div className="flex-1">
<p className="text-sm text-gray-600 mb-3">
Diese Basis-URL wird in Mails an Kunden eingesetzt, die ihre
Portal-Zugangsdaten zugeschickt bekommen, sowie bei
Passwort-Reset-Links. Ohne Eintrag wird auf den Server-
Standard zurückgegriffen, was lokal in <code>localhost:5173</code>
endet Kunden klicken dann auf einen toten Link.
</p>
<div className="flex gap-2">
<Input
value={portalLoginUrl}
onChange={(e) => setPortalLoginUrl(e.target.value)}
placeholder="https://crm.deine-domain.de"
className="flex-1"
/>
<Button
onClick={handleSavePortalLoginUrl}
disabled={updateMutation.isPending || portalLoginUrl === (settingsData?.data?.portalLoginUrl || '')}
>
Speichern
</Button>
</div>
<p className="text-xs text-gray-500 mt-2">
Beispiel: <code>https://crm.deine-domain.de</code> →
Login-Link in der Mail wird <code>https://crm.deine-domain.de/portal/login</code>.
Trailing-Slash wird automatisch entfernt.
</p>
</div>
</div>
</Card>
<Card title="Support-Anfragen">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">