feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -1,43 +1,61 @@
import React from 'react';
import { login } from '../api';
import { useNavigate } from 'react-router-dom';
import { Input } from '@/components/ui/input';
import { Location, useLocation, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import { useAuth } from '../auth/context';
interface LocationState {
from?: Location;
}
export default function LoginPage() {
const nav = useNavigate();
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const { status, login } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const searchParams = React.useMemo(() => new URLSearchParams(location.search), [location.search]);
const oauthError = searchParams.get('error');
async function submit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setLoading(true);
try {
const { token } = await login(email, password);
localStorage.setItem('ta_token', token);
nav('/admin', { replace: true });
} catch (err: any) {
setError('Login fehlgeschlagen');
} finally { setLoading(false); }
}
React.useEffect(() => {
if (status === 'authenticated') {
navigate('/admin', { replace: true });
}
}, [status, navigate]);
const redirectTarget = React.useMemo(() => {
const state = location.state as LocationState | null;
if (state?.from) {
const from = state.from;
const search = from.search ?? '';
const hash = from.hash ?? '';
return `${from.pathname}${search}${hash}`;
}
return '/admin';
}, [location.state]);
return (
<div className="mx-auto max-w-sm p-6">
<div className="mb-4 flex items-center justify-between">
<div className="mx-auto flex min-h-screen max-w-sm flex-col justify-center p-6">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-lg font-semibold">Tenant Admin</h1>
<AppearanceToggleDropdown />
</div>
<form onSubmit={submit} className="space-y-3">
{error && <div className="rounded border border-red-300 bg-red-50 p-2 text-sm text-red-700">{error}</div>}
<Input placeholder="E-Mail" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<Input placeholder="Passwort" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<Button type="submit" disabled={loading || !email || !password} className="w-full">{loading ? 'Bitte warten…' : 'Anmelden'}</Button>
</form>
<div className="space-y-4 text-sm text-muted-foreground">
<p>
Melde dich mit deinem Fotospiel-Account an. Du wirst zur sicheren OAuth-Anmeldung weitergeleitet und danach
wieder zur Admin-Oberfl<EFBFBD>che gebracht.
</p>
{oauthError && (
<div className="rounded border border-red-300 bg-red-50 p-2 text-sm text-red-700">
Anmeldung fehlgeschlagen: {oauthError}
</div>
)}
<Button
className="w-full"
disabled={status === 'loading'}
onClick={() => login(redirectTarget)}
>
{status === 'loading' ? 'Bitte warten <20>' : 'Mit Tenant-Account anmelden'}
</Button>
</div>
</div>
);
}