Browse Source

Add frontend support for forgot password and email login

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
copilot-swe-agent[bot] 3 months ago
parent
commit
6bdd5730de
2 changed files with 202 additions and 38 deletions
  1. 82 1
      frontend/src/api/client.ts
  2. 120 37
      frontend/src/pages/LoginPage.tsx

+ 82 - 1
frontend/src/api/client.ts

@@ -1843,6 +1843,7 @@ export interface LoginResponse {
 export interface UserResponse {
 export interface UserResponse {
   id: number;
   id: number;
   username: string;
   username: string;
+  email?: string;
   role: string;  // Deprecated, kept for backward compatibility
   role: string;  // Deprecated, kept for backward compatibility
   is_active: boolean;
   is_active: boolean;
   is_admin: boolean;  // Computed from role and group membership
   is_admin: boolean;  // Computed from role and group membership
@@ -1853,7 +1854,8 @@ export interface UserResponse {
 
 
 export interface UserCreate {
 export interface UserCreate {
   username: string;
   username: string;
-  password: string;
+  password?: string;  // Optional when advanced auth is enabled
+  email?: string;
   role: string;
   role: string;
   group_ids?: number[];
   group_ids?: number[];
 }
 }
@@ -1861,6 +1863,7 @@ export interface UserCreate {
 export interface UserUpdate {
 export interface UserUpdate {
   username?: string;
   username?: string;
   password?: string;
   password?: string;
+  email?: string;
   role?: string;
   role?: string;
   is_active?: boolean;
   is_active?: boolean;
   group_ids?: number[];
   group_ids?: number[];
@@ -1872,6 +1875,52 @@ export interface SetupRequest {
   admin_password?: string;
   admin_password?: string;
 }
 }
 
 
+export interface ForgotPasswordRequest {
+  email: string;
+}
+
+export interface ForgotPasswordResponse {
+  message: string;
+}
+
+export interface ResetPasswordRequest {
+  user_id: number;
+}
+
+export interface ResetPasswordResponse {
+  message: string;
+}
+
+export interface SMTPSettings {
+  smtp_host: string;
+  smtp_port: number;
+  smtp_username: string;
+  smtp_password?: string;
+  smtp_use_tls: boolean;
+  smtp_from_email: string;
+  smtp_from_name: string;
+}
+
+export interface TestSMTPRequest {
+  smtp_host: string;
+  smtp_port: number;
+  smtp_username: string;
+  smtp_password: string;
+  smtp_use_tls: boolean;
+  smtp_from_email: string;
+  test_recipient: string;
+}
+
+export interface TestSMTPResponse {
+  success: boolean;
+  message: string;
+}
+
+export interface AdvancedAuthStatus {
+  advanced_auth_enabled: boolean;
+  smtp_configured: boolean;
+}
+
 export interface SetupResponse {
 export interface SetupResponse {
   auth_enabled: boolean;
   auth_enabled: boolean;
   admin_created?: boolean;
   admin_created?: boolean;
@@ -1905,6 +1954,38 @@ export const api = {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
       method: 'POST',
       method: 'POST',
     }),
     }),
+  
+  // Advanced Authentication
+  testSMTP: (data: TestSMTPRequest) =>
+    request<TestSMTPResponse>('/auth/smtp/test', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  getSMTPSettings: () => request<SMTPSettings | null>('/auth/smtp'),
+  saveSMTPSettings: (data: SMTPSettings) =>
+    request<{ message: string }>('/auth/smtp', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  enableAdvancedAuth: () =>
+    request<{ message: string; advanced_auth_enabled: boolean }>('/auth/advanced-auth/enable', {
+      method: 'POST',
+    }),
+  disableAdvancedAuth: () =>
+    request<{ message: string; advanced_auth_enabled: boolean }>('/auth/advanced-auth/disable', {
+      method: 'POST',
+    }),
+  getAdvancedAuthStatus: () => request<AdvancedAuthStatus>('/auth/advanced-auth/status'),
+  forgotPassword: (data: ForgotPasswordRequest) =>
+    request<ForgotPasswordResponse>('/auth/forgot-password', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  resetUserPassword: (data: ResetPasswordRequest) =>
+    request<ResetPasswordResponse>('/auth/reset-password', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
 
 
   // Users
   // Users
   getUsers: () => request<UserResponse[]>('/users/'),
   getUsers: () => request<UserResponse[]>('/users/'),

+ 120 - 37
frontend/src/pages/LoginPage.tsx

@@ -1,11 +1,12 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
 import { useToast } from '../contexts/ToastContext';
 import { useToast } from '../contexts/ToastContext';
 import { useTheme } from '../contexts/ThemeContext';
 import { useTheme } from '../contexts/ThemeContext';
-import { HelpCircle, X } from 'lucide-react';
+import { HelpCircle, X, Mail } from 'lucide-react';
+import { api } from '../api/client';
 
 
 export function LoginPage() {
 export function LoginPage() {
   const navigate = useNavigate();
   const navigate = useNavigate();
@@ -16,6 +17,13 @@ export function LoginPage() {
   const [username, setUsername] = useState('');
   const [username, setUsername] = useState('');
   const [password, setPassword] = useState('');
   const [password, setPassword] = useState('');
   const [showForgotPassword, setShowForgotPassword] = useState(false);
   const [showForgotPassword, setShowForgotPassword] = useState(false);
+  const [forgotEmail, setForgotEmail] = useState('');
+
+  // Check if advanced auth is enabled
+  const { data: advancedAuthStatus } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
 
 
   const loginMutation = useMutation({
   const loginMutation = useMutation({
     mutationFn: () => login(username, password),
     mutationFn: () => login(username, password),
@@ -28,6 +36,18 @@ export function LoginPage() {
     },
     },
   });
   });
 
 
+  const forgotPasswordMutation = useMutation({
+    mutationFn: (email: string) => api.forgotPassword({ email }),
+    onSuccess: (data) => {
+      showToast(data.message, 'success');
+      setShowForgotPassword(false);
+      setForgotEmail('');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
   const handleSubmit = (e: React.FormEvent) => {
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     if (!username || !password) {
     if (!username || !password) {
@@ -37,6 +57,15 @@ export function LoginPage() {
     loginMutation.mutate();
     loginMutation.mutate();
   };
   };
 
 
+  const handleForgotPassword = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!forgotEmail) {
+      showToast('Please enter your email address', 'error');
+      return;
+    }
+    forgotPasswordMutation.mutate(forgotEmail);
+  };
+
   return (
   return (
     <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
     <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
       <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
       <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
@@ -60,7 +89,9 @@ export function LoginPage() {
           <div className="space-y-4">
           <div className="space-y-4">
             <div>
             <div>
               <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
               <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
-                {t('login.username')}
+                {advancedAuthStatus?.advanced_auth_enabled 
+                  ? t('login.usernameOrEmail') || 'Username or Email'
+                  : t('login.username')}
               </label>
               </label>
               <input
               <input
                 id="username"
                 id="username"
@@ -69,7 +100,9 @@ export function LoginPage() {
                 value={username}
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 onChange={(e) => setUsername(e.target.value)}
                 className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
                 className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                placeholder={t('login.usernamePlaceholder')}
+                placeholder={advancedAuthStatus?.advanced_auth_enabled 
+                  ? t('login.usernameOrEmailPlaceholder') || 'Enter your username or email'
+                  : t('login.usernamePlaceholder')}
                 autoComplete="username"
                 autoComplete="username"
               />
               />
             </div>
             </div>
@@ -101,22 +134,24 @@ export function LoginPage() {
             </button>
             </button>
           </div>
           </div>
 
 
-          <div className="text-center">
-            <button
-              type="button"
-              onClick={() => setShowForgotPassword(true)}
-              className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
-            >
-              {t('login.forgotPassword')}
-            </button>
-          </div>
+          {advancedAuthStatus?.advanced_auth_enabled && (
+            <div className="text-center">
+              <button
+                type="button"
+                onClick={() => setShowForgotPassword(true)}
+                className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
+              >
+                {t('login.forgotPassword')}
+              </button>
+            </div>
+          )}
         </form>
         </form>
       </div>
       </div>
 
 
       {/* Forgot Password Modal */}
       {/* Forgot Password Modal */}
       {showForgotPassword && (
       {showForgotPassword && (
         <div
         <div
-          className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
+          className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
           onClick={() => setShowForgotPassword(false)}
           onClick={() => setShowForgotPassword(false)}
         >
         >
           <div
           <div
@@ -125,39 +160,87 @@ export function LoginPage() {
           >
           >
             <div className="flex items-center justify-between mb-4">
             <div className="flex items-center justify-between mb-4">
               <div className="flex items-center gap-2">
               <div className="flex items-center gap-2">
-                <HelpCircle className="w-5 h-5 text-bambu-green" />
+                <Mail className="w-5 h-5 text-bambu-green" />
                 <h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
                 <h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
               </div>
               </div>
               <button
               <button
-                onClick={() => setShowForgotPassword(false)}
+                onClick={() => {
+                  setShowForgotPassword(false);
+                  setForgotEmail('');
+                }}
                 className="p-1 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
                 className="p-1 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
               >
               >
                 <X className="w-5 h-5" />
                 <X className="w-5 h-5" />
               </button>
               </button>
             </div>
             </div>
 
 
-            <div className="space-y-4">
-              <p className="text-bambu-gray">
-                {t('login.forgotPasswordMessage')}
-              </p>
-
-              <div className="bg-bambu-dark rounded-lg p-4 space-y-2">
-                <p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
-                <ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
-                  <li>{t('login.resetStep1')}</li>
-                  <li>{t('login.resetStep2')}</li>
-                  <li>{t('login.resetStep3')}</li>
-                  <li>{t('login.resetStep4')}</li>
-                </ol>
-              </div>
+            {advancedAuthStatus?.advanced_auth_enabled ? (
+              <form onSubmit={handleForgotPassword} className="space-y-4">
+                <p className="text-bambu-gray text-sm">
+                  {t('login.forgotPasswordEmailMessage') || 'Enter your email address and we\'ll send you a new password.'}
+                </p>
 
 
-              <button
-                onClick={() => setShowForgotPassword(false)}
-                className="w-full py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
-              >
-                {t('login.gotIt')}
-              </button>
-            </div>
+                <div>
+                  <label htmlFor="forgot-email" className="block text-sm font-medium text-white mb-2">
+                    {t('login.emailAddress') || 'Email Address'}
+                  </label>
+                  <input
+                    id="forgot-email"
+                    type="email"
+                    required
+                    value={forgotEmail}
+                    onChange={(e) => setForgotEmail(e.target.value)}
+                    className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
+                    placeholder={t('login.emailPlaceholder') || 'your.email@example.com'}
+                  />
+                </div>
+
+                <div className="flex gap-2">
+                  <button
+                    type="button"
+                    onClick={() => {
+                      setShowForgotPassword(false);
+                      setForgotEmail('');
+                    }}
+                    className="flex-1 py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
+                  >
+                    {t('login.cancel') || 'Cancel'}
+                  </button>
+                  <button
+                    type="submit"
+                    disabled={forgotPasswordMutation.isPending}
+                    className="flex-1 py-2 px-4 bg-bambu-green hover:bg-bambu-green-light text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+                  >
+                    {forgotPasswordMutation.isPending 
+                      ? (t('login.sending') || 'Sending...') 
+                      : (t('login.sendResetEmail') || 'Send Reset Email')}
+                  </button>
+                </div>
+              </form>
+            ) : (
+              <div className="space-y-4">
+                <p className="text-bambu-gray">
+                  {t('login.forgotPasswordMessage')}
+                </p>
+
+                <div className="bg-bambu-dark rounded-lg p-4 space-y-2">
+                  <p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
+                  <ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
+                    <li>{t('login.resetStep1')}</li>
+                    <li>{t('login.resetStep2')}</li>
+                    <li>{t('login.resetStep3')}</li>
+                    <li>{t('login.resetStep4')}</li>
+                  </ol>
+                </div>
+
+                <button
+                  onClick={() => setShowForgotPassword(false)}
+                  className="w-full py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
+                >
+                  {t('login.gotIt')}
+                </button>
+              </div>
+            )}
           </div>
           </div>
         </div>
         </div>
       )}
       )}