user magic link inmplemented

This commit is contained in:
ValueOn AG 2026-01-13 00:27:09 +01:00
parent 06ffce8984
commit 9fc0c9bc4c
11 changed files with 1136 additions and 127 deletions

View file

@ -6,6 +6,8 @@ import './index.css';
import Login from './pages/Login'; import Login from './pages/Login';
import Register from './pages/Register'; import Register from './pages/Register';
import PasswordResetRequest from './pages/PasswordResetRequest';
import Reset from './pages/Reset';
import { AuthProvider } from './providers/auth/AuthProvider'; import { AuthProvider } from './providers/auth/AuthProvider';
import { ProtectedRoute } from './providers/auth/ProtectedRoute'; import { ProtectedRoute } from './providers/auth/ProtectedRoute';
@ -39,25 +41,37 @@ function App() {
return ( return (
<LanguageProvider> <LanguageProvider>
<AuthProvider> <AuthProvider>
<FileProvider> <Router>
<WorkflowSelectionProvider> <Routes>
<Router> {/* PUBLIC AUTH ROUTES - NO AUTHENTICATION REQUIRED */}
<Routes> <Route path="/login" element={<Login />} />
{/* Public route */} <Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} /> <Route path="/password-reset-request" element={<PasswordResetRequest />} />
<Route path="/register" element={<Register />} /> <Route path="/reset" element={<Reset />} />
<Route path="/" element={
<ProtectedRoute> {/* PROTECTED ROUTE - requires authentication */}
<Home /> <Route path="/" element={
</ProtectedRoute> <ProtectedRoute>
}> <FileProvider>
{/* All page routing is now handled by the Page Loader in Home.tsx */} <WorkflowSelectionProvider>
<Route path="*" element={null} /> <Home />
</Route> </WorkflowSelectionProvider>
</Routes> </FileProvider>
</Router> </ProtectedRoute>
</WorkflowSelectionProvider> } />
</FileProvider>
{/* Catch-all redirect to home */}
<Route path="*" element={
<ProtectedRoute>
<FileProvider>
<WorkflowSelectionProvider>
<Home />
</WorkflowSelectionProvider>
</FileProvider>
</ProtectedRoute>
} />
</Routes>
</Router>
</AuthProvider> </AuthProvider>
</LanguageProvider> </LanguageProvider>
); );

View file

@ -22,7 +22,6 @@ export interface LoginResponse {
export interface RegisterData { export interface RegisterData {
username: string; username: string;
password: string;
email: string; email: string;
fullName: string; fullName: string;
language?: string; language?: string;
@ -38,8 +37,19 @@ export interface RegisterRequest {
language: string; language: string;
enabled: boolean; enabled: boolean;
privilege: string; privilege: string;
authenticationAuthority: string;
}; };
password: string; frontendUrl: string;
}
export interface PasswordResetRequestResponse {
success: boolean;
message: string;
}
export interface PasswordResetResponse {
success: boolean;
message: string;
} }
export interface RegisterResponse { export interface RegisterResponse {
@ -142,11 +152,13 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise<User>
} }
/** /**
* Register a new user * Register a new user (magic link based - no password required)
* Endpoint: POST /api/local/register * Endpoint: POST /api/local/register
*
* After registration, user receives an email with a magic link to set their password.
*/ */
export async function registerApi(registerData: RegisterData): Promise<RegisterResponse> { export async function registerApi(registerData: RegisterData): Promise<RegisterResponse> {
// Prepare data to match backend expectations // Prepare data to match backend expectations (no password - magic link flow)
const dataToSend: RegisterRequest = { const dataToSend: RegisterRequest = {
userData: { userData: {
username: registerData.username, username: registerData.username,
@ -154,9 +166,10 @@ export async function registerApi(registerData: RegisterData): Promise<RegisterR
fullName: registerData.fullName, fullName: registerData.fullName,
language: registerData.language || 'de', language: registerData.language || 'de',
enabled: registerData.enabled !== undefined ? registerData.enabled : true, enabled: registerData.enabled !== undefined ? registerData.enabled : true,
privilege: registerData.privilege || 'user' privilege: registerData.privilege || 'user',
authenticationAuthority: 'local'
}, },
password: registerData.password frontendUrl: window.location.origin
}; };
// Prepare headers with CSRF token if available // Prepare headers with CSRF token if available
@ -174,19 +187,64 @@ export async function registerApi(registerData: RegisterData): Promise<RegisterR
const userData: any = response.data; const userData: any = response.data;
return { return {
success: true, success: true,
message: 'Registration successful', message: 'Registration successful - check email for password setup link',
user: userData && typeof userData === 'object' && 'id' in userData ? { user: userData && typeof userData === 'object' && 'id' in userData ? {
id: String(userData.id || ''), id: String(userData.id || ''),
username: String(userData.username || ''), username: String(userData.username || ''),
email: String(userData.email || ''), email: String(userData.email || ''),
fullName: String(userData.fullName || ''), fullName: String(userData.fullName || ''),
language: String(userData.language || 'en'), language: String(userData.language || 'de'),
enabled: Boolean(userData.enabled !== false), enabled: Boolean(userData.enabled !== false),
privilege: String(userData.privilege || 'user') privilege: String(userData.privilege || 'user')
} : undefined } : undefined
}; };
} }
/**
* Request password reset by username
* Endpoint: POST /api/local/password-reset-request
*
* Sends a reset email to the user's registered email address.
*/
export async function requestPasswordResetApi(username: string): Promise<PasswordResetRequestResponse> {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
addCSRFTokenToHeaders(headers);
const response = await api.post<PasswordResetRequestResponse>(
'/api/local/password-reset-request',
{
username,
frontendUrl: window.location.origin
},
{ headers }
);
return response.data;
}
/**
* Reset password using token from magic link
* Endpoint: POST /api/local/password-reset
*/
export async function resetPasswordApi(token: string, password: string): Promise<PasswordResetResponse> {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
addCSRFTokenToHeaders(headers);
const response = await api.post<PasswordResetResponse>(
'/api/local/password-reset',
{ token, password },
{ headers }
);
return response.data;
}
/** /**
* Register with Microsoft account * Register with Microsoft account
* Endpoint: POST /api/msft/register * Endpoint: POST /api/msft/register

View file

@ -12,11 +12,15 @@ import {
registerWithMsalApi, registerWithMsalApi,
checkUsernameAvailabilityApi, checkUsernameAvailabilityApi,
logoutApi, logoutApi,
requestPasswordResetApi,
resetPasswordApi,
type LoginResponse, type LoginResponse,
type RegisterResponse, type RegisterResponse,
type UsernameAvailabilityResponse, type UsernameAvailabilityResponse,
type RegisterData, type RegisterData,
type MsalRegisterData type MsalRegisterData,
type PasswordResetRequestResponse,
type PasswordResetResponse
} from '../api/authApi'; } from '../api/authApi';
// Regular authentication // Regular authentication
@ -480,6 +484,84 @@ export function useUsernameAvailability() {
}; };
} }
// Password reset request (by username)
export function usePasswordResetRequest() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const requestReset = async (username: string): Promise<PasswordResetRequestResponse> => {
setIsLoading(true);
setError(null);
setSuccess(false);
try {
const response = await requestPasswordResetApi(username);
setSuccess(true);
return response;
} catch (error: any) {
// For security, we don't reveal if the username exists or not
// So we still show success even on error
setSuccess(true);
return { success: true, message: 'If a user with this username exists, a reset link has been sent to their email.' };
} finally {
setIsLoading(false);
}
};
return {
requestReset,
isLoading,
error,
success
};
}
// Password reset (set new password with token)
export function usePasswordReset() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const resetPassword = async (token: string, password: string): Promise<PasswordResetResponse> => {
setIsLoading(true);
setError(null);
setSuccess(false);
try {
const response = await resetPasswordApi(token, password);
setSuccess(true);
return response;
} catch (error: any) {
let errorMessage = 'Passwort-Zurücksetzung fehlgeschlagen';
if (error.response) {
if (error.response.data?.detail) {
if (Array.isArray(error.response.data.detail)) {
errorMessage = error.response.data.detail.map((err: any) => err.msg || err).join(', ');
} else {
errorMessage = error.response.data.detail;
}
}
} else if (error.message) {
errorMessage = error.message;
}
setError(errorMessage);
throw error;
} finally {
setIsLoading(false);
}
};
return {
resetPassword,
isLoading,
error,
success
};
}
// Logout function // Logout function
export function useLogout() { export function useLogout() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);

View file

@ -263,3 +263,18 @@ button:disabled {
text-align: center; text-align: center;
font-family: var(--font-family); font-family: var(--font-family);
} }
.passwordResetLink {
display: flex;
justify-content: center;
margin-top: -0.5rem;
}
.passwordResetLink .textButton {
color: #9CA3AF;
font-size: 0.85rem;
}
.passwordResetLink .textButton:hover {
color: #F25843;
}

View file

@ -152,6 +152,15 @@ function Login() {
{isLoginLoading ? "wird geladen..." : "Anmelden"} {isLoginLoading ? "wird geladen..." : "Anmelden"}
</button> </button>
<div className={styles.passwordResetLink}>
<button
className={styles.textButton}
onClick={() => navigate("/password-reset-request")}
>
Passwort vergessen?
</button>
</div>
<div className={styles.divider}> <div className={styles.divider}>
<span>oder</span> <span>oder</span>
</div> </div>

View file

@ -0,0 +1,242 @@
.container {
display: flex;
min-height: 100vh;
font-family: "DM Sans", sans-serif;
color: #181818;
}
.mainContent {
flex: 1;
display: flex;
flex-direction: column;
padding: 3rem;
background-color: #181818;
}
.loginSection {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.logoText {
font-size: 35px;
display: flex;
align-items: center;
letter-spacing: -0.5px;
font-weight: 200;
}
.logoPower {
color: #E5E7EB;
}
.logoOn {
color: #F25843;
font-weight: 700;
}
.logo img {
height: 40px;
}
.loginBox {
background-color: #181818;
width: 25%;
height: auto;
margin-top: 5%;
padding: 2rem;
border-radius: 25px;
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
.title {
font-family: "DM Sans", sans-serif;
color: #E5E7EB;
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 1.5rem;
text-align: center;
}
.loginForm {
display: flex;
flex-direction: column;
gap: 1rem;
}
.floatingLabelInput {
position: relative;
}
.label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #C7C5B2;
font-size: 1rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: transparent;
font-family: var(--font-family);
}
.focusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: #F25843;
font-size: 0.85rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: #181818;
padding: 0 4px;
font-family: var(--font-family);
font-weight: 500;
}
.input {
width: 100%;
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 25px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
}
.input:focus {
outline: none;
border-color: #F25843;
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
}
.input::placeholder {
color: transparent;
}
/* Fix browser autocomplete styling */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
-webkit-text-fill-color: #E5E7EB !important;
background-color: #181818 !important;
transition: background-color 5000s ease-in-out 0s;
}
/* Ensure label background matches when autofilled */
.input:-webkit-autofill + .label,
.input:-webkit-autofill + .focusedLabel {
background-color: #181818 !important;
}
.button {
width: 100%;
height: 50px;
padding: 12px 20px;
border-radius: 25px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
text-align: center;
}
.loginButton {
background-color: #F25843;
color: #E5E7EB;
}
.loginButton:hover {
background-color: var(--color-secondary-hover);
}
.registerLink {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.registerLink span {
color: #E5E7EB;
font-size: 0.8rem;
}
.textButton {
background: none;
border: none;
color: var(--color-secondary);
font-weight: 500;
cursor: pointer;
padding: 0;
font-size: 0.9rem;
font-family: var(--font-family);
}
.textButton:hover {
text-decoration: underline;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.error {
color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
border-radius: 25px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);
margin-bottom: 10px;
}
.success {
color: #10b981;
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid #10b981;
border-radius: 25px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);
margin-bottom: 10px;
}
.infoMessage {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
padding: 12px;
font-size: 0.85rem;
color: #93c5fd;
text-align: center;
font-family: var(--font-family);
}
.infoMessage p {
margin: 0;
}

View file

@ -0,0 +1,129 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import styles from './PasswordResetRequest.module.css';
import { usePasswordResetRequest } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
function PasswordResetRequest() {
const navigate = useNavigate();
const { requestReset, isLoading, success } = usePasswordResetRequest();
const [username, setUsername] = useState('');
const [usernameFocused, setUsernameFocused] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Set page title and generate CSRF token
useEffect(() => {
document.title = "PowerOn AI Platform - Passwort zurücksetzen";
generateAndStoreCSRFToken();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setValidationError(null);
if (!username.trim()) {
setValidationError('Bitte geben Sie Ihren Benutzernamen ein.');
return;
}
try {
await requestReset(username.trim());
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
// Redirect to login after delay
setTimeout(() => {
navigate('/login', {
state: {
passwordResetRequested: true,
message: 'Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.'
}
});
}, 5000);
} catch (err) {
// For security, still show success message even on error
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
setTimeout(() => {
navigate('/login');
}, 5000);
}
};
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.logo}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>Passwort zurücksetzen</h2>
<div className={styles.loginForm}>
{validationError && (
<div className={styles.error}>{validationError}</div>
)}
{successMessage && (
<div className={styles.success}>{successMessage}</div>
)}
{!successMessage && (
<>
<div className={styles.floatingLabelInput}>
<input
type="text"
placeholder=" "
value={username}
onChange={(e) => {
setUsername(e.target.value);
setValidationError(null);
}}
onFocus={() => setUsernameFocused(true)}
onBlur={() => setUsernameFocused(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit(e);
}
}}
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
/>
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
</div>
<div className={styles.infoMessage}>
<p>Geben Sie Ihren Benutzernamen ein. Falls ein Konto existiert, erhalten Sie einen Link zum Zurücksetzen des Passworts an Ihre hinterlegte E-Mail-Adresse.</p>
</div>
<button
className={`${styles.button} ${styles.loginButton}`}
onClick={handleSubmit}
disabled={isLoading}
>
{isLoading ? "Wird gesendet..." : "Reset-Link anfordern"}
</button>
</>
)}
<div className={styles.registerLink}>
<span>Zurück zum</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default PasswordResetRequest;

View file

@ -227,3 +227,30 @@ button:disabled {
font-family: var(--font-family); font-family: var(--font-family);
margin-bottom: 10px; margin-bottom: 10px;
} }
.success {
color: #10b981;
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid #10b981;
border-radius: 25px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);
margin-bottom: 10px;
}
.infoMessage {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
padding: 12px;
font-size: 0.85rem;
color: #93c5fd;
text-align: center;
font-family: var(--font-family);
}
.infoMessage p {
margin: 0;
}

View file

@ -7,8 +7,6 @@ import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
interface RegisterFormData { interface RegisterFormData {
username: string; username: string;
password: string;
confirmPassword: string;
email: string; email: string;
fullName: string; fullName: string;
} }
@ -20,15 +18,12 @@ function Register() {
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability(); const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
const [formData, setFormData] = useState<RegisterFormData>({ const [formData, setFormData] = useState<RegisterFormData>({
username: '', username: '',
password: '',
confirmPassword: '',
email: '', email: '',
fullName: '' fullName: ''
}); });
const [validationError, setValidationError] = useState<string | null>(null); const [validationError, setValidationError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [usernameFocused, setUsernameFocused] = useState(false); const [usernameFocused, setUsernameFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);
const [emailFocused, setEmailFocused] = useState(false); const [emailFocused, setEmailFocused] = useState(false);
const [fullNameFocused, setFullNameFocused] = useState(false); const [fullNameFocused, setFullNameFocused] = useState(false);
const [usernameHighlight, setUsernameHighlight] = useState(false); const [usernameHighlight, setUsernameHighlight] = useState(false);
@ -55,16 +50,11 @@ function Register() {
}; };
const validateForm = (): boolean => { const validateForm = (): boolean => {
if (!formData.username || !formData.password || !formData.confirmPassword || !formData.email || !formData.fullName) { if (!formData.username || !formData.email || !formData.fullName) {
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.'); setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
return false; return false;
} }
if (formData.password !== formData.confirmPassword) {
setValidationError('Die Passwörter stimmen nicht überein.');
return false;
}
if (!formData.email.includes('@')) { if (!formData.email.includes('@')) {
setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.'); setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
return false; return false;
@ -96,15 +86,21 @@ function Register() {
return; return;
} }
// Username is available, proceed with registration // Username is available, proceed with registration (no password - magic link flow)
const { confirmPassword, ...registrationData } = formData; await register(formData);
await register(registrationData);
navigate('/login', { // Show success message instead of immediate redirect
state: { setSuccessMessage('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.');
registered: true,
message: 'Registration erfolgreich. Bitte melden Sie sich an.' // Redirect to login page after delay
} setTimeout(() => {
}); navigate('/login', {
state: {
registered: true,
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.'
}
});
}, 5000);
} catch (err) { } catch (err) {
console.error('Registration failed:', err); console.error('Registration failed:', err);
} }
@ -135,89 +131,73 @@ function Register() {
<div className={styles.error}>{getErrorMessage()}</div> <div className={styles.error}>{getErrorMessage()}</div>
)} )}
<div className={styles.floatingLabelInput}> {successMessage && (
<input <div className={styles.success}>{successMessage}</div>
type="text" )}
name="username"
placeholder=" " {!successMessage && (
value={formData.username} <>
onChange={handleInputChange} <div className={styles.floatingLabelInput}>
onFocus={() => setUsernameFocused(true)} <input
onBlur={() => setUsernameFocused(false)} type="text"
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`} name="username"
/> placeholder=" "
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>Benutzername</label> value={formData.username}
</div> onChange={handleInputChange}
onFocus={() => setUsernameFocused(true)}
onBlur={() => setUsernameFocused(false)}
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`}
/>
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>Benutzername</label>
</div>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
<input <input
type="email" type="email"
name="email" name="email"
placeholder=" " placeholder=" "
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={() => setEmailFocused(true)} onFocus={() => setEmailFocused(true)}
onBlur={() => setEmailFocused(false)} onBlur={() => setEmailFocused(false)}
className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`} className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`}
/> />
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label> <label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label>
</div> </div>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
<input <input
type="text" type="text"
name="fullName" name="fullName"
placeholder=" " placeholder=" "
value={formData.fullName} value={formData.fullName}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={() => setFullNameFocused(true)} onFocus={() => setFullNameFocused(true)}
onBlur={() => setFullNameFocused(false)} onBlur={() => setFullNameFocused(false)}
className={`${styles.input} ${fullNameFocused || formData.fullName ? styles.focused : ''}`} className={`${styles.input} ${fullNameFocused || formData.fullName ? styles.focused : ''}`}
/> />
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>Vollständiger Name</label> <label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>Vollständiger Name</label>
</div> </div>
<div className={styles.floatingLabelInput}> <div className={styles.infoMessage}>
<input <p>Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.</p>
type="password" </div>
name="password"
placeholder=" "
value={formData.password}
onChange={handleInputChange}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
className={`${styles.input} ${passwordFocused || formData.password ? styles.focused : ''}`}
/>
<label className={passwordFocused || formData.password ? styles.focusedLabel : styles.label}>Passwort</label>
</div>
<div className={styles.floatingLabelInput}> <div className={styles.disclaimer}>
<input <p>
type="password" Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.
name="confirmPassword" </p>
placeholder=" " </div>
value={formData.confirmPassword}
onChange={handleInputChange}
onFocus={() => setConfirmPasswordFocused(true)}
onBlur={() => setConfirmPasswordFocused(false)}
className={`${styles.input} ${confirmPasswordFocused || formData.confirmPassword ? styles.focused : ''}`}
/>
<label className={confirmPasswordFocused || formData.confirmPassword ? styles.focusedLabel : styles.label}>Passwort bestätigen</label>
</div>
<div className={styles.disclaimer}> <button
<p> className={`${styles.button} ${styles.loginButton}`}
Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu. onClick={handleSubmit}
</p> disabled={isLoading || isChecking}
</div> >
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : "Registrieren"}
<button </button>
className={`${styles.button} ${styles.loginButton}`} </>
onClick={handleSubmit} )}
disabled={isLoading || isChecking}
>
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : "Registrieren"}
</button>
<div className={styles.registerLink}> <div className={styles.registerLink}>
<span>Bereits registriert?</span> <span>Bereits registriert?</span>

234
src/pages/Reset.module.css Normal file
View file

@ -0,0 +1,234 @@
.container {
display: flex;
min-height: 100vh;
font-family: "DM Sans", sans-serif;
color: #181818;
}
.mainContent {
flex: 1;
display: flex;
flex-direction: column;
padding: 3rem;
background-color: #181818;
}
.loginSection {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.logoText {
font-size: 35px;
display: flex;
align-items: center;
letter-spacing: -0.5px;
font-weight: 200;
}
.logoPower {
color: #E5E7EB;
}
.logoOn {
color: #F25843;
font-weight: 700;
}
.logo img {
height: 40px;
}
.loginBox {
background-color: #181818;
width: 25%;
height: auto;
margin-top: 5%;
padding: 2rem;
border-radius: 25px;
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
.title {
font-family: "DM Sans", sans-serif;
color: #E5E7EB;
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 1.5rem;
text-align: center;
}
.loginForm {
display: flex;
flex-direction: column;
gap: 1rem;
}
.floatingLabelInput {
position: relative;
}
.label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #C7C5B2;
font-size: 1rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: transparent;
font-family: var(--font-family);
}
.focusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: #F25843;
font-size: 0.85rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: #181818;
padding: 0 4px;
font-family: var(--font-family);
font-weight: 500;
}
.input {
width: 100%;
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 25px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
}
.input:focus {
outline: none;
border-color: #F25843;
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
}
.input::placeholder {
color: transparent;
}
/* Fix browser autocomplete styling */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
-webkit-text-fill-color: #E5E7EB !important;
background-color: #181818 !important;
transition: background-color 5000s ease-in-out 0s;
}
/* Ensure label background matches when autofilled */
.input:-webkit-autofill + .label,
.input:-webkit-autofill + .focusedLabel {
background-color: #181818 !important;
}
.passwordHint {
font-size: 0.8rem;
color: #9CA3AF;
margin-top: -0.5rem;
padding-left: 16px;
}
.button {
width: 100%;
height: 50px;
padding: 12px 20px;
border-radius: 25px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
text-align: center;
}
.loginButton {
background-color: #F25843;
color: #E5E7EB;
}
.loginButton:hover {
background-color: var(--color-secondary-hover);
}
.registerLink {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.registerLink span {
color: #E5E7EB;
font-size: 0.8rem;
}
.textButton {
background: none;
border: none;
color: var(--color-secondary);
font-weight: 500;
cursor: pointer;
padding: 0;
font-size: 0.9rem;
font-family: var(--font-family);
}
.textButton:hover {
text-decoration: underline;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.error {
color: var(--color-secondary);
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
border-radius: 25px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);
margin-bottom: 10px;
}
.success {
color: #10b981;
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid #10b981;
border-radius: 25px;
padding: 12px;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);
margin-bottom: 10px;
}

219
src/pages/Reset.tsx Normal file
View file

@ -0,0 +1,219 @@
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import styles from './Reset.module.css';
import { usePasswordReset } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
function Reset() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { resetPassword, isLoading, error } = usePasswordReset();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordFocused, setPasswordFocused] = useState(false);
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [tokenError, setTokenError] = useState<string | null>(null);
// Get token from URL
const token = searchParams.get('token');
// Set page title and generate CSRF token
useEffect(() => {
document.title = "PowerOn AI Platform - Neues Passwort setzen";
generateAndStoreCSRFToken();
// Validate token exists and format
if (!token) {
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
} else if (!_isValidUUID(token)) {
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
}
}, [token]);
const _isValidUUID = (str: string): boolean => {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
};
const validateForm = (): boolean => {
if (!password || password.length < 8) {
setValidationError('Passwort muss mindestens 8 Zeichen lang sein.');
return false;
}
if (password !== confirmPassword) {
setValidationError('Die Passwörter stimmen nicht überein.');
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setValidationError(null);
if (!validateForm()) {
return;
}
if (!token) {
setValidationError('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.');
return;
}
try {
await resetPassword(token, password);
setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...');
// Redirect to login after delay
setTimeout(() => {
navigate('/login', {
state: {
passwordReset: true,
message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.'
}
});
}, 3000);
} catch (err: any) {
// Error is already set by the hook
const errorMessage = err?.response?.data?.detail || err?.message || 'Passwort-Zurücksetzung fehlgeschlagen.';
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
setValidationError('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.');
} else {
setValidationError(errorMessage);
}
}
};
// Show token error if invalid
if (tokenError) {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.logo}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>Neues Passwort setzen</h2>
<div className={styles.loginForm}>
<div className={styles.error}>{tokenError}</div>
<div className={styles.registerLink}>
<button
className={styles.textButton}
onClick={() => navigate("/password-reset-request")}
>
Neuen Reset-Link anfordern
</button>
</div>
<div className={styles.registerLink}>
<span>oder zurück zum</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div className={styles.logo}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>Neues Passwort setzen</h2>
<div className={styles.loginForm}>
{(validationError || error) && (
<div className={styles.error}>{validationError || error}</div>
)}
{successMessage && (
<div className={styles.success}>{successMessage}</div>
)}
{!successMessage && (
<form onSubmit={handleSubmit}>
<div className={styles.floatingLabelInput}>
<input
type="password"
placeholder=" "
value={password}
onChange={(e) => {
setPassword(e.target.value);
setValidationError(null);
}}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>Neues Passwort</label>
</div>
<div className={styles.passwordHint}>Mindestens 8 Zeichen</div>
<div className={styles.floatingLabelInput}>
<input
type="password"
placeholder=" "
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
setValidationError(null);
}}
onFocus={() => setConfirmPasswordFocused(true)}
onBlur={() => setConfirmPasswordFocused(false)}
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>Passwort bestätigen</label>
</div>
<button
type="submit"
className={`${styles.button} ${styles.loginButton}`}
disabled={isLoading}
>
{isLoading ? "Wird gespeichert..." : "Passwort setzen"}
</button>
</form>
)}
<div className={styles.registerLink}>
<span>Zurück zum</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
Login
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Reset;