Browse Source

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

Add advanced authentication with email-based login and SMTP notifications
Thomas Rambach 3 months ago
parent
commit
a45edc3fae

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

@@ -8,16 +8,39 @@ from sqlalchemy.orm import selectinload
 from backend.app.core.auth import (
     ACCESS_TOKEN_EXPIRE_MINUTES,
     authenticate_user,
+    authenticate_user_by_email,
     create_access_token,
     get_current_active_user,
     get_password_hash,
+    get_user_by_email,
     get_user_by_username,
 )
 from backend.app.core.database import get_db
 from backend.app.models.group import Group
 from backend.app.models.settings import Settings
 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:
@@ -25,6 +48,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
         id=user.id,
         username=user.username,
+        email=user.email,
         role=user.role,
         is_active=user.is_active,
         is_admin=user.is_admin,
@@ -46,6 +70,27 @@ async def is_auth_enabled(db: AsyncSession) -> bool:
     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:
     """Set authentication enabled status."""
     from sqlalchemy import func
@@ -216,7 +261,10 @@ async def disable_auth(
 
 @router.post("/login", response_model=LoginResponse)
 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
     auth_enabled = await is_auth_enabled(db)
     if not auth_enabled:
@@ -225,7 +273,15 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
             detail="Authentication is not enabled",
         )
 
+    # Try username-based authentication first
     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:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -263,3 +319,331 @@ async def get_current_user_info(
 async def logout():
     """Logout (client should discard token)."""
     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.library import LibraryFile
 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.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"])
 
@@ -26,6 +33,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
         id=user.id,
         username=user.username,
+        email=user.email,
         role=user.role,
         is_active=user.is_active,
         is_admin=user.is_admin,
@@ -54,9 +62,27 @@ async def create_user(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
     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():
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
@@ -70,9 +96,38 @@ async def create_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(
         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,
         is_active=True,
     )
@@ -92,6 +147,21 @@ async def create_user(
     await db.commit()
     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)
 
 
@@ -161,8 +231,10 @@ async def update_user(
             )
 
     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():
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
@@ -170,6 +242,18 @@ async def update_user(
             )
         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:
         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 jwt.exceptions import PyJWTError as JWTError
 from passlib.context import CryptContext
-from sqlalchemy import select
+from sqlalchemy import func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 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:
-    """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()
 
 
 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)
     if not user:
         return None
@@ -145,6 +158,21 @@ async def authenticate_user(db: AsyncSession, username: str, password: str) -> U
     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:
     """Check if authentication is enabled."""
     try:

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

@@ -24,6 +24,7 @@ class User(Base):
 
     id: Mapped[int] = mapped_column(primary_key=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))
     role: Mapped[str] = mapped_column(
         String(20), default="user"

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

@@ -24,7 +24,8 @@ class LoginResponse(BaseModel):
 
 class UserCreate(BaseModel):
     username: str
-    password: str
+    password: str | None = None  # Optional when advanced auth is enabled
+    email: str | None = None
     role: str = "user"
     group_ids: list[int] | None = None
 
@@ -32,6 +33,7 @@ class UserCreate(BaseModel):
 class UserUpdate(BaseModel):
     username: str | None = None
     password: str | None = None
+    email: str | None = None
     role: str | None = None
     is_active: bool | None = None
     group_ids: list[int] | None = None
@@ -40,6 +42,7 @@ class UserUpdate(BaseModel):
 class UserResponse(BaseModel):
     id: int
     username: str
+    email: str | None = None
     role: str  # Deprecated, kept for backward compatibility
     is_active: bool
     is_admin: bool  # Computed from role and group membership
@@ -65,3 +68,44 @@ class SetupRequest(BaseModel):
 class SetupResponse(BaseModel):
     auth_enabled: bool
     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 {
   id: number;
   username: string;
+  email?: string;
   role: string;  // Deprecated, kept for backward compatibility
   is_active: boolean;
   is_admin: boolean;  // Computed from role and group membership
@@ -1853,7 +1854,8 @@ export interface UserResponse {
 
 export interface UserCreate {
   username: string;
-  password: string;
+  password?: string;  // Optional when advanced auth is enabled
+  email?: string;
   role: string;
   group_ids?: number[];
 }
@@ -1861,6 +1863,7 @@ export interface UserCreate {
 export interface UserUpdate {
   username?: string;
   password?: string;
+  email?: string;
   role?: string;
   is_active?: boolean;
   group_ids?: number[];
@@ -1872,6 +1875,52 @@ export interface SetupRequest {
   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 {
   auth_enabled: boolean;
   admin_created?: boolean;
@@ -1905,6 +1954,38 @@ export const api = {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
       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
   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 { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useAuth } from '../contexts/AuthContext';
 import { useToast } from '../contexts/ToastContext';
 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() {
   const navigate = useNavigate();
@@ -16,6 +17,13 @@ export function LoginPage() {
   const [username, setUsername] = useState('');
   const [password, setPassword] = useState('');
   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({
     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) => {
     e.preventDefault();
     if (!username || !password) {
@@ -37,6 +57,15 @@ export function LoginPage() {
     loginMutation.mutate();
   };
 
+  const handleForgotPassword = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!forgotEmail) {
+      showToast('Please enter your email address', 'error');
+      return;
+    }
+    forgotPasswordMutation.mutate(forgotEmail);
+  };
+
   return (
     <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">
@@ -60,7 +89,9 @@ export function LoginPage() {
           <div className="space-y-4">
             <div>
               <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>
               <input
                 id="username"
@@ -69,7 +100,9 @@ export function LoginPage() {
                 value={username}
                 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"
-                placeholder={t('login.usernamePlaceholder')}
+                placeholder={advancedAuthStatus?.advanced_auth_enabled 
+                  ? t('login.usernameOrEmailPlaceholder') || 'Enter your username or email'
+                  : t('login.usernamePlaceholder')}
                 autoComplete="username"
               />
             </div>
@@ -101,22 +134,24 @@ export function LoginPage() {
             </button>
           </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>
       </div>
 
       {/* Forgot Password Modal */}
       {showForgotPassword && (
         <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)}
         >
           <div
@@ -125,39 +160,87 @@ export function LoginPage() {
           >
             <div className="flex items-center justify-between mb-4">
               <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>
               </div>
               <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"
               >
                 <X className="w-5 h-5" />
               </button>
             </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>
       )}

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

@@ -1,5 +1,5 @@
 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 { useNavigate, useSearchParams } from 'react-router-dom';
 import { api } from '../api/client';
@@ -19,6 +19,7 @@ import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { ExternalLinksSettings } from '../components/ExternalLinksSettings';
 import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
+import { EmailSettings } from '../components/EmailSettings';
 import { APIBrowser } from '../components/APIBrowser';
 import { virtualPrinterApi } from '../api/client';
 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 { 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];
 
 export function SettingsPage() {
@@ -1025,6 +1026,17 @@ export function SettingsPage() {
             <span className="w-2 h-2 rounded-full bg-green-400" />
           )}
         </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
           onClick={() => handleTabChange('backup')}
           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 */}
       {activeTab === 'backup' && (
         <GitHubBackupSettings />

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

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 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 type { UserCreate, UserUpdate, UserResponse } from '../api/client';
 import { useAuth } from '../contexts/AuthContext';
@@ -14,6 +14,7 @@ import { ConfirmModal } from '../components/ConfirmModal';
 interface FormData extends UserCreate {
   group_ids: number[];
   confirmPassword: string;
+  email?: string;
 }
 
 export function UsersPage() {
@@ -29,23 +30,30 @@ export function UsersPage() {
   const [formData, setFormData] = useState<FormData>({
     username: '',
     password: '',
+    email: '',
     confirmPassword: '',
     role: 'user',
     group_ids: [],
   });
 
+  // Check if advanced auth is enabled
+  const { data: advancedAuthStatus } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
+
   // Close modal on Escape key
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if (e.key === 'Escape') {
         if (showCreateModal) {
           setShowCreateModal(false);
-          setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+          setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
         }
         if (showEditModal) {
           setShowEditModal(false);
           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: ['groups'] });
       setShowCreateModal(false);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.created'));
     },
     onError: (error: Error) => {
@@ -86,7 +94,7 @@ export function UsersPage() {
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowEditModal(false);
       setEditingUserId(null);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.updated'));
     },
     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 = () => {
-    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');
       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;
     }
-    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({
       username: formData.username,
-      password: formData.password,
+      password: advancedAuthEnabled ? undefined : formData.password,
+      email: formData.email || undefined,
       role: formData.role,
       group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
     });
@@ -141,6 +178,7 @@ export function UsersPage() {
     const updateData: UserUpdate = {
       username: formData.username || undefined,
       password: formData.password || undefined,
+      email: formData.email || undefined,
       role: formData.role,
       group_ids: formData.group_ids,
     };
@@ -148,6 +186,10 @@ export function UsersPage() {
     if (!updateData.password) {
       delete updateData.password;
     }
+    // Remove email if empty
+    if (!updateData.email) {
+      delete updateData.email;
+    }
     updateMutation.mutate({ id, data: updateData });
   };
 
@@ -160,6 +202,7 @@ export function UsersPage() {
     setFormData({
       username: user.username,
       password: '',
+      email: user.email || '',
       confirmPassword: '',
       role: user.role,
       group_ids: user.groups?.map(g => g.id) || [],
@@ -170,7 +213,7 @@ export function UsersPage() {
   const closeEditModal = () => {
     setShowEditModal(false);
     setEditingUserId(null);
-    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
   };
 
   const toggleGroup = (groupId: number) => {
@@ -221,7 +264,7 @@ export function UsersPage() {
         <Button
           onClick={() => {
             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" />
@@ -316,6 +359,17 @@ export function UsersPage() {
                             {t('users.delete')}
                           </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>
                     </td>
                   </tr>
@@ -332,7 +386,7 @@ export function UsersPage() {
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           onClick={() => {
             setShowCreateModal(false);
-            setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
         >
           <Card
@@ -350,7 +404,7 @@ export function UsersPage() {
                   size="sm"
                   onClick={() => {
                     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" />
@@ -372,41 +426,67 @@ export function UsersPage() {
                     autoComplete="username"
                   />
                 </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>
                   <label className="block text-sm font-medium text-white mb-2">
                     {t('users.form.groups')}
@@ -440,7 +520,7 @@ export function UsersPage() {
                   variant="secondary"
                   onClick={() => {
                     setShowCreateModal(false);
-                    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                 >
                   {t('users.modal.cancel')}
@@ -507,6 +587,18 @@ export function UsersPage() {
                     autoComplete="username"
                   />
                 </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>
                   <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>

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-962x7uln.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Ce6bi4R2.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-togsBDt6.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <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>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff