Просмотр исходного кода

Merge pull request #12 from cadtoolbox/copilot/add-advanced-authentication-toggle

Add advanced authentication with email-based login and SMTP notifications
Thomas Rambach 7 месяцев назад
Родитель
Сommit
a45edc3fae

+ 386 - 2
backend/app/api/routes/auth.py

@@ -8,16 +8,39 @@ from sqlalchemy.orm import selectinload
 from backend.app.core.auth import (
 from backend.app.core.auth import (
     ACCESS_TOKEN_EXPIRE_MINUTES,
     ACCESS_TOKEN_EXPIRE_MINUTES,
     authenticate_user,
     authenticate_user,
+    authenticate_user_by_email,
     create_access_token,
     create_access_token,
     get_current_active_user,
     get_current_active_user,
     get_password_hash,
     get_password_hash,
+    get_user_by_email,
     get_user_by_username,
     get_user_by_username,
 )
 )
 from backend.app.core.database import get_db
 from backend.app.core.database import get_db
 from backend.app.models.group import Group
 from backend.app.models.group import Group
 from backend.app.models.settings import Settings
 from backend.app.models.settings import Settings
 from backend.app.models.user import User
 from backend.app.models.user import User
-from backend.app.schemas.auth import GroupBrief, LoginRequest, LoginResponse, SetupRequest, SetupResponse, UserResponse
+from backend.app.schemas.auth import (
+    ForgotPasswordRequest,
+    ForgotPasswordResponse,
+    GroupBrief,
+    LoginRequest,
+    LoginResponse,
+    ResetPasswordRequest,
+    ResetPasswordResponse,
+    SetupRequest,
+    SetupResponse,
+    SMTPSettings,
+    TestSMTPRequest,
+    TestSMTPResponse,
+    UserResponse,
+)
+from backend.app.services.email_service import (
+    create_password_reset_email,
+    generate_secure_password,
+    get_smtp_settings,
+    save_smtp_settings,
+    send_email,
+)
 
 
 
 
 def _user_to_response(user: User) -> UserResponse:
 def _user_to_response(user: User) -> UserResponse:
@@ -25,6 +48,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
     return UserResponse(
         id=user.id,
         id=user.id,
         username=user.username,
         username=user.username,
+        email=user.email,
         role=user.role,
         role=user.role,
         is_active=user.is_active,
         is_active=user.is_active,
         is_admin=user.is_admin,
         is_admin=user.is_admin,
@@ -46,6 +70,27 @@ async def is_auth_enabled(db: AsyncSession) -> bool:
     return setting.value.lower() == "true"
     return setting.value.lower() == "true"
 
 
 
 
+async def is_advanced_auth_enabled(db: AsyncSession) -> bool:
+    """Check if advanced authentication is enabled."""
+    result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
+    setting = result.scalar_one_or_none()
+    if setting is None:
+        return False
+    return setting.value.lower() == "true"
+
+
+async def set_advanced_auth_enabled(db: AsyncSession, enabled: bool) -> None:
+    """Set advanced authentication enabled status."""
+    from sqlalchemy import func
+    from sqlalchemy.dialects.sqlite import insert as sqlite_insert
+
+    stmt = sqlite_insert(Settings).values(key="advanced_auth_enabled", value="true" if enabled else "false")
+    stmt = stmt.on_conflict_do_update(
+        index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
+    )
+    await db.execute(stmt)
+
+
 async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
 async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
     """Set authentication enabled status."""
     """Set authentication enabled status."""
     from sqlalchemy import func
     from sqlalchemy import func
@@ -216,7 +261,10 @@ async def disable_auth(
 
 
 @router.post("/login", response_model=LoginResponse)
 @router.post("/login", response_model=LoginResponse)
 async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
 async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
-    """Login and get access token."""
+    """Login and get access token.
+    
+    Supports username or email-based login. Username lookup is case-insensitive.
+    """
     # Check if auth is enabled
     # Check if auth is enabled
     auth_enabled = await is_auth_enabled(db)
     auth_enabled = await is_auth_enabled(db)
     if not auth_enabled:
     if not auth_enabled:
@@ -225,7 +273,15 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
             detail="Authentication is not enabled",
             detail="Authentication is not enabled",
         )
         )
 
 
+    # Try username-based authentication first
     user = await authenticate_user(db, request.username, request.password)
     user = await authenticate_user(db, request.username, request.password)
+    
+    # If username auth failed and advanced auth is enabled, try email-based authentication
+    if not user:
+        advanced_auth = await is_advanced_auth_enabled(db)
+        if advanced_auth:
+            user = await authenticate_user_by_email(db, request.username, request.password)
+    
     if not user:
     if not user:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -263,3 +319,331 @@ async def get_current_user_info(
 async def logout():
 async def logout():
     """Logout (client should discard token)."""
     """Logout (client should discard token)."""
     return {"message": "Logged out successfully"}
     return {"message": "Logged out successfully"}
+
+
+# Advanced Authentication Endpoints
+
+
+@router.post("/smtp/test", response_model=TestSMTPResponse)
+async def test_smtp_connection(
+    test_request: TestSMTPRequest,
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Test SMTP connection with provided settings (admin only)."""
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can test SMTP settings",
+        )
+
+    try:
+        smtp_settings = SMTPSettings(
+            smtp_host=test_request.smtp_host,
+            smtp_port=test_request.smtp_port,
+            smtp_username=test_request.smtp_username,
+            smtp_password=test_request.smtp_password,
+            smtp_use_tls=test_request.smtp_use_tls,
+            smtp_from_email=test_request.smtp_from_email,
+        )
+
+        # Send test email
+        send_email(
+            smtp_settings=smtp_settings,
+            to_email=test_request.test_recipient,
+            subject="BamBuddy SMTP Test",
+            body_text="This is a test email from BamBuddy. If you received this, your SMTP settings are working correctly!",
+            body_html="<p>This is a test email from <strong>BamBuddy</strong>.</p><p>If you received this, your SMTP settings are working correctly!</p>",
+        )
+
+        logger.info(f"Test email sent successfully to {test_request.test_recipient}")
+        return TestSMTPResponse(success=True, message="Test email sent successfully")
+    except Exception as e:
+        logger.error(f"Failed to send test email: {e}")
+        return TestSMTPResponse(success=False, message=f"Failed to send test email: {str(e)}")
+
+
+@router.get("/smtp", response_model=SMTPSettings | None)
+async def get_smtp_config(
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Get SMTP settings (admin only). Password is not returned."""
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can view SMTP settings",
+        )
+
+    smtp_settings = await get_smtp_settings(db)
+    if smtp_settings:
+        # Don't return password in response
+        smtp_settings.smtp_password = None
+    return smtp_settings
+
+
+@router.post("/smtp", response_model=dict)
+async def save_smtp_config(
+    smtp_settings: SMTPSettings,
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Save SMTP settings (admin only)."""
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can update SMTP settings",
+        )
+
+    try:
+        await save_smtp_settings(db, smtp_settings)
+        await db.commit()
+        logger.info(f"SMTP settings updated by admin user: {user.username}")
+        return {"message": "SMTP settings saved successfully"}
+    except Exception as e:
+        await db.rollback()
+        logger.error(f"Failed to save SMTP settings: {e}")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"Failed to save SMTP settings: {str(e)}",
+        )
+
+
+@router.post("/advanced-auth/enable", response_model=dict)
+async def enable_advanced_auth(
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Enable advanced authentication (admin only).
+    
+    Requires SMTP settings to be configured and tested first.
+    """
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can enable advanced authentication",
+        )
+
+    # Verify SMTP settings are configured
+    smtp_settings = await get_smtp_settings(db)
+    if not smtp_settings:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="SMTP settings must be configured before enabling advanced authentication",
+        )
+
+    try:
+        await set_advanced_auth_enabled(db, True)
+        await db.commit()
+        logger.info(f"Advanced authentication enabled by admin user: {user.username}")
+        return {"message": "Advanced authentication enabled successfully", "advanced_auth_enabled": True}
+    except Exception as e:
+        await db.rollback()
+        logger.error(f"Failed to enable advanced authentication: {e}")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"Failed to enable advanced authentication: {str(e)}",
+        )
+
+
+@router.post("/advanced-auth/disable", response_model=dict)
+async def disable_advanced_auth(
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Disable advanced authentication (admin only)."""
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can disable advanced authentication",
+        )
+
+    try:
+        await set_advanced_auth_enabled(db, False)
+        await db.commit()
+        logger.info(f"Advanced authentication disabled by admin user: {user.username}")
+        return {"message": "Advanced authentication disabled successfully", "advanced_auth_enabled": False}
+    except Exception as e:
+        await db.rollback()
+        logger.error(f"Failed to disable advanced authentication: {e}")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"Failed to disable advanced authentication: {str(e)}",
+        )
+
+
+@router.get("/advanced-auth/status")
+async def get_advanced_auth_status(db: AsyncSession = Depends(get_db)):
+    """Get advanced authentication status."""
+    advanced_auth_enabled = await is_advanced_auth_enabled(db)
+    smtp_configured = await get_smtp_settings(db) is not None
+    return {
+        "advanced_auth_enabled": advanced_auth_enabled,
+        "smtp_configured": smtp_configured,
+    }
+
+
+@router.post("/forgot-password", response_model=ForgotPasswordResponse)
+async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
+    """Request password reset via email (advanced auth only)."""
+    import logging
+    import os
+
+    logger = logging.getLogger(__name__)
+
+    # Check if advanced auth is enabled
+    advanced_auth = await is_advanced_auth_enabled(db)
+    if not advanced_auth:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Advanced authentication is not enabled",
+        )
+
+    # Get SMTP settings
+    smtp_settings = await get_smtp_settings(db)
+    if not smtp_settings:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Email service is not configured",
+        )
+
+    # Find user by email
+    user = await get_user_by_email(db, request.email)
+    
+    # Always return success message to prevent email enumeration
+    # but only send email if user exists
+    if user and user.is_active:
+        try:
+            # Generate new password
+            new_password = generate_secure_password()
+            user.password_hash = get_password_hash(new_password)
+            await db.commit()
+
+            # Get login URL from environment or use default
+            login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
+
+            # Send password reset email
+            subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
+            send_email(smtp_settings, user.email, subject, text_body, html_body)
+
+            logger.info(f"Password reset email sent to {user.email}")
+        except Exception as e:
+            logger.error(f"Failed to send password reset email: {e}")
+            # Don't reveal error to user for security
+
+    return ForgotPasswordResponse(
+        message="If the email address is associated with an account, a password reset email has been sent."
+    )
+
+
+@router.post("/reset-password", response_model=ResetPasswordResponse)
+async def reset_user_password(
+    request: ResetPasswordRequest,
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Reset a user's password and send them an email (admin only, advanced auth only)."""
+    import logging
+    import os
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    admin_user = result.scalar_one()
+
+    if not admin_user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can reset user passwords",
+        )
+
+    # Check if advanced auth is enabled
+    advanced_auth = await is_advanced_auth_enabled(db)
+    if not advanced_auth:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Advanced authentication is not enabled",
+        )
+
+    # Get SMTP settings
+    smtp_settings = await get_smtp_settings(db)
+    if not smtp_settings:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Email service is not configured",
+        )
+
+    # Find user to reset
+    result = await db.execute(select(User).where(User.id == request.user_id))
+    user = result.scalar_one_or_none()
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="User not found",
+        )
+
+    if not user.email:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="User does not have an email address configured",
+        )
+
+    try:
+        # Generate new password
+        new_password = generate_secure_password()
+        user.password_hash = get_password_hash(new_password)
+        await db.commit()
+
+        # Get login URL from environment or use default
+        login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
+
+        # Send password reset email
+        subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
+        send_email(smtp_settings, user.email, subject, text_body, html_body)
+
+        logger.info(f"Password reset by admin {admin_user.username} for user {user.username}")
+        return ResetPasswordResponse(message=f"Password reset email sent to {user.email}")
+    except Exception as e:
+        await db.rollback()
+        logger.error(f"Failed to reset password for user {user.username}: {e}")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"Failed to reset password: {str(e)}",
+        )

+ 90 - 6
backend/app/api/routes/users.py

@@ -15,8 +15,15 @@ from backend.app.models.archive import PrintArchive
 from backend.app.models.group import Group
 from backend.app.models.group import Group
 from backend.app.models.library import LibraryFile
 from backend.app.models.library import LibraryFile
 from backend.app.models.print_queue import PrintQueueItem
 from backend.app.models.print_queue import PrintQueueItem
+from backend.app.models.settings import Settings
 from backend.app.models.user import User
 from backend.app.models.user import User
 from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
 from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
+from backend.app.services.email_service import (
+    create_welcome_email,
+    generate_secure_password,
+    get_smtp_settings,
+    send_email,
+)
 
 
 router = APIRouter(prefix="/users", tags=["users"])
 router = APIRouter(prefix="/users", tags=["users"])
 
 
@@ -26,6 +33,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
     return UserResponse(
         id=user.id,
         id=user.id,
         username=user.username,
         username=user.username,
+        email=user.email,
         role=user.role,
         role=user.role,
         is_active=user.is_active,
         is_active=user.is_active,
         is_admin=user.is_admin,
         is_admin=user.is_admin,
@@ -54,9 +62,27 @@ async def create_user(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
     db: AsyncSession = Depends(get_db),
     db: AsyncSession = Depends(get_db),
 ):
 ):
-    """Create a new user."""
-    # Check if username already exists
-    existing_user = await db.execute(select(User).where(User.username == user_data.username))
+    """Create a new user.
+    
+    When advanced authentication is enabled:
+    - Email is required
+    - Password is auto-generated and emailed to user
+    - Admin cannot set or see the password
+    """
+    import logging
+    import os
+
+    logger = logging.getLogger(__name__)
+
+    # Check if advanced auth is enabled
+    result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
+    advanced_auth_setting = result.scalar_one_or_none()
+    advanced_auth_enabled = advanced_auth_setting and advanced_auth_setting.value.lower() == "true"
+
+    # Check if username already exists (case-insensitive)
+    existing_user = await db.execute(
+        select(User).where(func.lower(User.username) == func.lower(user_data.username))
+    )
     if existing_user.scalar_one_or_none():
     if existing_user.scalar_one_or_none():
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
@@ -70,9 +96,38 @@ async def create_user(
             detail="Role must be 'admin' or 'user'",
             detail="Role must be 'admin' or 'user'",
         )
         )
 
 
+    # Advanced auth validation
+    if advanced_auth_enabled:
+        if not user_data.email:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email is required when advanced authentication is enabled",
+            )
+        # Check if email already exists (case-insensitive)
+        existing_email = await db.execute(
+            select(User).where(func.lower(User.email) == func.lower(user_data.email))
+        )
+        if existing_email.scalar_one_or_none():
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email already exists",
+            )
+
+    # Generate password if advanced auth enabled, otherwise require password
+    if advanced_auth_enabled:
+        password = generate_secure_password()
+    else:
+        if not user_data.password:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Password is required when advanced authentication is disabled",
+            )
+        password = user_data.password
+
     new_user = User(
     new_user = User(
         username=user_data.username,
         username=user_data.username,
-        password_hash=get_password_hash(user_data.password),
+        email=user_data.email,
+        password_hash=get_password_hash(password),
         role=user_data.role,
         role=user_data.role,
         is_active=True,
         is_active=True,
     )
     )
@@ -92,6 +147,21 @@ async def create_user(
     await db.commit()
     await db.commit()
     await db.refresh(new_user)
     await db.refresh(new_user)
 
 
+    # Send welcome email if advanced auth enabled
+    if advanced_auth_enabled and new_user.email:
+        try:
+            smtp_settings = await get_smtp_settings(db)
+            if smtp_settings:
+                login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
+                subject, text_body, html_body = create_welcome_email(new_user.username, password, login_url)
+                send_email(smtp_settings, new_user.email, subject, text_body, html_body)
+                logger.info(f"Welcome email sent to {new_user.email}")
+            else:
+                logger.warning(f"SMTP not configured, could not send welcome email to {new_user.email}")
+        except Exception as e:
+            logger.error(f"Failed to send welcome email: {e}")
+            # Don't fail user creation if email fails
+
     return _user_to_response(new_user)
     return _user_to_response(new_user)
 
 
 
 
@@ -161,8 +231,10 @@ async def update_user(
             )
             )
 
 
     if user_data.username is not None:
     if user_data.username is not None:
-        # Check if new username already exists
-        existing_user = await db.execute(select(User).where(User.username == user_data.username, User.id != user_id))
+        # Check if new username already exists (case-insensitive)
+        existing_user = await db.execute(
+            select(User).where(func.lower(User.username) == func.lower(user_data.username), User.id != user_id)
+        )
         if existing_user.scalar_one_or_none():
         if existing_user.scalar_one_or_none():
             raise HTTPException(
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 status_code=status.HTTP_400_BAD_REQUEST,
@@ -170,6 +242,18 @@ async def update_user(
             )
             )
         user.username = user_data.username
         user.username = user_data.username
 
 
+    if user_data.email is not None:
+        # Check if new email already exists (case-insensitive)
+        existing_email = await db.execute(
+            select(User).where(func.lower(User.email) == func.lower(user_data.email), User.id != user_id)
+        )
+        if existing_email.scalar_one_or_none():
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email already exists",
+            )
+        user.email = user_data.email
+
     if user_data.password is not None:
     if user_data.password is not None:
         user.password_hash = get_password_hash(user_data.password)
         user.password_hash = get_password_hash(user_data.password)
 
 

+ 32 - 4
backend/app/core/auth.py

@@ -12,7 +12,7 @@ from fastapi import Depends, Header, HTTPException, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from jwt.exceptions import PyJWTError as JWTError
 from jwt.exceptions import PyJWTError as JWTError
 from passlib.context import CryptContext
 from passlib.context import CryptContext
-from sqlalchemy import select
+from sqlalchemy import func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import selectinload
 
 
@@ -128,13 +128,26 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None) -> s
 
 
 
 
 async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
 async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
-    """Get a user by username with groups loaded for permission checks."""
-    result = await db.execute(select(User).where(User.username == username).options(selectinload(User.groups)))
+    """Get a user by username (case-insensitive) with groups loaded for permission checks."""
+    result = await db.execute(
+        select(User).where(func.lower(User.username) == func.lower(username)).options(selectinload(User.groups))
+    )
+    return result.scalar_one_or_none()
+
+
+async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
+    """Get a user by email (case-insensitive) with groups loaded for permission checks."""
+    result = await db.execute(
+        select(User).where(func.lower(User.email) == func.lower(email)).options(selectinload(User.groups))
+    )
     return result.scalar_one_or_none()
     return result.scalar_one_or_none()
 
 
 
 
 async def authenticate_user(db: AsyncSession, username: str, password: str) -> User | None:
 async def authenticate_user(db: AsyncSession, username: str, password: str) -> User | None:
-    """Authenticate a user by username and password."""
+    """Authenticate a user by username and password.
+    
+    Username lookup is case-insensitive. Password is case-sensitive.
+    """
     user = await get_user_by_username(db, username)
     user = await get_user_by_username(db, username)
     if not user:
     if not user:
         return None
         return None
@@ -145,6 +158,21 @@ async def authenticate_user(db: AsyncSession, username: str, password: str) -> U
     return user
     return user
 
 
 
 
+async def authenticate_user_by_email(db: AsyncSession, email: str, password: str) -> User | None:
+    """Authenticate a user by email and password.
+    
+    Email lookup is case-insensitive. Password is case-sensitive.
+    """
+    user = await get_user_by_email(db, email)
+    if not user:
+        return None
+    if not verify_password(password, user.password_hash):
+        return None
+    if not user.is_active:
+        return None
+    return user
+
+
 async def is_auth_enabled(db: AsyncSession) -> bool:
 async def is_auth_enabled(db: AsyncSession) -> bool:
     """Check if authentication is enabled."""
     """Check if authentication is enabled."""
     try:
     try:

+ 1 - 0
backend/app/models/user.py

@@ -24,6 +24,7 @@ class User(Base):
 
 
     id: Mapped[int] = mapped_column(primary_key=True)
     id: Mapped[int] = mapped_column(primary_key=True)
     username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
     username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
+    email: Mapped[str | None] = mapped_column(String(255), unique=True, index=True, nullable=True)
     password_hash: Mapped[str] = mapped_column(String(255))
     password_hash: Mapped[str] = mapped_column(String(255))
     role: Mapped[str] = mapped_column(
     role: Mapped[str] = mapped_column(
         String(20), default="user"
         String(20), default="user"

+ 45 - 1
backend/app/schemas/auth.py

@@ -24,7 +24,8 @@ class LoginResponse(BaseModel):
 
 
 class UserCreate(BaseModel):
 class UserCreate(BaseModel):
     username: str
     username: str
-    password: str
+    password: str | None = None  # Optional when advanced auth is enabled
+    email: str | None = None
     role: str = "user"
     role: str = "user"
     group_ids: list[int] | None = None
     group_ids: list[int] | None = None
 
 
@@ -32,6 +33,7 @@ class UserCreate(BaseModel):
 class UserUpdate(BaseModel):
 class UserUpdate(BaseModel):
     username: str | None = None
     username: str | None = None
     password: str | None = None
     password: str | None = None
+    email: str | None = None
     role: str | None = None
     role: str | None = None
     is_active: bool | None = None
     is_active: bool | None = None
     group_ids: list[int] | None = None
     group_ids: list[int] | None = None
@@ -40,6 +42,7 @@ class UserUpdate(BaseModel):
 class UserResponse(BaseModel):
 class UserResponse(BaseModel):
     id: int
     id: int
     username: str
     username: str
+    email: str | None = None
     role: str  # Deprecated, kept for backward compatibility
     role: str  # Deprecated, kept for backward compatibility
     is_active: bool
     is_active: bool
     is_admin: bool  # Computed from role and group membership
     is_admin: bool  # Computed from role and group membership
@@ -65,3 +68,44 @@ class SetupRequest(BaseModel):
 class SetupResponse(BaseModel):
 class SetupResponse(BaseModel):
     auth_enabled: bool
     auth_enabled: bool
     admin_created: bool | None = None
     admin_created: bool | None = None
+
+
+class ForgotPasswordRequest(BaseModel):
+    email: str
+
+
+class ForgotPasswordResponse(BaseModel):
+    message: str
+
+
+class ResetPasswordRequest(BaseModel):
+    user_id: int
+
+
+class ResetPasswordResponse(BaseModel):
+    message: str
+
+
+class SMTPSettings(BaseModel):
+    smtp_host: str
+    smtp_port: int
+    smtp_username: str
+    smtp_password: str | None = None  # Optional for read operations
+    smtp_use_tls: bool = True
+    smtp_from_email: str
+    smtp_from_name: str = "BamBuddy"
+
+
+class TestSMTPRequest(BaseModel):
+    smtp_host: str
+    smtp_port: int
+    smtp_username: str
+    smtp_password: str
+    smtp_use_tls: bool = True
+    smtp_from_email: str
+    test_recipient: str
+
+
+class TestSMTPResponse(BaseModel):
+    success: bool
+    message: str

+ 318 - 0
backend/app/services/email_service.py

@@ -0,0 +1,318 @@
+"""Email service for sending authentication-related emails."""
+
+from __future__ import annotations
+
+import logging
+import secrets
+import smtplib
+import string
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.settings import Settings
+from backend.app.schemas.auth import SMTPSettings
+
+logger = logging.getLogger(__name__)
+
+
+def generate_secure_password(length: int = 16) -> str:
+    """Generate a secure random password.
+    
+    Args:
+        length: Length of the password (default: 16)
+        
+    Returns:
+        A secure random password containing uppercase, lowercase, digits, and special characters
+    """
+    import random
+    
+    # Define character sets
+    lowercase = string.ascii_lowercase
+    uppercase = string.ascii_uppercase
+    digits = string.digits
+    special = "!@#$%^&*()_+-=[]{}|;:,.<>?"
+    
+    # Ensure at least one character from each set
+    password_chars = [
+        secrets.choice(lowercase),
+        secrets.choice(uppercase),
+        secrets.choice(digits),
+        secrets.choice(special),
+    ]
+    
+    # Fill the rest with random characters from all sets
+    all_chars = lowercase + uppercase + digits + special
+    password_chars.extend(secrets.choice(all_chars) for _ in range(length - 4))
+    
+    # Shuffle to avoid predictable patterns
+    random.shuffle(password_chars)
+    
+    return "".join(password_chars)
+
+
+async def get_smtp_settings(db: AsyncSession) -> SMTPSettings | None:
+    """Get SMTP settings from database.
+    
+    Args:
+        db: Database session
+        
+    Returns:
+        SMTPSettings object or None if not configured
+    """
+    # Fetch all SMTP-related settings
+    result = await db.execute(
+        select(Settings).where(
+            Settings.key.in_([
+                "smtp_host",
+                "smtp_port",
+                "smtp_username",
+                "smtp_password",
+                "smtp_use_tls",
+                "smtp_from_email",
+                "smtp_from_name",
+            ])
+        )
+    )
+    settings_dict = {s.key: s.value for s in result.scalars().all()}
+    
+    # Check if minimum required settings are present
+    required_keys = ["smtp_host", "smtp_port", "smtp_username", "smtp_from_email"]
+    if not all(key in settings_dict for key in required_keys):
+        return None
+    
+    return SMTPSettings(
+        smtp_host=settings_dict["smtp_host"],
+        smtp_port=int(settings_dict["smtp_port"]),
+        smtp_username=settings_dict["smtp_username"],
+        smtp_password=settings_dict.get("smtp_password"),
+        smtp_use_tls=settings_dict.get("smtp_use_tls", "true").lower() == "true",
+        smtp_from_email=settings_dict["smtp_from_email"],
+        smtp_from_name=settings_dict.get("smtp_from_name", "BamBuddy"),
+    )
+
+
+async def save_smtp_settings(db: AsyncSession, smtp_settings: SMTPSettings) -> None:
+    """Save SMTP settings to database.
+    
+    Args:
+        db: Database session
+        smtp_settings: SMTP settings to save
+    """
+    from sqlalchemy import func
+    from sqlalchemy.dialects.sqlite import insert as sqlite_insert
+    
+    settings_data = {
+        "smtp_host": smtp_settings.smtp_host,
+        "smtp_port": str(smtp_settings.smtp_port),
+        "smtp_username": smtp_settings.smtp_username,
+        "smtp_use_tls": "true" if smtp_settings.smtp_use_tls else "false",
+        "smtp_from_email": smtp_settings.smtp_from_email,
+        "smtp_from_name": smtp_settings.smtp_from_name,
+    }
+    
+    # Only save password if provided
+    if smtp_settings.smtp_password:
+        settings_data["smtp_password"] = smtp_settings.smtp_password
+    
+    for key, value in settings_data.items():
+        stmt = sqlite_insert(Settings).values(key=key, value=value)
+        stmt = stmt.on_conflict_do_update(
+            index_elements=["key"],
+            set_={"value": value, "updated_at": func.now()},
+        )
+        await db.execute(stmt)
+
+
+def send_email(
+    smtp_settings: SMTPSettings,
+    to_email: str,
+    subject: str,
+    body_text: str,
+    body_html: str | None = None,
+) -> None:
+    """Send an email using SMTP.
+    
+    Args:
+        smtp_settings: SMTP configuration
+        to_email: Recipient email address
+        subject: Email subject
+        body_text: Plain text body
+        body_html: Optional HTML body
+        
+    Raises:
+        Exception: If email sending fails
+    """
+    msg = MIMEMultipart("alternative")
+    msg["From"] = f"{smtp_settings.smtp_from_name} <{smtp_settings.smtp_from_email}>"
+    msg["To"] = to_email
+    msg["Subject"] = subject
+    
+    # Attach plain text part
+    msg.attach(MIMEText(body_text, "plain"))
+    
+    # Attach HTML part if provided
+    if body_html:
+        msg.attach(MIMEText(body_html, "html"))
+    
+    # Send email
+    try:
+        if smtp_settings.smtp_use_tls:
+            # Use TLS (port 587 typically)
+            with smtplib.SMTP(smtp_settings.smtp_host, smtp_settings.smtp_port, timeout=10) as server:
+                server.starttls()
+                if smtp_settings.smtp_password:
+                    server.login(smtp_settings.smtp_username, smtp_settings.smtp_password)
+                server.send_message(msg)
+        else:
+            # Use SSL (port 465 typically) or no encryption
+            with smtplib.SMTP_SSL(smtp_settings.smtp_host, smtp_settings.smtp_port, timeout=10) as server:
+                if smtp_settings.smtp_password:
+                    server.login(smtp_settings.smtp_username, smtp_settings.smtp_password)
+                server.send_message(msg)
+        logger.info(f"Email sent successfully to {to_email}")
+    except Exception as e:
+        logger.error(f"Failed to send email to {to_email}: {e}")
+        raise
+
+
+def create_welcome_email(username: str, password: str, login_url: str) -> tuple[str, str, str]:
+    """Create welcome email content for new user.
+    
+    Args:
+        username: Username of the new user
+        password: Auto-generated password
+        login_url: URL to login page
+        
+    Returns:
+        Tuple of (subject, text_body, html_body)
+    """
+    subject = "Welcome to BamBuddy - Your Account Details"
+    
+    text_body = f"""Welcome to BamBuddy!
+
+Your account has been created. Here are your login details:
+
+Username: {username}
+Password: {password}
+
+You can login at: {login_url}
+
+For security reasons, please change your password after your first login.
+
+Best regards,
+BamBuddy Team
+"""
+    
+    html_body = f"""<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
+    <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: white; margin: 0; font-size: 24px;">Welcome to BamBuddy!</h1>
+    </div>
+    <div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
+        <p style="font-size: 16px;">Your account has been created. Here are your login details:</p>
+        
+        <div style="background: white; padding: 20px; border-radius: 4px; margin: 20px 0; border-left: 4px solid #667eea;">
+            <p style="margin: 0 0 10px 0;"><strong>Username:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{username}</code></p>
+            <p style="margin: 0;"><strong>Password:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{password}</code></p>
+        </div>
+        
+        <div style="text-align: center; margin: 30px 0;">
+            <a href="{login_url}" style="display: inline-block; background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
+        </div>
+        
+        <p style="font-size: 14px; color: #666; border-top: 1px solid #ddd; padding-top: 20px; margin-top: 20px;">
+            <strong>Security Note:</strong> For security reasons, please change your password after your first login.
+        </p>
+        
+        <p style="font-size: 14px; color: #999; margin-top: 30px;">
+            Best regards,<br>
+            BamBuddy Team
+        </p>
+    </div>
+</body>
+</html>
+"""
+    
+    return subject, text_body, html_body
+
+
+def create_password_reset_email(username: str, password: str, login_url: str) -> tuple[str, str, str]:
+    """Create password reset email content.
+    
+    Args:
+        username: Username of the user
+        password: New auto-generated password
+        login_url: URL to login page
+        
+    Returns:
+        Tuple of (subject, text_body, html_body)
+    """
+    subject = "BamBuddy - Your Password Has Been Reset"
+    
+    text_body = f"""Your BamBuddy password has been reset.
+
+Your login details:
+
+Username: {username}
+New Password: {password}
+
+You can login at: {login_url}
+
+For security reasons, please change your password after logging in.
+
+If you did not request this password reset, please contact your administrator immediately.
+
+Best regards,
+BamBuddy Team
+"""
+    
+    html_body = f"""<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
+    <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: white; margin: 0; font-size: 24px;">Password Reset</h1>
+    </div>
+    <div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
+        <p style="font-size: 16px;">Your BamBuddy password has been reset.</p>
+        
+        <div style="background: white; padding: 20px; border-radius: 4px; margin: 20px 0; border-left: 4px solid #667eea;">
+            <p style="margin: 0 0 10px 0;"><strong>Username:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{username}</code></p>
+            <p style="margin: 0;"><strong>New Password:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{password}</code></p>
+        </div>
+        
+        <div style="text-align: center; margin: 30px 0;">
+            <a href="{login_url}" style="display: inline-block; background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
+        </div>
+        
+        <div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;">
+            <p style="margin: 0; font-size: 14px; color: #856404;">
+                <strong>⚠️ Security Alert:</strong> If you did not request this password reset, please contact your administrator immediately.
+            </p>
+        </div>
+        
+        <p style="font-size: 14px; color: #666; border-top: 1px solid #ddd; padding-top: 20px; margin-top: 20px;">
+            <strong>Security Note:</strong> For security reasons, please change your password after logging in.
+        </p>
+        
+        <p style="font-size: 14px; color: #999; margin-top: 30px;">
+            Best regards,<br>
+            BamBuddy Team
+        </p>
+    </div>
+</body>
+</html>
+"""
+    
+    return subject, text_body, html_body

+ 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/'),

+ 365 - 0
frontend/src/components/EmailSettings.tsx

@@ -0,0 +1,365 @@
+import { useState } from 'react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
+import { Mail, Send, Lock, Unlock, AlertTriangle, CheckCircle, Loader2 } from 'lucide-react';
+import { api } from '../api/client';
+import type { SMTPSettings, TestSMTPRequest } from '../api/client';
+import { Card, CardContent, CardHeader } from './Card';
+import { Button } from './Button';
+import { useToast } from '../contexts/ToastContext';
+import { useEffect } from 'react';
+
+export function EmailSettings() {
+  const { t } = useTranslation();
+  const { showToast } = useToast();
+  const queryClient = useQueryClient();
+  
+  const [smtpSettings, setSMTPSettings] = useState<SMTPSettings>({
+    smtp_host: '',
+    smtp_port: 587,
+    smtp_username: '',
+    smtp_password: '',
+    smtp_use_tls: true,
+    smtp_from_email: '',
+    smtp_from_name: 'BamBuddy',
+  });
+  const [testEmail, setTestEmail] = useState('');
+
+  // Fetch SMTP settings
+  const { data: existingSettings, isLoading } = useQuery({
+    queryKey: ['smtpSettings'],
+    queryFn: () => api.getSMTPSettings(),
+  });
+
+  // Fetch advanced auth status
+  const { data: advancedAuthStatus } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
+
+  // Load existing settings when fetched
+  useEffect(() => {
+    if (existingSettings) {
+      setSMTPSettings({
+        ...existingSettings,
+        smtp_password: '', // Never show password
+      });
+    }
+  }, [existingSettings]);
+
+  // Save SMTP settings
+  const saveMutation = useMutation({
+    mutationFn: (settings: SMTPSettings) => api.saveSMTPSettings(settings),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['smtpSettings'] });
+      queryClient.invalidateQueries({ queryKey: ['advancedAuthStatus'] });
+      showToast('SMTP settings saved successfully', 'success');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
+  // Test SMTP connection
+  const testMutation = useMutation({
+    mutationFn: (request: TestSMTPRequest) => api.testSMTP(request),
+    onSuccess: (data) => {
+      showToast(data.message, data.success ? 'success' : 'error');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
+  // Toggle advanced auth
+  const toggleAdvancedAuthMutation = useMutation({
+    mutationFn: (enabled: boolean) =>
+      enabled ? api.enableAdvancedAuth() : api.disableAdvancedAuth(),
+    onSuccess: (data) => {
+      queryClient.invalidateQueries({ queryKey: ['advancedAuthStatus'] });
+      showToast(data.message, 'success');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
+  const handleSave = () => {
+    // Validate required fields
+    if (!smtpSettings.smtp_host || !smtpSettings.smtp_username || !smtpSettings.smtp_from_email) {
+      showToast('Please fill in all required fields', 'error');
+      return;
+    }
+    saveMutation.mutate(smtpSettings);
+  };
+
+  const handleTest = () => {
+    if (!testEmail) {
+      showToast('Please enter a test email address', 'error');
+      return;
+    }
+    if (!smtpSettings.smtp_host || !smtpSettings.smtp_username || !smtpSettings.smtp_password || !smtpSettings.smtp_from_email) {
+      showToast('Please fill in all SMTP settings before testing', 'error');
+      return;
+    }
+    testMutation.mutate({
+      smtp_host: smtpSettings.smtp_host,
+      smtp_port: smtpSettings.smtp_port,
+      smtp_username: smtpSettings.smtp_username,
+      smtp_password: smtpSettings.smtp_password,
+      smtp_use_tls: smtpSettings.smtp_use_tls,
+      smtp_from_email: smtpSettings.smtp_from_email,
+      test_recipient: testEmail,
+    });
+  };
+
+  const handleToggleAdvancedAuth = () => {
+    if (!advancedAuthStatus?.advanced_auth_enabled && !advancedAuthStatus?.smtp_configured) {
+      showToast('Please configure and test SMTP settings first', 'error');
+      return;
+    }
+    toggleAdvancedAuthMutation.mutate(!advancedAuthStatus?.advanced_auth_enabled);
+  };
+
+  if (isLoading) {
+    return (
+      <div className="flex items-center justify-center p-12">
+        <Loader2 className="w-8 h-8 animate-spin text-bambu-green" />
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-6">
+      {/* Advanced Authentication Toggle */}
+      <Card>
+        <CardHeader>
+          <div className="flex items-center justify-between">
+            <div className="flex items-center gap-2">
+              <Mail className="w-5 h-5 text-bambu-green" />
+              <h2 className="text-lg font-semibold text-white">
+                {t('settings.email.advancedAuth') || 'Advanced Authentication'}
+              </h2>
+            </div>
+            <Button
+              onClick={handleToggleAdvancedAuth}
+              disabled={toggleAdvancedAuthMutation.isPending}
+              variant={advancedAuthStatus?.advanced_auth_enabled ? 'danger' : 'primary'}
+            >
+              {advancedAuthStatus?.advanced_auth_enabled ? (
+                <>
+                  <Unlock className="w-4 h-4" />
+                  {t('settings.email.disable') || 'Disable'}
+                </>
+              ) : (
+                <>
+                  <Lock className="w-4 h-4" />
+                  {t('settings.email.enable') || 'Enable'}
+                </>
+              )}
+            </Button>
+          </div>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            {advancedAuthStatus?.advanced_auth_enabled ? (
+              <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4">
+                <div className="flex items-start gap-3">
+                  <CheckCircle className="w-5 h-5 text-green-400 mt-0.5 flex-shrink-0" />
+                  <div className="space-y-2">
+                    <p className="text-white font-medium">
+                      {t('settings.email.advancedAuthEnabled') || 'Advanced Authentication is enabled'}
+                    </p>
+                    <ul className="text-sm text-green-300 space-y-1 list-disc list-inside">
+                      <li>{t('settings.email.feature1') || 'Passwords are auto-generated and emailed to new users'}</li>
+                      <li>{t('settings.email.feature2') || 'Users can login with username or email'}</li>
+                      <li>{t('settings.email.feature3') || 'Forgot password feature is available'}</li>
+                      <li>{t('settings.email.feature4') || 'Admins can reset user passwords via email'}</li>
+                    </ul>
+                  </div>
+                </div>
+              </div>
+            ) : (
+              <div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4">
+                <div className="flex items-start gap-3">
+                  <AlertTriangle className="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0" />
+                  <div className="space-y-2">
+                    <p className="text-white font-medium">
+                      {t('settings.email.advancedAuthDisabled') || 'Advanced Authentication is disabled'}
+                    </p>
+                    <p className="text-sm text-yellow-300">
+                      {t('settings.email.advancedAuthDisabledDesc') || 'Configure and test SMTP settings below, then enable advanced authentication to activate email-based features.'}
+                    </p>
+                  </div>
+                </div>
+              </div>
+            )}
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* SMTP Configuration */}
+      <Card>
+        <CardHeader>
+          <h2 className="text-lg font-semibold text-white">
+            {t('settings.email.smtpSettings') || 'SMTP Configuration'}
+          </h2>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+              <div>
+                <label className="block text-sm font-medium text-white mb-2">
+                  {t('settings.email.smtpHost') || 'SMTP Server'} *
+                </label>
+                <input
+                  type="text"
+                  value={smtpSettings.smtp_host}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_host: e.target.value })}
+                  placeholder="smtp.gmail.com"
+                  className="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"
+                />
+              </div>
+              <div>
+                <label className="block text-sm font-medium text-white mb-2">
+                  {t('settings.email.smtpPort') || 'Port'} *
+                </label>
+                <input
+                  type="number"
+                  value={smtpSettings.smtp_port}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_port: parseInt(e.target.value) || 587 })}
+                  className="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"
+                />
+              </div>
+            </div>
+
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('settings.email.username') || 'Username'} *
+              </label>
+              <input
+                type="text"
+                value={smtpSettings.smtp_username}
+                onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_username: e.target.value })}
+                placeholder="your.email@gmail.com"
+                className="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"
+              />
+            </div>
+
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('settings.email.password') || 'Password'} *
+              </label>
+              <input
+                type="password"
+                value={smtpSettings.smtp_password}
+                onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_password: e.target.value })}
+                placeholder={existingSettings ? '••••••••' : 'Enter password'}
+                className="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"
+              />
+            </div>
+
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+              <div>
+                <label className="block text-sm font-medium text-white mb-2">
+                  {t('settings.email.fromEmail') || 'From Email'} *
+                </label>
+                <input
+                  type="email"
+                  value={smtpSettings.smtp_from_email}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_from_email: e.target.value })}
+                  placeholder="noreply@yourdomain.com"
+                  className="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"
+                />
+              </div>
+              <div>
+                <label className="block text-sm font-medium text-white mb-2">
+                  {t('settings.email.fromName') || 'From Name'}
+                </label>
+                <input
+                  type="text"
+                  value={smtpSettings.smtp_from_name}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_from_name: e.target.value })}
+                  placeholder="BamBuddy"
+                  className="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"
+                />
+              </div>
+            </div>
+
+            <div className="flex items-center gap-2">
+              <input
+                type="checkbox"
+                id="use_tls"
+                checked={smtpSettings.smtp_use_tls}
+                onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_use_tls: e.target.checked })}
+                className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark"
+              />
+              <label htmlFor="use_tls" className="text-sm text-white">
+                {t('settings.email.useTLS') || 'Use TLS (recommended)'}
+              </label>
+            </div>
+
+            <div className="flex gap-2">
+              <Button
+                onClick={handleSave}
+                disabled={saveMutation.isPending}
+                className="flex-1"
+              >
+                {saveMutation.isPending ? (
+                  <>
+                    <Loader2 className="w-4 h-4 animate-spin" />
+                    {t('settings.email.saving') || 'Saving...'}
+                  </>
+                ) : (
+                  t('settings.email.save') || 'Save Settings'
+                )}
+              </Button>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Test SMTP */}
+      <Card>
+        <CardHeader>
+          <h2 className="text-lg font-semibold text-white">
+            {t('settings.email.testConnection') || 'Test SMTP Connection'}
+          </h2>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('settings.email.testRecipient') || 'Test Recipient Email'}
+              </label>
+              <input
+                type="email"
+                value={testEmail}
+                onChange={(e) => setTestEmail(e.target.value)}
+                placeholder="test@example.com"
+                className="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"
+              />
+            </div>
+            <Button
+              onClick={handleTest}
+              disabled={testMutation.isPending}
+              variant="secondary"
+            >
+              {testMutation.isPending ? (
+                <>
+                  <Loader2 className="w-4 h-4 animate-spin" />
+                  {t('settings.email.sending') || 'Sending...'}
+                </>
+              ) : (
+                <>
+                  <Send className="w-4 h-4" />
+                  {t('settings.email.sendTest') || 'Send Test Email'}
+                </>
+              )}
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 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 { 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>
       )}
       )}

+ 19 - 2
frontend/src/pages/SettingsPage.tsx

@@ -1,5 +1,5 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save } from 'lucide-react';
+import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save, Mail } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { api } from '../api/client';
 import { api } from '../api/client';
@@ -19,6 +19,7 @@ import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { ExternalLinksSettings } from '../components/ExternalLinksSettings';
 import { ExternalLinksSettings } from '../components/ExternalLinksSettings';
 import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings';
 import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
+import { EmailSettings } from '../components/EmailSettings';
 import { APIBrowser } from '../components/APIBrowser';
 import { APIBrowser } from '../components/APIBrowser';
 import { virtualPrinterApi } from '../api/client';
 import { virtualPrinterApi } from '../api/client';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
@@ -28,7 +29,7 @@ import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, t
 import { useState, useEffect, useRef, useCallback } from 'react';
 import { useState, useEffect, useRef, useCallback } from 'react';
 import { Palette } from 'lucide-react';
 import { Palette } from 'lucide-react';
 
 
-const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const;
+const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'email', 'backup'] as const;
 type TabType = typeof validTabs[number];
 type TabType = typeof validTabs[number];
 
 
 export function SettingsPage() {
 export function SettingsPage() {
@@ -1025,6 +1026,17 @@ export function SettingsPage() {
             <span className="w-2 h-2 rounded-full bg-green-400" />
             <span className="w-2 h-2 rounded-full bg-green-400" />
           )}
           )}
         </button>
         </button>
+        <button
+          onClick={() => handleTabChange('email')}
+          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
+            activeTab === 'email'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray hover:text-gray-900 dark:hover:text-white border-transparent'
+          }`}
+        >
+          <Mail className="w-4 h-4" />
+          {t('settings.tabs.email') || 'Email'}
+        </button>
         <button
         <button
           onClick={() => handleTabChange('backup')}
           onClick={() => handleTabChange('backup')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
@@ -4179,6 +4191,11 @@ export function SettingsPage() {
         />
         />
       )}
       )}
 
 
+      {/* Email Tab */}
+      {activeTab === 'email' && (
+        <EmailSettings />
+      )}
+
       {/* Backup Tab */}
       {/* Backup Tab */}
       {activeTab === 'backup' && (
       {activeTab === 'backup' && (
         <GitHubBackupSettings />
         <GitHubBackupSettings />

+ 144 - 52
frontend/src/pages/UsersPage.tsx

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft } from 'lucide-react';
+import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft, RotateCcw } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { UserCreate, UserUpdate, UserResponse } from '../api/client';
 import type { UserCreate, UserUpdate, UserResponse } from '../api/client';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
@@ -14,6 +14,7 @@ import { ConfirmModal } from '../components/ConfirmModal';
 interface FormData extends UserCreate {
 interface FormData extends UserCreate {
   group_ids: number[];
   group_ids: number[];
   confirmPassword: string;
   confirmPassword: string;
+  email?: string;
 }
 }
 
 
 export function UsersPage() {
 export function UsersPage() {
@@ -29,23 +30,30 @@ export function UsersPage() {
   const [formData, setFormData] = useState<FormData>({
   const [formData, setFormData] = useState<FormData>({
     username: '',
     username: '',
     password: '',
     password: '',
+    email: '',
     confirmPassword: '',
     confirmPassword: '',
     role: 'user',
     role: 'user',
     group_ids: [],
     group_ids: [],
   });
   });
 
 
+  // Check if advanced auth is enabled
+  const { data: advancedAuthStatus } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
+
   // Close modal on Escape key
   // Close modal on Escape key
   useEffect(() => {
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if (e.key === 'Escape') {
       if (e.key === 'Escape') {
         if (showCreateModal) {
         if (showCreateModal) {
           setShowCreateModal(false);
           setShowCreateModal(false);
-          setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+          setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
         }
         }
         if (showEditModal) {
         if (showEditModal) {
           setShowEditModal(false);
           setShowEditModal(false);
           setEditingUserId(null);
           setEditingUserId(null);
-          setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+          setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
         }
         }
       }
       }
     };
     };
@@ -71,7 +79,7 @@ export function UsersPage() {
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowCreateModal(false);
       setShowCreateModal(false);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.created'));
       showToast(t('users.toast.created'));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
@@ -86,7 +94,7 @@ export function UsersPage() {
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowEditModal(false);
       setShowEditModal(false);
       setEditingUserId(null);
       setEditingUserId(null);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.updated'));
       showToast(t('users.toast.updated'));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
@@ -105,22 +113,51 @@ export function UsersPage() {
     },
     },
   });
   });
 
 
+  const resetPasswordMutation = useMutation({
+    mutationFn: (userId: number) => api.resetUserPassword({ user_id: userId }),
+    onSuccess: (data) => {
+      showToast(data.message, 'success');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
   const handleCreate = () => {
   const handleCreate = () => {
-    if (!formData.username || !formData.password) {
+    // Use the status from the query hook
+    const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false;
+    
+    if (!formData.username) {
       showToast(t('users.toast.fillRequired'), 'error');
       showToast(t('users.toast.fillRequired'), 'error');
       return;
       return;
     }
     }
-    if (formData.password !== formData.confirmPassword) {
-      showToast(t('users.toast.passwordsDoNotMatch'), 'error');
+    
+    // Email is required when advanced auth is enabled
+    if (advancedAuthEnabled && !formData.email) {
+      showToast('Email is required when advanced authentication is enabled', 'error');
       return;
       return;
     }
     }
-    if (formData.password.length < 6) {
-      showToast(t('users.toast.passwordTooShort'), 'error');
-      return;
+    
+    // Password validation only when advanced auth is disabled
+    if (!advancedAuthEnabled) {
+      if (!formData.password) {
+        showToast(t('users.toast.fillRequired'), 'error');
+        return;
+      }
+      if (formData.password !== formData.confirmPassword) {
+        showToast(t('users.toast.passwordsDoNotMatch'), 'error');
+        return;
+      }
+      if (formData.password.length < 6) {
+        showToast(t('users.toast.passwordTooShort'), 'error');
+        return;
+      }
     }
     }
+    
     createMutation.mutate({
     createMutation.mutate({
       username: formData.username,
       username: formData.username,
-      password: formData.password,
+      password: advancedAuthEnabled ? undefined : formData.password,
+      email: formData.email || undefined,
       role: formData.role,
       role: formData.role,
       group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
       group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
     });
     });
@@ -141,6 +178,7 @@ export function UsersPage() {
     const updateData: UserUpdate = {
     const updateData: UserUpdate = {
       username: formData.username || undefined,
       username: formData.username || undefined,
       password: formData.password || undefined,
       password: formData.password || undefined,
+      email: formData.email || undefined,
       role: formData.role,
       role: formData.role,
       group_ids: formData.group_ids,
       group_ids: formData.group_ids,
     };
     };
@@ -148,6 +186,10 @@ export function UsersPage() {
     if (!updateData.password) {
     if (!updateData.password) {
       delete updateData.password;
       delete updateData.password;
     }
     }
+    // Remove email if empty
+    if (!updateData.email) {
+      delete updateData.email;
+    }
     updateMutation.mutate({ id, data: updateData });
     updateMutation.mutate({ id, data: updateData });
   };
   };
 
 
@@ -160,6 +202,7 @@ export function UsersPage() {
     setFormData({
     setFormData({
       username: user.username,
       username: user.username,
       password: '',
       password: '',
+      email: user.email || '',
       confirmPassword: '',
       confirmPassword: '',
       role: user.role,
       role: user.role,
       group_ids: user.groups?.map(g => g.id) || [],
       group_ids: user.groups?.map(g => g.id) || [],
@@ -170,7 +213,7 @@ export function UsersPage() {
   const closeEditModal = () => {
   const closeEditModal = () => {
     setShowEditModal(false);
     setShowEditModal(false);
     setEditingUserId(null);
     setEditingUserId(null);
-    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
   };
   };
 
 
   const toggleGroup = (groupId: number) => {
   const toggleGroup = (groupId: number) => {
@@ -221,7 +264,7 @@ export function UsersPage() {
         <Button
         <Button
           onClick={() => {
           onClick={() => {
             setShowCreateModal(true);
             setShowCreateModal(true);
-            setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Plus className="w-4 h-4" />
           <Plus className="w-4 h-4" />
@@ -316,6 +359,17 @@ export function UsersPage() {
                             {t('users.delete')}
                             {t('users.delete')}
                           </Button>
                           </Button>
                         )}
                         )}
+                        {advancedAuthStatus?.advanced_auth_enabled && user.email && user.id !== currentUser?.id && (
+                          <Button
+                            size="sm"
+                            variant="ghost"
+                            onClick={() => resetPasswordMutation.mutate(user.id)}
+                            disabled={resetPasswordMutation.isPending}
+                          >
+                            <RotateCcw className="w-4 h-4" />
+                            {t('users.resetPassword') || 'Reset Password'}
+                          </Button>
+                        )}
                       </div>
                       </div>
                     </td>
                     </td>
                   </tr>
                   </tr>
@@ -332,7 +386,7 @@ export function UsersPage() {
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           onClick={() => {
           onClick={() => {
             setShowCreateModal(false);
             setShowCreateModal(false);
-            setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Card
           <Card
@@ -350,7 +404,7 @@ export function UsersPage() {
                   size="sm"
                   size="sm"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateModal(false);
                     setShowCreateModal(false);
-                    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                   }}
                 >
                 >
                   <X className="w-5 h-5" />
                   <X className="w-5 h-5" />
@@ -372,41 +426,67 @@ export function UsersPage() {
                     autoComplete="username"
                     autoComplete="username"
                   />
                   />
                 </div>
                 </div>
-                <div>
-                  <label className="block text-sm font-medium text-white mb-2">
-                    {t('users.form.password')}
-                  </label>
-                  <input
-                    type="password"
-                    value={formData.password}
-                    onChange={(e) => setFormData({ ...formData, password: e.target.value })}
-                    className="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('users.form.passwordPlaceholder')}
-                    autoComplete="new-password"
-                    minLength={6}
-                  />
-                </div>
-                <div>
-                  <label className="block text-sm font-medium text-white mb-2">
-                    {t('users.form.confirmPassword')}
-                  </label>
-                  <input
-                    type="password"
-                    value={formData.confirmPassword}
-                    onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
-                    className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${
-                      formData.confirmPassword && formData.password !== formData.confirmPassword
-                        ? 'border-red-500'
-                        : 'border-bambu-dark-tertiary'
-                    }`}
-                    placeholder={t('users.form.confirmPasswordPlaceholder')}
-                    autoComplete="new-password"
-                    minLength={6}
-                  />
-                  {formData.confirmPassword && formData.password !== formData.confirmPassword && (
-                    <p className="text-red-400 text-xs mt-1">{t('users.toast.passwordsDoNotMatch')}</p>
-                  )}
-                </div>
+                {advancedAuthStatus?.advanced_auth_enabled && (
+                  <div>
+                    <label className="block text-sm font-medium text-white mb-2">
+                      {t('users.form.email') || 'Email'}
+                    </label>
+                    <input
+                      type="email"
+                      value={formData.email}
+                      onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                      className="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('users.form.emailPlaceholder') || 'user@example.com'}
+                      required={advancedAuthStatus?.advanced_auth_enabled}
+                    />
+                  </div>
+                )}
+                {!advancedAuthStatus?.advanced_auth_enabled && (
+                  <>
+                    <div>
+                      <label className="block text-sm font-medium text-white mb-2">
+                        {t('users.form.password')}
+                      </label>
+                      <input
+                        type="password"
+                        value={formData.password}
+                        onChange={(e) => setFormData({ ...formData, password: e.target.value })}
+                        className="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('users.form.passwordPlaceholder')}
+                        autoComplete="new-password"
+                        minLength={6}
+                      />
+                    </div>
+                    <div>
+                      <label className="block text-sm font-medium text-white mb-2">
+                        {t('users.form.confirmPassword')}
+                      </label>
+                      <input
+                        type="password"
+                        value={formData.confirmPassword}
+                        onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
+                        className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${
+                          formData.confirmPassword && formData.password !== formData.confirmPassword
+                            ? 'border-red-500'
+                            : 'border-bambu-dark-tertiary'
+                        }`}
+                        placeholder={t('users.form.confirmPasswordPlaceholder')}
+                        autoComplete="new-password"
+                        minLength={6}
+                      />
+                      {formData.confirmPassword && formData.password !== formData.confirmPassword && (
+                        <p className="text-red-400 text-xs mt-1">{t('users.toast.passwordsDoNotMatch')}</p>
+                      )}
+                    </div>
+                  </>
+                )}
+                {advancedAuthStatus?.advanced_auth_enabled && (
+                  <div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3">
+                    <p className="text-sm text-bambu-gray">
+                      {t('users.form.autoGeneratedPassword') || 'A secure password will be automatically generated and emailed to the user.'}
+                    </p>
+                  </div>
+                )}
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <label className="block text-sm font-medium text-white mb-2">
                     {t('users.form.groups')}
                     {t('users.form.groups')}
@@ -440,7 +520,7 @@ export function UsersPage() {
                   variant="secondary"
                   variant="secondary"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateModal(false);
                     setShowCreateModal(false);
-                    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                   }}
                 >
                 >
                   {t('users.modal.cancel')}
                   {t('users.modal.cancel')}
@@ -507,6 +587,18 @@ export function UsersPage() {
                     autoComplete="username"
                     autoComplete="username"
                   />
                   />
                 </div>
                 </div>
+                <div>
+                  <label className="block text-sm font-medium text-white mb-2">
+                    {t('users.form.email') || 'Email'} <span className="text-bambu-gray font-normal">({t('users.form.optional') || 'optional'})</span>
+                  </label>
+                  <input
+                    type="email"
+                    value={formData.email}
+                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                    className="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('users.form.emailPlaceholder') || 'user@example.com'}
+                  />
+                </div>
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <label className="block text-sm font-medium text-white mb-2">
                     {t('users.form.password')} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep')})</span>
                     {t('users.form.password')} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep')})</span>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-962x7uln.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-Ce6bi4R2.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-togsBDt6.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BZQD54OI.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-togsBDt6.css">
+    <script type="module" crossorigin src="/assets/index-Ce6bi4R2.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-962x7uln.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Некоторые файлы не были показаны из-за большого количества измененных файлов