mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat: Login-Screen-Branding im Global Admin (Hintergrund + Logo)
Im Bereich Anpassungen (AdminCustomizePage) kann der Platform-Admin nun den Login-Screen individuell gestalten: - Hintergrundtyp: Farbverlauf, Einfarbig oder Hintergrundbild - Farbverlauf: zwei Farbpicker (Von/Bis) mit Hex-Eingabe - Hintergrundbild: Datei-Upload max. 2MB, Live-Vorschau - Logo auf Login-Screen: wird automatisch aus dem Sidebar-Logo uebernommen Backend: settings.controller.ts GET/POST /settings/branding um loginBgType, loginBgColor1, loginBgColor2, loginBgImage erweitert. LoginPage laedt Branding per oeffentlichem Endpoint (kein Auth), leitet containerStyle per useMemo ab und zeigt Logo-Bild statt Hardcode-Text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b872b7e708
commit
c333cbfa4b
5 changed files with 348 additions and 13 deletions
32
Summarize.md
32
Summarize.md
|
|
@ -6,6 +6,38 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (16): Global Admin — Login-Screen-Branding (Hintergrund + Logo)
|
||||||
|
|
||||||
|
#### Backend (Core Service)
|
||||||
|
- `settings/settings.controller.ts` — `GET /settings/branding` + `POST /settings/branding` erweitert:
|
||||||
|
- 4 neue Felder: `loginBgType` ('gradient'|'solid'|'image'), `loginBgColor1`, `loginBgColor2`, `loginBgImage`
|
||||||
|
- `loginBgImage` Groeßen-Validierung: max. 2MB
|
||||||
|
- `loginBgType` Enum-Validierung: nur erlaubte Werte werden gespeichert
|
||||||
|
- Rueckwaertskompatibel: bestehende Redis-Daten ohne neue Felder liefern `null`-Defaults
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `admin/AdminCustomizePage.tsx` — Neuer Card-Block "Login-Hintergrund":
|
||||||
|
- Typ-Toggle: Farbverlauf / Einfarbig / Hintergrundbild
|
||||||
|
- Farbverlauf: Zwei Farbpicker (Von/Bis) mit Hex-Eingabe
|
||||||
|
- Einfarbig: Ein Farbpicker mit Hex-Eingabe
|
||||||
|
- Hintergrundbild: Datei-Upload (max. 2MB, nur Bilder), Entfernen-Button
|
||||||
|
- Live-Vorschau: Mini-Login-Mockup (200x120px) mit aktuellem Hintergrund und Logo
|
||||||
|
- `auth/LoginPage.tsx` — Dynamisches Branding:
|
||||||
|
- `useQuery(['settings', 'branding'])` — oeffentlicher Endpoint, kein Auth noetig
|
||||||
|
- `useMemo` → `containerStyle`: leitet CSS `background` aus `loginBgType` ab
|
||||||
|
- Logo-Anzeige: zeigt `<img>` mit `branding.logo` statt hardcoded "INSIGHT"-Text
|
||||||
|
- `auth/LoginPage.module.css` — Neue Klasse `.logoImage` (max-width 180px, object-fit contain)
|
||||||
|
|
||||||
|
#### TypeScript
|
||||||
|
- `npx tsc --noEmit` in packages/frontend: 0 Fehler
|
||||||
|
- `npx tsc --noEmit` in packages/core-service: 0 Fehler
|
||||||
|
|
||||||
|
#### Deployment-Hinweis (Schritt 16)
|
||||||
|
- Rebuild + Restart: core-service (Backend) und frontend
|
||||||
|
- Kein Datenbankschema-Aenderung, keine Migration noetig (nur Redis)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Aenderungen 2026-03-13 (15): Experten-Profil – BulletEditor Fett/Kursiv/Unterstrichen + Aufgaben-Anzeige
|
### Aenderungen 2026-03-13 (15): Experten-Profil – BulletEditor Fett/Kursiv/Unterstrichen + Aufgaben-Anzeige
|
||||||
|
|
||||||
#### Frontend
|
#### Frontend
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export class SettingsController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/settings/branding
|
* GET /api/v1/settings/branding
|
||||||
* Branding-Einstellungen lesen (Logo, Sidebar-Farbe etc.).
|
* Branding-Einstellungen lesen (Logo, Sidebar-Farbe, Login-Hintergrund etc.).
|
||||||
*/
|
*/
|
||||||
@Get('branding')
|
@Get('branding')
|
||||||
@ApiOperation({ summary: 'Branding-Einstellungen lesen' })
|
@ApiOperation({ summary: 'Branding-Einstellungen lesen' })
|
||||||
|
|
@ -147,21 +147,36 @@ export class SettingsController {
|
||||||
sidebarColor: string | null;
|
sidebarColor: string | null;
|
||||||
logoWidth: number | null;
|
logoWidth: number | null;
|
||||||
sidebarWidth: number | null;
|
sidebarWidth: number | null;
|
||||||
|
loginBgType: 'gradient' | 'solid' | 'image' | null;
|
||||||
|
loginBgColor1: string | null;
|
||||||
|
loginBgColor2: string | null;
|
||||||
|
loginBgImage: string | null;
|
||||||
}> {
|
}> {
|
||||||
const raw = await this.redis.get(BRANDING_LOGO_KEY);
|
const raw = await this.redis.get(BRANDING_LOGO_KEY);
|
||||||
if (!raw) return { logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null };
|
const empty = {
|
||||||
|
logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null,
|
||||||
|
loginBgType: null, loginBgColor1: null, loginBgColor2: null, loginBgImage: null,
|
||||||
|
};
|
||||||
|
if (!raw) return empty;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
|
const loginBgType = ['gradient', 'solid', 'image'].includes(data.loginBgType)
|
||||||
|
? (data.loginBgType as 'gradient' | 'solid' | 'image')
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
logo: data.logo || null,
|
logo: data.logo || null,
|
||||||
sidebarColor: data.sidebarColor || null,
|
sidebarColor: data.sidebarColor || null,
|
||||||
logoWidth: typeof data.logoWidth === 'number' ? data.logoWidth : null,
|
logoWidth: typeof data.logoWidth === 'number' ? data.logoWidth : null,
|
||||||
sidebarWidth: typeof data.sidebarWidth === 'number' ? data.sidebarWidth : null,
|
sidebarWidth: typeof data.sidebarWidth === 'number' ? data.sidebarWidth : null,
|
||||||
|
loginBgType,
|
||||||
|
loginBgColor1: data.loginBgColor1 || null,
|
||||||
|
loginBgColor2: data.loginBgColor2 || null,
|
||||||
|
loginBgImage: data.loginBgImage || null,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// Legacy: nur Logo als String
|
// Legacy: nur Logo als String
|
||||||
return { logo: raw, sidebarColor: null, logoWidth: null, sidebarWidth: null };
|
return { ...empty, logo: raw };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,12 +194,25 @@ export class SettingsController {
|
||||||
sidebarColor?: string | null;
|
sidebarColor?: string | null;
|
||||||
logoWidth?: number | null;
|
logoWidth?: number | null;
|
||||||
sidebarWidth?: number | null;
|
sidebarWidth?: number | null;
|
||||||
|
loginBgType?: string | null;
|
||||||
|
loginBgColor1?: string | null;
|
||||||
|
loginBgColor2?: string | null;
|
||||||
|
loginBgImage?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
if (body.logo && body.logo.length > 500_000) {
|
if (body.logo && body.logo.length > 500_000) {
|
||||||
throw new BadRequestException('Logo darf maximal 500KB gross sein');
|
throw new BadRequestException('Logo darf maximal 500KB gross sein');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.loginBgImage && body.loginBgImage.length > 2_000_000) {
|
||||||
|
throw new BadRequestException('Hintergrundbild darf maximal 2MB gross sein');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validBgTypes = ['gradient', 'solid', 'image'];
|
||||||
|
const loginBgType = body.loginBgType && validBgTypes.includes(body.loginBgType)
|
||||||
|
? body.loginBgType
|
||||||
|
: null;
|
||||||
|
|
||||||
// Werte-Grenzen
|
// Werte-Grenzen
|
||||||
const logoWidth = typeof body.logoWidth === 'number'
|
const logoWidth = typeof body.logoWidth === 'number'
|
||||||
? Math.min(Math.max(Math.round(body.logoWidth), 40), 240)
|
? Math.min(Math.max(Math.round(body.logoWidth), 40), 240)
|
||||||
|
|
@ -194,10 +222,14 @@ export class SettingsController {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
logo: body.logo || null,
|
logo: body.logo || null,
|
||||||
sidebarColor: body.sidebarColor || null,
|
sidebarColor: body.sidebarColor || null,
|
||||||
logoWidth,
|
logoWidth,
|
||||||
sidebarWidth,
|
sidebarWidth,
|
||||||
|
loginBgType,
|
||||||
|
loginBgColor1: body.loginBgColor1 || null,
|
||||||
|
loginBgColor2: body.loginBgColor2 || null,
|
||||||
|
loginBgImage: body.loginBgImage || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.redis.set(BRANDING_LOGO_KEY, JSON.stringify(data));
|
await this.redis.set(BRANDING_LOGO_KEY, JSON.stringify(data));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ interface BrandingData {
|
||||||
sidebarColor: string | null;
|
sidebarColor: string | null;
|
||||||
logoWidth: number | null;
|
logoWidth: number | null;
|
||||||
sidebarWidth: number | null;
|
sidebarWidth: number | null;
|
||||||
|
loginBgType: 'gradient' | 'solid' | 'image' | null;
|
||||||
|
loginBgColor1: string | null;
|
||||||
|
loginBgColor2: string | null;
|
||||||
|
loginBgImage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIDEBAR_PRESETS = [
|
const SIDEBAR_PRESETS = [
|
||||||
|
|
@ -63,10 +67,15 @@ const labelStyle: React.CSSProperties = {
|
||||||
export function AdminCustomizePage() {
|
export function AdminCustomizePage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const loginBgFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [logo, setLogo] = useState<string | null>(null);
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
const [sidebarColor, setSidebarColor] = useState<string>('#1e293b');
|
const [sidebarColor, setSidebarColor] = useState<string>('#1e293b');
|
||||||
const [logoWidth, setLogoWidth] = useState<number>(160);
|
const [logoWidth, setLogoWidth] = useState<number>(160);
|
||||||
const [sidebarWidth, setSidebarWidth] = useState<number>(240);
|
const [sidebarWidth, setSidebarWidth] = useState<number>(240);
|
||||||
|
const [loginBgType, setLoginBgType] = useState<'gradient' | 'solid' | 'image'>('gradient');
|
||||||
|
const [loginBgColor1, setLoginBgColor1] = useState<string>('#1a56db');
|
||||||
|
const [loginBgColor2, setLoginBgColor2] = useState<string>('#1e3a5f');
|
||||||
|
const [loginBgImage, setLoginBgImage] = useState<string | null>(null);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
|
||||||
|
|
@ -84,6 +93,10 @@ export function AdminCustomizePage() {
|
||||||
setSidebarColor(data.sidebarColor || '#1e293b');
|
setSidebarColor(data.sidebarColor || '#1e293b');
|
||||||
setLogoWidth(data.logoWidth ?? 160);
|
setLogoWidth(data.logoWidth ?? 160);
|
||||||
setSidebarWidth(data.sidebarWidth ?? 240);
|
setSidebarWidth(data.sidebarWidth ?? 240);
|
||||||
|
setLoginBgType(data.loginBgType || 'gradient');
|
||||||
|
setLoginBgColor1(data.loginBgColor1 || '#1a56db');
|
||||||
|
setLoginBgColor2(data.loginBgColor2 || '#1e3a5f');
|
||||||
|
setLoginBgImage(data.loginBgImage || null);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
@ -94,6 +107,10 @@ export function AdminCustomizePage() {
|
||||||
sidebarColor: string;
|
sidebarColor: string;
|
||||||
logoWidth: number;
|
logoWidth: number;
|
||||||
sidebarWidth: number;
|
sidebarWidth: number;
|
||||||
|
loginBgType: string;
|
||||||
|
loginBgColor1: string;
|
||||||
|
loginBgColor2: string;
|
||||||
|
loginBgImage: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const res = await api.post('/settings/branding', branding);
|
const res = await api.post('/settings/branding', branding);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -137,9 +154,44 @@ export function AdminCustomizePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth });
|
saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoginBgFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('Bitte nur Bilddateien hochladen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 2_000_000) {
|
||||||
|
alert('Datei darf maximal 2MB gross sein');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setLoginBgImage(reader.result as string);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginBgPreviewStyle: React.CSSProperties = (() => {
|
||||||
|
if (loginBgType === 'gradient') {
|
||||||
|
return { background: `linear-gradient(135deg, ${loginBgColor1} 0%, ${loginBgColor2} 100%)` };
|
||||||
|
}
|
||||||
|
if (loginBgType === 'solid') {
|
||||||
|
return { background: loginBgColor1 };
|
||||||
|
}
|
||||||
|
if (loginBgType === 'image' && loginBgImage) {
|
||||||
|
return { background: `url(${loginBgImage}) center/cover no-repeat` };
|
||||||
|
}
|
||||||
|
return { background: `linear-gradient(135deg, ${loginBgColor1} 0%, ${loginBgColor2} 100%)` };
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
|
@ -511,6 +563,176 @@ export function AdminCustomizePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Login-Hintergrund */}
|
||||||
|
<div style={cardStyle}>
|
||||||
|
<h3 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.5rem' }}>
|
||||||
|
Login-Hintergrund
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', marginBottom: '1.25rem' }}>
|
||||||
|
Hintergrund des Login-Screens. Das Plattform-Logo (oben) wird ebenfalls auf dem Login-Screen angezeigt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Typ-Toggle */}
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label style={labelStyle}>Hintergrundtyp</label>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
{(['gradient', 'solid', 'image'] as const).map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => { setLoginBgType(type); setHasChanges(true); }}
|
||||||
|
style={{
|
||||||
|
padding: '0.375rem 0.875rem',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
border: loginBgType === type ? '2px solid var(--color-primary)' : '1px solid var(--color-border)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
background: loginBgType === type ? 'var(--color-primary-light)' : 'none',
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: loginBgType === type ? 600 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type === 'gradient' ? 'Farbverlauf' : type === 'solid' ? 'Einfarbig' : 'Hintergrundbild'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Farbverlauf */}
|
||||||
|
{loginBgType === 'gradient' && (
|
||||||
|
<div style={{ display: 'flex', gap: '2rem', marginBottom: '1.25rem', flexWrap: 'wrap' }}>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Von</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={loginBgColor1}
|
||||||
|
onChange={(e) => { setLoginBgColor1(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ width: 40, height: 36, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={loginBgColor1}
|
||||||
|
onChange={(e) => { setLoginBgColor1(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ padding: '0.375rem 0.5rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 100, fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Bis</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={loginBgColor2}
|
||||||
|
onChange={(e) => { setLoginBgColor2(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ width: 40, height: 36, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={loginBgColor2}
|
||||||
|
onChange={(e) => { setLoginBgColor2(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ padding: '0.375rem 0.5rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 100, fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Einfarbig */}
|
||||||
|
{loginBgType === 'solid' && (
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label style={labelStyle}>Farbe</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={loginBgColor1}
|
||||||
|
onChange={(e) => { setLoginBgColor1(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ width: 40, height: 40, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={loginBgColor1}
|
||||||
|
onChange={(e) => { setLoginBgColor1(e.target.value); setHasChanges(true); }}
|
||||||
|
style={{ padding: '0.5rem 0.75rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 120, fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hintergrundbild */}
|
||||||
|
{loginBgType === 'image' && (
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label style={labelStyle}>Hintergrundbild</label>
|
||||||
|
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', marginBottom: '0.75rem' }}>
|
||||||
|
Empfohlen: JPG oder PNG, min. 1920×1080px, max. 2MB.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
ref={loginBgFileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleLoginBgFileSelect}
|
||||||
|
/>
|
||||||
|
<button style={btnSecondary} onClick={() => loginBgFileInputRef.current?.click()}>
|
||||||
|
Bild hochladen
|
||||||
|
</button>
|
||||||
|
{loginBgImage && (
|
||||||
|
<button
|
||||||
|
style={{ ...btnSecondary, color: 'var(--color-error)', borderColor: 'var(--color-error)' }}
|
||||||
|
onClick={() => { setLoginBgImage(null); setHasChanges(true); }}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{loginBgImage && (
|
||||||
|
<span style={{ fontSize: '0.8125rem', color: 'var(--color-success)' }}>✓ Bild geladen</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vorschau */}
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Vorschau</label>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...loginBgPreviewStyle,
|
||||||
|
width: 200,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mini-Login-Card */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 80,
|
||||||
|
height: 72,
|
||||||
|
background: 'rgba(255,255,255,0.95)',
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '0.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logo ? (
|
||||||
|
<img src={logo} alt="" style={{ maxWidth: 40, maxHeight: 14, objectFit: 'contain' }} />
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '0.5rem', fontWeight: 700, color: 'var(--color-primary)', letterSpacing: 1 }}>INSIGHT</div>
|
||||||
|
)}
|
||||||
|
<div style={{ width: '100%', height: 8, background: '#e5e7eb', borderRadius: 2 }} />
|
||||||
|
<div style={{ width: '100%', height: 8, background: '#e5e7eb', borderRadius: 2 }} />
|
||||||
|
<div style={{ width: '100%', height: 10, background: 'var(--color-primary)', borderRadius: 2, opacity: 0.8 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Speichern */}
|
{/* Speichern */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logoImage {
|
||||||
|
max-width: 180px;
|
||||||
|
max-height: 64px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, useMemo, type FormEvent } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import styles from './LoginPage.module.css';
|
import styles from './LoginPage.module.css';
|
||||||
|
|
||||||
|
interface BrandingData {
|
||||||
|
logo: string | null;
|
||||||
|
loginBgType: 'gradient' | 'solid' | 'image' | null;
|
||||||
|
loginBgColor1: string | null;
|
||||||
|
loginBgColor2: string | null;
|
||||||
|
loginBgImage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
@ -25,6 +33,35 @@ export function LoginPage() {
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// Branding laden (kein Auth-Guard auf diesem Endpoint)
|
||||||
|
const { data: branding } = useQuery<BrandingData>({
|
||||||
|
queryKey: ['settings', 'branding'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get<BrandingData>('/settings/branding');
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamischer Hintergrund basierend auf Branding
|
||||||
|
const containerStyle = useMemo((): React.CSSProperties => {
|
||||||
|
if (!branding) return {};
|
||||||
|
const type = branding.loginBgType || 'gradient';
|
||||||
|
if (type === 'gradient') {
|
||||||
|
const c1 = branding.loginBgColor1 || '#1a56db';
|
||||||
|
const c2 = branding.loginBgColor2 || '#1e3a5f';
|
||||||
|
return { background: `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)` };
|
||||||
|
}
|
||||||
|
if (type === 'solid') {
|
||||||
|
return { background: branding.loginBgColor1 || '#1a56db' };
|
||||||
|
}
|
||||||
|
if (type === 'image' && branding.loginBgImage) {
|
||||||
|
return { background: `url(${branding.loginBgImage}) center/cover no-repeat` };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [branding]);
|
||||||
|
|
||||||
// SSO-Status abfragen: Ist Microsoft SSO konfiguriert?
|
// SSO-Status abfragen: Ist Microsoft SSO konfiguriert?
|
||||||
const { data: ssoStatus } = useQuery<{ microsoft: boolean }>({
|
const { data: ssoStatus } = useQuery<{ microsoft: boolean }>({
|
||||||
queryKey: ['sso', 'status'],
|
queryKey: ['sso', 'status'],
|
||||||
|
|
@ -70,10 +107,14 @@ export function LoginPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container} style={containerStyle}>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.logo}>
|
<div className={styles.logo}>
|
||||||
<h1>INSIGHT</h1>
|
{branding?.logo ? (
|
||||||
|
<img src={branding.logo} alt="Logo" className={styles.logoImage} />
|
||||||
|
) : (
|
||||||
|
<h1>INSIGHT</h1>
|
||||||
|
)}
|
||||||
<p>Business Platform</p>
|
<p>Business Platform</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue