wiki/ui_nyla/feature-userregistration/doc_userregistration_ui_adaptations.md
2026-01-11 13:07:11 +01:00

25 KiB

User Registration & Password Reset - UI Adaptations

Overview

This document describes the necessary UI changes and adaptations required in the Nyla frontend (frontend_nyla) to implement the magic link-based user authentication process described in doc_userauth_process_concept.md.

Last Updated: Based on codebase analysis of current Nyla frontend implementation.

Frontend Stack: React 19, TypeScript, Vite, React Router
Pattern: Hooks-based architecture (useAuthentication.ts), component-based pages

Current Frontend State

Existing Pages

  1. src/pages/Register.tsx

    • Contains registration form with:
      • Username field (with availability check)
      • Password field (required)
      • Confirm password field (required)
      • Email field
      • Full name field
      • Language selector (defaults to 'de')
    • Uses useRegister() hook from useAuthentication.ts
    • Needs modification: Remove password fields, add email-only registration
  2. src/pages/Login.tsx

    • Contains login form with:
      • Username field
      • Password field
      • Microsoft authentication button
      • Google authentication button (placeholder)
      • Registration link
    • Missing: Password reset button/link
  3. src/hooks/useAuthentication.ts

    • Contains useRegister() hook that sends password
    • Contains useAuth() hook for login
    • Contains useUsernameAvailability() hook
    • Needs modification: Update useRegister() to handle no-password registration
    • Missing: Password reset request and reset password hooks
  4. src/api.ts

    • Axios instance with interceptors
    • Base URL from environment variables
    • No changes needed - existing API setup is sufficient

Required UI Changes

1. Login Page (src/pages/Login.tsx)

Changes Required:

  • Add "Password Reset" link/button below the login form
  • Link should navigate to /password-reset-request route
  • Style should match existing link styles (use styles.textButton)

Implementation:

// Add after the disclaimer div, before the login button
<div className={styles.passwordResetLink}>
  <button 
    className={styles.textButton}
    onClick={() => navigate("/password-reset-request")}
  >
    Passwort zurücksetzen
  </button>
</div>

Styling Considerations:

  • Use existing CSS classes from Login.module.css
  • Match styling with registration link
  • Ensure responsive design matches login page layout

2. Registration Page (src/pages/Register.tsx)

Changes Required:

  • Remove password and confirm password fields
  • Keep username, email, fullName fields
  • Update form validation to not require password
  • Update success message to indicate email will be sent
  • Add spam folder reminder message

Implementation Changes:

Remove password-related state:

// Remove these lines:
const [passwordFocused, setPasswordFocused] = useState(false);
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);

// Update formData interface:
interface RegisterFormData {
  username: string;
  // password: string; // REMOVED
  // confirmPassword: string; // REMOVED
  email: string;
  fullName: string;
}

// Update initial state:
const [formData, setFormData] = useState<RegisterFormData>({
  username: '',
  // password: '', // REMOVED
  // confirmPassword: '', // REMOVED
  email: '',
  fullName: ''
});

Update validation:

const validateForm = (): boolean => {
  // Remove password checks
  if (!formData.username || !formData.email || !formData.fullName) {
    setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
    return false;
  }

  if (!formData.email.includes('@')) {
    setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
    return false;
  }

  return true;
};

Update success message:

// After successful registration:
await register(registrationData);
navigate('/login', { 
  state: { 
    registered: true,
    message: 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail, um Ihr Passwort zu setzen. Falls Sie keine E-Mail erhalten, prüfen Sie bitte auch Ihren Spam-Ordner.' 
  } 
});

Remove password input fields from JSX:

// Remove these divs:
{/* <div className={styles.floatingLabelInput}>
  <input type="password" name="password" ... />
  <label>Passwort</label>
</div>
<div className={styles.floatingLabelInput}>
  <input type="password" name="confirmPassword" ... />
  <label>Passwort bestätigen</label>
</div> */}

Add info message:

// Add after the disclaimer, before the submit button:
<div className={styles.infoMessage}>
  <p>Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.</p>
  <p className={styles.spamReminder}>Bitte prüfen Sie auch Ihren Spam-Ordner, falls Sie keine E-Mail erhalten.</p>
</div>

3. New Page: Password Reset Request (src/pages/PasswordResetRequest.tsx)

Purpose:

Allow users to request a password reset by entering their email address.

Structure:

  • Similar layout to Register.tsx and Login.tsx
  • Single email input field
  • Submit button
  • Link back to login page
  • Success/error message area

Implementation:

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePasswordResetRequest } from '../hooks/useAuthentication';
import styles from './PasswordResetRequest.module.css';

function PasswordResetRequest() {
  const navigate = useNavigate();
  const { requestPasswordReset, error, isLoading } = usePasswordResetRequest();
  const [email, setEmail] = useState('');
  const [emailFocused, setEmailFocused] = useState(false);
  const [successMessage, setSuccessMessage] = useState<string | null>(null);

  useEffect(() => {
    document.title = "PowerOn AI Platform - Passwort zurücksetzen";
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!email || !email.includes('@')) {
      return;
    }

    try {
      await requestPasswordReset(email);
      setSuccessMessage('Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet. Bitte prüfen Sie Ihre E-Mail und auch Ihren Spam-Ordner.');
      
      // Redirect to login after showing message
      setTimeout(() => {
        navigate('/login', {
          state: {
            message: 'Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.'
          }
        });
      }, 3000);
    } catch (err) {
      console.error('Password reset request failed:', err);
    }
  };

  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}>
            <div className={styles.loginForm}>
              {error && (
                <div className={styles.error}>{error}</div>
              )}
              {successMessage && (
                <div className={styles.success}>{successMessage}</div>
              )}
              
              <h2 className={styles.title}>Passwort zurücksetzen</h2>
              
              <div className={styles.floatingLabelInput}>
                <input
                  type="email"
                  name="email"
                  placeholder=" "
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  onFocus={() => setEmailFocused(true)}
                  onBlur={() => setEmailFocused(false)}
                  className={`${styles.input} ${emailFocused || email ? styles.focused : ''}`}
                />
                <label className={emailFocused || email ? styles.focusedLabel : styles.label}>
                  E-Mail-Adresse
                </label>
              </div>

              <button 
                className={`${styles.button} ${styles.loginButton}`}
                onClick={handleSubmit}
                disabled={isLoading || !email}
              >
                {isLoading ? "wird geladen..." : "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;

CSS Module (src/pages/PasswordResetRequest.module.css):

  • Copy styles from Register.module.css or Login.module.css
  • Add styles for .success message (green background)
  • Add styles for .title heading

4. New Page: Password Reset (src/pages/ResetPassword.tsx)

Purpose:

Allow users to set a new password using the token from the magic link.

Structure:

  • Similar layout to Register.tsx
  • Password field (with strength indicator)
  • Confirm password field
  • Submit button
  • Extract token from URL parameter (?token=<UUID>)
  • Success/error message area

Implementation:

import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useResetPassword } from '../hooks/useAuthentication';
import styles from './ResetPassword.module.css';

function ResetPassword() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();
  const { resetPassword, error, isLoading } = useResetPassword();
  
  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 token = searchParams.get('token');

  useEffect(() => {
    document.title = "PowerOn AI Platform - Neues Passwort setzen";
    
    // Validate token format (UUID)
    if (!token || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(token)) {
      setValidationError('Ungültiger oder fehlender Reset-Token.');
    }
  }, [token]);

  const validateForm = (): boolean => {
    if (!password || !confirmPassword) {
      setValidationError('Bitte füllen Sie alle Felder aus.');
      return false;
    }

    if (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();
    
    if (!validateForm() || !token) {
      return;
    }

    try {
      await resetPassword(token, password);
      setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...');
      
      // Redirect to login after 3 seconds
      setTimeout(() => {
        navigate('/login', {
          state: {
            message: 'Passwort erfolgreich gesetzt. Bitte melden Sie sich an.'
          }
        });
      }, 3000);
    } catch (err) {
      console.error('Password reset failed:', err);
    }
  };

  if (!token) {
    return (
      <div className={styles.container}>
        <div className={styles.mainContent}>
          <div className={styles.error}>
            Ungültiger oder fehlender Reset-Token. Bitte fordern Sie einen neuen Reset-Link an.
          </div>
          <button 
            className={styles.button}
            onClick={() => navigate('/password-reset-request')}
          >
            Reset-Link anfordern
          </button>
        </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}>
            <div className={styles.loginForm}>
              <h2 className={styles.title}>Neues Passwort setzen</h2>
              
              {(validationError || error) && (
                <div className={styles.error}>{validationError || error}</div>
              )}
              {successMessage && (
                <div className={styles.success}>{successMessage}</div>
              )}
              
              <div className={styles.floatingLabelInput}>
                <input
                  type="password"
                  name="password"
                  placeholder=" "
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  onFocus={() => setPasswordFocused(true)}
                  onBlur={() => setPasswordFocused(false)}
                  className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
                />
                <label className={passwordFocused || password ? styles.focusedLabel : styles.label}>
                  Neues Passwort
                </label>
              </div>
              <small className={styles.passwordHint}>Mindestens 8 Zeichen</small>

              <div className={styles.floatingLabelInput}>
                <input
                  type="password"
                  name="confirmPassword"
                  placeholder=" "
                  value={confirmPassword}
                  onChange={(e) => setConfirmPassword(e.target.value)}
                  onFocus={() => setConfirmPasswordFocused(true)}
                  onBlur={() => setConfirmPasswordFocused(false)}
                  className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
                />
                <label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>
                  Passwort bestätigen
                </label>
              </div>

              <button 
                className={`${styles.button} ${styles.loginButton}`}
                onClick={handleSubmit}
                disabled={isLoading || !password || !confirmPassword}
              >
                {isLoading ? "wird geladen..." : "Passwort setzen"}
              </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 ResetPassword;

CSS Module (src/pages/ResetPassword.module.css):

  • Copy styles from Register.module.css
  • Add styles for .success message
  • Add styles for .title heading
  • Add styles for .passwordHint (small text below password field)

5. Authentication Hooks (src/hooks/useAuthentication.ts)

New Hooks Required:

  1. usePasswordResetRequest()
export function usePasswordResetRequest() {
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const requestPasswordReset = async (email: string): Promise<void> => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await api.post('/api/local/password-reset-request', { email });
      
      // Backend always returns success (security: don't reveal if email exists)
      // No need to check response data
    } catch (error: any) {
      let errorMessage = 'An error occurred during password reset request';
      
      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).join(', ');
          } else {
            errorMessage = error.response.data.detail;
          }
        }
      } else if (error.request) {
        errorMessage = 'No response received from server';
      } else {
        errorMessage = error.message;
      }
      
      setError(errorMessage);
      throw error;
    } finally {
      setIsLoading(false);
    }
  };

  return {
    requestPasswordReset,
    error,
    isLoading
  };
}
  1. useResetPassword()
export function useResetPassword() {
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const resetPassword = async (token: string, password: string): Promise<void> => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await api.post('/api/local/password-reset', { token, password });
      
      // Success - password reset completed
    } catch (error: any) {
      let errorMessage = 'An error occurred during password reset';
      
      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).join(', ');
          } else {
            errorMessage = error.response.data.detail;
          }
        }
      } else if (error.request) {
        errorMessage = 'No response received from server';
      } else {
        errorMessage = error.message;
      }
      
      setError(errorMessage);
      throw error;
    } finally {
      setIsLoading(false);
    }
  };

  return {
    resetPassword,
    error,
    isLoading
  };
}

Update useRegister() Hook:

Modify the register function:

const register = async (userData: RegisterData): Promise<RegisterResponse> => {
  setIsLoading(true);
  setError(null);

  try {
    // Remove password from dataToSend
    const dataToSend = {
      userData: {
        username: userData.username,
        email: userData.email,
        fullName: userData.fullName,
        language: userData.language || 'de',
        enabled: userData.enabled !== undefined ? userData.enabled : true,
        privilege: userData.privilege || 'user'
      }
      // password: userData.password // REMOVED
    };

    const response = await api.post('/api/local/register', dataToSend, {
      headers: {
        'Content-Type': 'application/json'
      }
    });

    return {
      success: true,
      message: 'Registration successful. Please check your email to set your password.',
      user: response.data
    };
  } catch (error: any) {
    // ... existing error handling ...
  } finally {
    setIsLoading(false);
  }
};

Update RegisterData interface:

interface RegisterData {
  username: string;
  // password: string; // REMOVED - no longer required
  email: string;
  fullName: string;
  language?: string;
  enabled?: boolean;
  privilege?: string;
}

6. Routing Configuration

Update src/App.tsx or routing configuration:

Add routes for new pages:

import PasswordResetRequest from './pages/PasswordResetRequest';
import ResetPassword from './pages/ResetPassword';

// In your routes:
<Route path="/password-reset-request" element={<PasswordResetRequest />} />
<Route path="/reset" element={<ResetPassword />} />

UI/UX Considerations

Error Handling

  1. Registration Errors:

    • Username already exists → Show error on username field (already implemented)
    • Email already exists → Show generic success (security: don't reveal email exists)
    • Email sending fails → Show generic success (don't reveal email issues)
  2. Password Reset Request Errors:

    • Invalid email format → Show error on email field
    • Email not found → Show generic success (security: don't reveal email doesn't exist)
    • Rate limiting → Show error message
  3. Password Reset Errors:

    • Invalid/expired token → Show error message, link back to password reset request
    • Password too weak → Show specific requirements
    • Password mismatch → Show error on confirm password field

Success Messages

  1. Registration Success:

    "Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail, um Ihr Passwort zu setzen.
    Falls Sie keine E-Mail erhalten, prüfen Sie bitte auch Ihren Spam-Ordner."
    
  2. Password Reset Request Success:

    "Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.
    Bitte prüfen Sie Ihre E-Mail und auch Ihren Spam-Ordner."
    
  3. Password Reset Success:

    "Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet..."
    

Accessibility

  • All form fields should have proper labels (already implemented with floating labels)
  • Error messages should be associated with form fields using ARIA attributes
  • Success messages should be announced to screen readers
  • Form validation should provide clear, actionable feedback

Responsive Design

  • All pages should work on mobile devices (existing styles should handle this)
  • Form layouts should adapt to smaller screens
  • Buttons should be appropriately sized for touch interfaces
  • Error messages should be readable on all screen sizes

Testing Checklist

Registration Flow

  • User can register without password
  • Email validation works correctly
  • Success message displays correctly
  • Redirect to login works
  • Error handling for duplicate username
  • Error handling for duplicate email (should show generic success)

Password Reset Request Flow

  • User can access password reset request page from login
  • Email validation works correctly
  • Success message displays correctly
  • Redirect to login works
  • Error handling for invalid email format
  • Error handling for rate limiting

Password Reset Flow

  • User can access reset page with valid token
  • Token extraction from URL works
  • Password validation works correctly
  • Password confirmation validation works
  • Success message displays correctly
  • Redirect to login works after 3 seconds
  • Error handling for invalid token
  • Error handling for expired token
  • Error handling for weak password

Integration Testing

  • End-to-end registration flow works
  • End-to-end password reset flow works
  • Email links work correctly
  • Token expiration handling works
  • Multiple reset requests invalidate old tokens

Implementation Order

  1. Backend Changes First (prerequisites):

    • Add resetToken fields to UserInDB model
    • Implement password reset endpoints
    • Implement email sending functionality
  2. Frontend Hooks:

    • Add usePasswordResetRequest() hook
    • Add useResetPassword() hook
    • Update useRegister() hook (remove password requirement)
  3. Frontend Pages:

    • Create PasswordResetRequest.tsx
    • Create ResetPassword.tsx
    • Update Login.tsx (add reset button)
    • Update Register.tsx (remove password fields)
  4. Routing:

    • Add routes for new pages
  5. Styling:

    • Create CSS modules for new pages
    • Add success message styles
    • Ensure consistent styling
  6. Testing:

    • Test each flow independently
    • Test integration between frontend and backend
    • Test error scenarios
    • Test edge cases

Notes

  • All text should be in German to match existing UI (Login.tsx uses German)
  • CSS modules should match existing patterns from Register.module.css and Login.module.css
  • Form validation should use existing patterns from Register.tsx
  • Error handling should use existing patterns (error state, error display)
  • Success messages should use new success state pattern
  • API calls should use existing api.ts instance
  • Hooks should follow existing patterns from useAuthentication.ts
  • Token validation should check UUID format before making API call
  • Use React Router's useSearchParams for token extraction from URL

File Structure

frontend_nyla/
├── src/
│   ├── pages/
│   │   ├── Login.tsx (MODIFY - add password reset link)
│   │   ├── Register.tsx (MODIFY - remove password fields)
│   │   ├── PasswordResetRequest.tsx (NEW)
│   │   ├── ResetPassword.tsx (NEW)
│   │   ├── Login.module.css (MODIFY - add password reset link styles)
│   │   ├── Register.module.css (MODIFY - add info message styles)
│   │   ├── PasswordResetRequest.module.css (NEW)
│   │   └── ResetPassword.module.css (NEW)
│   ├── hooks/
│   │   └── useAuthentication.ts (MODIFY - update useRegister, add new hooks)
│   └── App.tsx (MODIFY - add routes)