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

Merge pull request #322 from cadtoolbox/feature_user_authentication

Feature - Advanced Authentication via Email
MartinNYHC 3 месяцев назад
Родитель
Сommit
6ecc29de54
31 измененных файлов с 3907 добавлено и 374 удалено
  1. 388 2
      backend/app/api/routes/auth.py
  2. 3 0
      backend/app/api/routes/notification_templates.py
  3. 21 0
      backend/app/api/routes/settings.py
  4. 88 6
      backend/app/api/routes/users.py
  5. 32 4
      backend/app/core/auth.py
  6. 2 0
      backend/app/main.py
  7. 12 0
      backend/app/models/notification_template.py
  8. 1 0
      backend/app/models/user.py
  9. 51 1
      backend/app/schemas/auth.py
  10. 18 0
      backend/app/schemas/notification_template.py
  11. 516 0
      backend/app/services/email_service.py
  12. 27 0
      backend/tests/integration/test_auth_api.py
  13. 163 0
      backend/tests/unit/services/test_email_service.py
  14. 228 198
      frontend/package-lock.json
  15. 3 0
      frontend/package.json
  16. 84 1
      frontend/src/api/client.ts
  17. 11 9
      frontend/src/components/AddSmartPlugModal.tsx
  18. 6 5
      frontend/src/components/ConfigureAmsSlotModal.tsx
  19. 181 0
      frontend/src/components/CreateUserAdvancedAuthModal.tsx
  20. 401 0
      frontend/src/components/EmailSettings.tsx
  21. 5 3
      frontend/src/components/ExternalLinksSettings.tsx
  22. 12 10
      frontend/src/components/RichTextEditor.tsx
  23. 10 8
      frontend/src/components/SmartPlugCard.tsx
  24. 417 0
      frontend/src/i18n/locales/de.ts
  25. 418 0
      frontend/src/i18n/locales/en.ts
  26. 335 3
      frontend/src/i18n/locales/ja.ts
  27. 141 50
      frontend/src/pages/LoginPage.tsx
  28. 203 53
      frontend/src/pages/SettingsPage.tsx
  29. 130 21
      frontend/src/pages/UsersPage.tsx
  30. 0 0
      static/assets/index-BwfbnBQ9.css
  31. 0 0
      static/assets/index-QQNcmTSY.js

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

@@ -5,19 +5,43 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
+from backend.app.api.routes.settings import get_external_login_url
 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_from_template,
+    generate_secure_password,
+    get_smtp_settings,
+    save_smtp_settings,
+    send_email,
+)
 
 
 def _user_to_response(user: User) -> UserResponse:
@@ -25,6 +49,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 +71,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 +262,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 +274,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 +320,332 @@ 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_security=test_request.smtp_security,
+            smtp_auth_enabled=test_request.smtp_auth_enabled,
+            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
+
+    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()
+
+            login_url = await get_external_login_url(db)
+
+            # Send password reset email
+            subject, text_body, html_body = await create_password_reset_email_from_template(
+                db, 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
+
+    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()
+
+        login_url = await get_external_login_url(db)
+
+        # Send password reset email
+        subject, text_body, html_body = await create_password_reset_email_from_template(
+            db, 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)}",
+        )

+ 3 - 0
backend/app/api/routes/notification_templates.py

@@ -43,6 +43,9 @@ EVENT_NAMES = {
     "queue_job_skipped": "Queue Job Skipped",
     "queue_job_failed": "Queue Job Failed",
     "queue_completed": "Queue Completed",
+    # User management
+    "user_created": "Welcome Email",
+    "password_reset": "Password Reset",
 }
 
 

+ 21 - 0
backend/app/api/routes/settings.py

@@ -32,6 +32,27 @@ async def get_setting(db: AsyncSession, key: str) -> str | None:
     return setting.value if setting else None
 
 
+async def get_external_login_url(db: AsyncSession) -> str:
+    """Get the external URL for the login page.
+
+    Uses external_url from settings if available, otherwise falls back to APP_URL env var.
+
+    Args:
+        db: Database session
+
+    Returns:
+        Full URL to the login page
+    """
+    import os
+
+    external_url = await get_setting(db, "external_url")
+    if external_url:
+        external_url = external_url.rstrip("/")
+    else:
+        external_url = os.environ.get("APP_URL", "http://localhost:5173")
+    return external_url + "/login"
+
+
 async def set_setting(db: AsyncSession, key: str, value: str) -> None:
     """Set a single setting value."""
     from sqlalchemy import func

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

@@ -3,6 +3,7 @@ from sqlalchemy import delete, func, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
+from backend.app.api.routes.settings import get_external_login_url
 from backend.app.core.auth import (
     RequirePermissionIfAuthEnabled,
     get_current_user_optional,
@@ -15,8 +16,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_from_template,
+    generate_secure_password,
+    get_smtp_settings,
+    send_email,
+)
 
 router = APIRouter(prefix="/users", tags=["users"])
 
@@ -26,6 +34,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 +63,24 @@ 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
+
+    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 +94,36 @@ 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 +143,23 @@ 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 = await get_external_login_url(db)
+                subject, text_body, html_body = await create_welcome_email_from_template(
+                    db, 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 +229,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 +240,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:

+ 2 - 0
backend/app/main.py

@@ -2764,6 +2764,8 @@ PUBLIC_API_ROUTES = {
     "/api/v1/auth/status",
     "/api/v1/auth/login",
     "/api/v1/auth/setup",  # Needed for initial setup and recovery
+    "/api/v1/auth/advanced-auth/status",  # Advanced auth status needed for login page
+    "/api/v1/auth/forgot-password",  # Password reset for advanced auth
     # Version check for updates (no sensitive data)
     "/api/v1/updates/version",
     # Metrics endpoint handles its own prometheus_token authentication

+ 12 - 0
backend/app/models/notification_template.py

@@ -146,4 +146,16 @@ DEFAULT_TEMPLATES = [
         "title_template": "Queue Complete",
         "body_template": "All {completed_count} queued jobs have finished",
     },
+    {
+        "event_type": "user_created",
+        "name": "Welcome Email",
+        "title_template": "Welcome to {app_name}",
+        "body_template": "Welcome {username}!\n\nYour account has been created.\nUsername: {username}\nPassword: {password}\n\nLogin at: {login_url}",
+    },
+    {
+        "event_type": "password_reset",
+        "name": "Password Reset",
+        "title_template": "{app_name} - Password Reset",
+        "body_template": "Hello {username},\n\nYour password has been reset.\nNew Password: {password}\n\nLogin at: {login_url}",
+    },
 ]

+ 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"

+ 51 - 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,50 @@ 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 | None = None  # Optional when auth is disabled
+    smtp_password: str | None = None  # Optional for read operations or when auth is disabled
+    smtp_security: str = "starttls"  # 'starttls', 'ssl', 'none'
+    smtp_auth_enabled: bool = True
+    smtp_from_email: str
+    smtp_from_name: str = "BamBuddy"
+    # Deprecated field for backward compatibility
+    smtp_use_tls: bool | None = None
+
+
+class TestSMTPRequest(BaseModel):
+    smtp_host: str
+    smtp_port: int
+    smtp_username: str | None = None  # Optional when auth is disabled
+    smtp_password: str | None = None  # Optional when auth is disabled
+    smtp_security: str = "starttls"  # 'starttls', 'ssl', 'none'
+    smtp_auth_enabled: bool = True
+    smtp_from_email: str
+    test_recipient: str
+    # Deprecated field for backward compatibility
+    smtp_use_tls: bool | None = None
+
+
+class TestSMTPResponse(BaseModel):
+    success: bool
+    message: str

+ 18 - 0
backend/app/schemas/notification_template.py

@@ -53,6 +53,9 @@ EVENT_VARIABLES: dict[str, list[str]] = {
     "queue_job_skipped": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_job_failed": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_completed": ["completed_count", "timestamp", "app_name"],
+    # User management notifications
+    "user_created": ["username", "password", "login_url", "app_name", "timestamp"],
+    "password_reset": ["username", "password", "login_url", "app_name", "timestamp"],
 }
 
 # Sample data for previewing templates
@@ -191,6 +194,21 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 18:30",
         "app_name": "Bambuddy",
     },
+    # User management notifications
+    "user_created": {
+        "username": "john_doe",
+        "password": "TempPass123!",
+        "login_url": "https://bambuddy.example.com/login",
+        "app_name": "Bambuddy",
+        "timestamp": "2024-01-15 14:30",
+    },
+    "password_reset": {
+        "username": "john_doe",
+        "password": "NewPass456!",
+        "login_url": "https://bambuddy.example.com/login",
+        "app_name": "Bambuddy",
+        "timestamp": "2024-01-15 14:30",
+    },
 }
 
 

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

@@ -0,0 +1,516 @@
+"""Email service for sending authentication-related emails."""
+
+from __future__ import annotations
+
+import html
+import logging
+import re
+import secrets
+import smtplib
+import string
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from typing import Any
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.notification_template import NotificationTemplate
+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_notification_template(db: AsyncSession, event_type: str) -> NotificationTemplate | None:
+    """Get a notification template by event type from database.
+
+    Args:
+        db: Database session
+        event_type: Type of event (e.g., 'user_created', 'password_reset')
+
+    Returns:
+        NotificationTemplate object or None if not found
+    """
+    result = await db.execute(select(NotificationTemplate).where(NotificationTemplate.event_type == event_type))
+    return result.scalar_one_or_none()
+
+
+def render_template(template_str: str, variables: dict[str, Any]) -> str:
+    """Render a template string with variables.
+
+    Args:
+        template_str: Template string with {variable} placeholders
+        variables: Dictionary of variables to substitute
+
+    Returns:
+        Rendered template string
+    """
+    result = template_str
+    for key, value in variables.items():
+        result = result.replace("{" + key + "}", str(value) if value is not None else "")
+    # Remove any remaining unreplaced placeholders (case-insensitive, alphanumeric + underscore)
+    result = re.sub(r"\{[a-zA-Z0-9_]+\}", "", result)
+    return result
+
+
+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_security",
+                    "smtp_auth_enabled",
+                    "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_from_email"]
+    if not all(key in settings_dict for key in required_keys):
+        return None
+
+    # Handle migration: convert old smtp_use_tls to smtp_security if needed
+    smtp_security = settings_dict.get("smtp_security")
+    if not smtp_security:
+        # Migrate from old smtp_use_tls format
+        smtp_use_tls = settings_dict.get("smtp_use_tls", "true").lower() == "true"
+        smtp_security = "starttls" if smtp_use_tls else "ssl"
+
+    smtp_auth_enabled = settings_dict.get("smtp_auth_enabled", "true").lower() == "true"
+
+    return SMTPSettings(
+        smtp_host=settings_dict["smtp_host"],
+        smtp_port=int(settings_dict["smtp_port"]),
+        smtp_username=settings_dict.get("smtp_username"),
+        smtp_password=settings_dict.get("smtp_password"),
+        smtp_security=smtp_security,
+        smtp_auth_enabled=smtp_auth_enabled,
+        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_security": smtp_settings.smtp_security,
+        "smtp_auth_enabled": "true" if smtp_settings.smtp_auth_enabled else "false",
+        "smtp_from_email": smtp_settings.smtp_from_email,
+        "smtp_from_name": smtp_settings.smtp_from_name,
+    }
+
+    # Only save username if auth is enabled or if provided
+    if smtp_settings.smtp_username:
+        settings_data["smtp_username"] = smtp_settings.smtp_username
+
+    # 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:
+        security = smtp_settings.smtp_security
+        auth_enabled = smtp_settings.smtp_auth_enabled
+
+        # Validate username is provided when authentication is enabled
+        if auth_enabled and smtp_settings.smtp_password:
+            if not smtp_settings.smtp_username:
+                raise ValueError("SMTP username is required when authentication is enabled")
+
+        if security == "ssl":
+            # Direct SSL connection (typically port 465)
+            with smtplib.SMTP_SSL(smtp_settings.smtp_host, smtp_settings.smtp_port, timeout=10) as server:
+                if auth_enabled and smtp_settings.smtp_password and smtp_settings.smtp_username:
+                    server.login(smtp_settings.smtp_username, smtp_settings.smtp_password)
+                server.send_message(msg)
+        elif security == "starttls":
+            # STARTTLS upgrade (typically port 587)
+            with smtplib.SMTP(smtp_settings.smtp_host, smtp_settings.smtp_port, timeout=10) as server:
+                server.starttls()
+                if auth_enabled and smtp_settings.smtp_password and smtp_settings.smtp_username:
+                    server.login(smtp_settings.smtp_username, smtp_settings.smtp_password)
+                server.send_message(msg)
+        else:
+            # No encryption (typically port 25) - use with caution
+            with smtplib.SMTP(smtp_settings.smtp_host, smtp_settings.smtp_port, timeout=10) as server:
+                if auth_enabled and smtp_settings.smtp_password and smtp_settings.smtp_username:
+                    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%); background-color: #667eea; padding: 20px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: #ffffff; margin: 0; font-size: 24px; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">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-color: #667eea; color: #ffffff; 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%); background-color: #667eea; padding: 20px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: #ffffff; margin: 0; font-size: 24px; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">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-color: #667eea; color: #ffffff; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
+        </div>
+
+        <div style="background-color: #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
+
+
+async def create_welcome_email_from_template(
+    db: AsyncSession, username: str, password: str, login_url: str, app_name: str = "BamBuddy"
+) -> tuple[str, str, str]:
+    """Create welcome email content using notification template from database.
+
+    Args:
+        db: Database session
+        username: Username of the new user
+        password: Auto-generated password
+        login_url: URL to login page
+        app_name: Application name (default: BamBuddy)
+
+    Returns:
+        Tuple of (subject, text_body, html_body)
+    """
+    # Try to get template from database
+    template = await get_notification_template(db, "user_created")
+
+    if template:
+        # Render template with variables
+        variables = {
+            "app_name": app_name,
+            "username": username,
+            "password": password,
+            "login_url": login_url,
+        }
+
+        subject = render_template(template.title_template, variables)
+        text_body = render_template(template.body_template, variables)
+
+        # Create HTML version with embedded login button
+        # Escape text_body to prevent XSS vulnerabilities and convert newlines to <br> tags
+        escaped_text_body = html.escape(text_body).replace("\n", "<br>\n")
+        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%); background-color: #667eea; padding: 30px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: #ffffff; margin: 0; font-size: 24px; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">{html.escape(subject)}</h1>
+    </div>
+    <div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
+        <div style="font-size: 16px;">{escaped_text_body}</div>
+
+        <div style="text-align: center; margin: 30px 0;">
+            <a href="{login_url}" style="display: inline-block; background-color: #667eea; color: #ffffff; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
+        </div>
+    </div>
+</body>
+</html>
+"""
+
+        logger.info("Using custom welcome email template from database")
+        return subject, text_body, html_body
+    else:
+        # Fallback to hardcoded template
+        logger.warning("No welcome email template found in database, using default")
+        return create_welcome_email(username, password, login_url)
+
+
+async def create_password_reset_email_from_template(
+    db: AsyncSession, username: str, password: str, login_url: str, app_name: str = "BamBuddy"
+) -> tuple[str, str, str]:
+    """Create password reset email content using notification template from database.
+
+    Args:
+        db: Database session
+        username: Username of the user
+        password: New auto-generated password
+        login_url: URL to login page
+        app_name: Application name (default: BamBuddy)
+
+    Returns:
+        Tuple of (subject, text_body, html_body)
+    """
+    # Try to get template from database
+    template = await get_notification_template(db, "password_reset")
+
+    if template:
+        # Render template with variables
+        variables = {
+            "app_name": app_name,
+            "username": username,
+            "password": password,
+            "login_url": login_url,
+        }
+
+        subject = render_template(template.title_template, variables)
+        text_body = render_template(template.body_template, variables)
+
+        # Create HTML version with embedded login button
+        # Escape text_body to prevent XSS vulnerabilities and convert newlines to <br> tags
+        escaped_text_body = html.escape(text_body).replace("\n", "<br>\n")
+        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%); background-color: #667eea; padding: 30px; border-radius: 8px 8px 0 0;">
+        <h1 style="color: #ffffff; margin: 0; font-size: 24px; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">{html.escape(subject)}</h1>
+    </div>
+    <div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; border: 1px solid #ddd; border-top: none;">
+        <div style="font-size: 16px;">{escaped_text_body}</div>
+
+        <div style="text-align: center; margin: 30px 0;">
+            <a href="{login_url}" style="display: inline-block; background-color: #667eea; color: #ffffff; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">Login Now</a>
+        </div>
+
+        <div style="background-color: #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>
+    </div>
+</body>
+</html>
+"""
+
+        logger.info("Using custom password reset email template from database")
+        return subject, text_body, html_body
+    else:
+        # Fallback to hardcoded template
+        logger.warning("No password reset email template found in database, using default")
+        return create_password_reset_email(username, password, login_url)

+ 27 - 0
backend/tests/integration/test_auth_api.py

@@ -774,3 +774,30 @@ class TestAuthMiddlewarePublicRoutes:
             headers={"Authorization": f"Bearer {token}"},
         )
         assert response.status_code == 200
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_advanced_auth_status_is_public(self, async_client: AsyncClient, enabled_auth):
+        """Verify /api/v1/auth/advanced-auth/status is accessible without auth."""
+        response = await async_client.get("/api/v1/auth/advanced-auth/status")
+        # Should not be 401 (must be accessible for login page)
+        assert response.status_code != 401
+        # Should return valid response (200 with auth status)
+        if response.status_code == 200:
+            result = response.json()
+            assert "advanced_auth_enabled" in result
+            assert "smtp_configured" in result
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_forgot_password_is_public(self, async_client: AsyncClient, enabled_auth):
+        """Verify /api/v1/auth/forgot-password is accessible without auth."""
+        response = await async_client.post(
+            "/api/v1/auth/forgot-password",
+            json={"email": "test@example.com"},
+        )
+        # Should not be 401 (must be accessible for password reset from login page)
+        assert response.status_code != 401
+        # Will likely be 400 (advanced auth not enabled) but that's okay -
+        # the important thing is it's not blocked by auth middleware
+        assert response.status_code in [200, 400]

+ 163 - 0
backend/tests/unit/services/test_email_service.py

@@ -0,0 +1,163 @@
+"""Unit tests for email service.
+
+These tests verify email template rendering and HTML formatting.
+"""
+
+from unittest.mock import AsyncMock, patch
+
+import pytest
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from backend.app.models.notification_template import NotificationTemplate
+from backend.app.services.email_service import (
+    create_password_reset_email_from_template,
+    create_welcome_email_from_template,
+)
+
+
+class TestEmailTemplateFormatting:
+    """Tests for email template formatting."""
+
+    @pytest.mark.asyncio
+    async def test_welcome_email_newlines_converted_to_br(self):
+        """Verify that newlines in welcome email body are converted to <br> tags."""
+        # Mock database session
+        db = AsyncMock(spec=AsyncSession)
+
+        # Mock template with newlines
+        template = NotificationTemplate(
+            event_type="user_created",
+            name="Welcome Email",
+            title_template="Welcome to {app_name}",
+            body_template="Hello {username}!\n\nYour password is: {password}\n\nPlease login at: {login_url}",
+            is_default=True,
+        )
+
+        # Patch get_notification_template to return our template
+        with patch("backend.app.services.email_service.get_notification_template", return_value=template):
+            # Generate email
+            subject, text_body, html_body = await create_welcome_email_from_template(
+                db=db,
+                username="testuser",
+                password="testpass123",
+                login_url="http://example.com/login",
+                app_name="TestApp",
+            )
+
+        # Verify subject
+        assert subject == "Welcome to TestApp"
+
+        # Verify text body has newlines
+        assert "\n\n" in text_body
+        assert "Hello testuser!" in text_body
+        assert "Your password is: testpass123" in text_body
+
+        # Verify HTML body has <br> tags instead of relying on CSS
+        assert "<br>" in html_body
+        # Should not use white-space: pre-wrap
+        assert "white-space: pre-wrap" not in html_body
+        # Should have proper structure
+        assert "<!DOCTYPE html>" in html_body
+        assert '<div style="font-size: 16px;">' in html_body
+
+        # Verify that escaped content is present (XSS protection)
+        assert "Hello testuser!<br>" in html_body
+        assert "Your password is: testpass123<br>" in html_body
+
+    @pytest.mark.asyncio
+    async def test_password_reset_email_newlines_converted_to_br(self):
+        """Verify that newlines in password reset email body are converted to <br> tags."""
+        # Mock database session
+        db = AsyncMock(spec=AsyncSession)
+
+        # Mock template with newlines
+        template = NotificationTemplate(
+            event_type="password_reset",
+            name="Password Reset",
+            title_template="{app_name} - Password Reset",
+            body_template="Hello {username},\n\nYour password has been reset.\nNew password: {password}\n\nLogin at: {login_url}",
+            is_default=True,
+        )
+
+        # Patch get_notification_template to return our template
+        with patch("backend.app.services.email_service.get_notification_template", return_value=template):
+            # Generate email
+            subject, text_body, html_body = await create_password_reset_email_from_template(
+                db=db,
+                username="testuser",
+                password="newpass456",
+                login_url="http://example.com/login",
+                app_name="TestApp",
+            )
+
+        # Verify subject
+        assert subject == "TestApp - Password Reset"
+
+        # Verify text body has newlines
+        assert "\n\n" in text_body
+        assert "Hello testuser," in text_body
+
+        # Verify HTML body has <br> tags
+        assert "<br>" in html_body
+        # Should not use white-space: pre-wrap
+        assert "white-space: pre-wrap" not in html_body
+        # Should have security alert
+        assert "Security Alert" in html_body
+
+    @pytest.mark.asyncio
+    async def test_email_header_padding(self):
+        """Verify that email header has proper padding to prevent cutoff."""
+        # Mock database session
+        db = AsyncMock(spec=AsyncSession)
+
+        # Mock template
+        template = NotificationTemplate(
+            event_type="user_created",
+            name="Welcome Email",
+            title_template="Welcome",
+            body_template="Test body",
+            is_default=True,
+        )
+
+        # Patch get_notification_template to return our template
+        with patch("backend.app.services.email_service.get_notification_template", return_value=template):
+            # Generate email
+            subject, text_body, html_body = await create_welcome_email_from_template(
+                db=db,
+                username="testuser",
+                password="testpass123",
+                login_url="http://example.com/login",
+            )
+
+        # Verify header has 30px padding (not 20px which was cutting off)
+        assert "padding: 30px; border-radius: 8px 8px 0 0;" in html_body
+
+    @pytest.mark.asyncio
+    async def test_email_xss_protection(self):
+        """Verify that HTML escaping is applied to prevent XSS attacks."""
+        # Mock database session
+        db = AsyncMock(spec=AsyncSession)
+
+        # Mock template with potential XSS content
+        template = NotificationTemplate(
+            event_type="user_created",
+            name="Welcome Email",
+            title_template="Welcome <script>alert('xss')</script>",
+            body_template="Hello <script>alert('xss')</script>\nTest",
+            is_default=True,
+        )
+
+        # Patch get_notification_template to return our template
+        with patch("backend.app.services.email_service.get_notification_template", return_value=template):
+            # Generate email
+            subject, text_body, html_body = await create_welcome_email_from_template(
+                db=db,
+                username="testuser",
+                password="testpass123",
+                login_url="http://example.com/login",
+            )
+
+        # Verify that script tags are escaped
+        assert "&lt;script&gt;" in html_body
+        # Verify no unescaped script tags
+        assert "<script>" not in html_body

Разница между файлами не показана из-за своего большого размера
+ 228 - 198
frontend/package-lock.json


+ 3 - 0
frontend/package.json

@@ -17,6 +17,7 @@
     "@dnd-kit/core": "^6.3.1",
     "@dnd-kit/sortable": "^10.0.0",
     "@dnd-kit/utilities": "^3.2.2",
+    "@floating-ui/dom": "^1.7.5",
     "@tanstack/react-query": "^5.90.11",
     "@tiptap/extension-color": "^3.11.1",
     "@tiptap/extension-image": "^3.11.1",
@@ -31,8 +32,10 @@
     "i18next": "^25.6.3",
     "i18next-browser-languagedetector": "^8.2.0",
     "i18next-http-backend": "^3.0.2",
+    "install": "^0.13.0",
     "jszip": "^3.10.1",
     "lucide-react": "^0.555.0",
+    "npm": "^11.9.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-i18next": "^16.3.5",

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

@@ -1906,6 +1906,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
@@ -1916,7 +1917,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[];
 }
@@ -1924,6 +1926,7 @@ export interface UserCreate {
 export interface UserUpdate {
   username?: string;
   password?: string;
+  email?: string;
   role?: string;
   is_active?: boolean;
   group_ids?: number[];
@@ -1935,6 +1938,54 @@ 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_security: 'starttls' | 'ssl' | 'none';
+  smtp_auth_enabled: 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_security: 'starttls' | 'ssl' | 'none';
+  smtp_auth_enabled: 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;
@@ -1968,6 +2019,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/'),

+ 11 - 9
frontend/src/components/AddSmartPlugModal.tsx

@@ -1,6 +1,7 @@
 import { useState, useEffect, useRef } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
 import { Button } from './Button';
@@ -11,6 +12,7 @@ interface AddSmartPlugModalProps {
 }
 
 export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const isEditing = !!plug;
 
@@ -469,7 +471,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {isScanning && scanProgress.total > 0 && (
                 <div className="space-y-1">
                   <div className="flex justify-between text-xs text-bambu-gray">
-                    <span>Scanning network...</span>
+                    <span>{t('smartPlugs.addSmartPlug.scanningNetwork')}</span>
                     <span>{scanProgress.scanned} / {scanProgress.total}</span>
                   </div>
                   <div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
@@ -538,7 +540,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                       disabled
                       className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray cursor-not-allowed opacity-50"
                     >
-                      <option>Choose an entity...</option>
+                      <option>{t('smartPlugs.addSmartPlug.chooseEntity')}</option>
                     </select>
                   </div>
                 </div>
@@ -585,7 +587,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                               setIsEntityDropdownOpen(true);
                               setHaEntitySearch('');
                             }}
-                            placeholder="Search entities..."
+                            placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEntities')}
                             className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                           />
                           {haEntityId && !isEntityDropdownOpen && (
@@ -697,7 +699,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsPowerDropdownOpen(true);
                                   setPowerSensorSearch('');
                                 }}
-                                placeholder="Search power sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchPowerSensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haPowerEntity && !isPowerDropdownOpen && (
@@ -781,7 +783,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTodayDropdownOpen(true);
                                   setEnergyTodaySearch('');
                                 }}
-                                placeholder="Search energy sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
@@ -865,7 +867,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTotalDropdownOpen(true);
                                   setEnergyTotalSearch('');
                                 }}
-                                placeholder="Search energy sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
@@ -1060,7 +1062,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                           type="text"
                           value={mqttStateOnValue}
                           onChange={(e) => setMqttStateOnValue(e.target.value)}
-                          placeholder="ON, true, 1"
+                          placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttStateOnValue')}
                           className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                         />
                       </div>
@@ -1128,7 +1130,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               ) : (
                 <>
                   <WifiOff className="w-5 h-5" />
-                  <span>Connection failed</span>
+                  <span>{t('smartPlugs.addSmartPlug.connectionFailed')}</span>
                 </>
               )}
             </div>
@@ -1141,7 +1143,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               type="text"
               value={name}
               onChange={(e) => setName(e.target.value)}
-              placeholder="Living Room Plug"
+              placeholder={t('smartPlugs.addSmartPlug.placeholders.plugName')}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
             />
           </div>

+ 6 - 5
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react';
 import { useQuery, useMutation } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { KProfile } from '../api/client';
 import { Button } from './Button';
@@ -634,7 +635,7 @@ export function ConfigureAmsSlotModal({
               <div className="text-center space-y-3">
                 <CheckCircle2 className="w-16 h-16 text-bambu-green mx-auto" />
                 <p className="text-lg font-semibold text-white">Slot Configured!</p>
-                <p className="text-sm text-bambu-gray">Settings sent to printer</p>
+                <p className="text-sm text-bambu-gray">{t('configureAmsSlot.settingsSentToPrinter')}</p>
               </div>
             </div>
           )}
@@ -672,7 +673,7 @@ export function ConfigureAmsSlotModal({
                 <div className="relative">
                   <input
                     type="text"
-                    placeholder="Search presets..."
+                    placeholder={t('configureAmsSlot.searchPresets')}
                     value={searchQuery}
                     onChange={(e) => setSearchQuery(e.target.value)}
                     className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none mb-2"
@@ -705,7 +706,7 @@ export function ConfigureAmsSlotModal({
                               )}
                               {preset.isUser && (
                                 <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
-                                  Custom
+                                  {t('configureAmsSlot.custom')}
                                 </span>
                               )}
                             </div>
@@ -822,7 +823,7 @@ export function ConfigureAmsSlotModal({
                   />
                   <input
                     type="text"
-                    placeholder="Color name or hex (e.g., brown, FF8800)"
+                    placeholder={t('configureAmsSlot.colorPlaceholder')}
                     value={colorInput}
                     onChange={(e) => {
                       const input = e.target.value;
@@ -852,7 +853,7 @@ export function ConfigureAmsSlotModal({
                         setColorInput('');
                       }}
                       className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded"
-                      title="Clear custom color"
+                      title={t('configureAmsSlot.clearCustomColor')}
                     >
                       Clear
                     </button>

+ 181 - 0
frontend/src/components/CreateUserAdvancedAuthModal.tsx

@@ -0,0 +1,181 @@
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { X, Plus, Loader2, Users as UsersIcon } from 'lucide-react';
+import { Card, CardContent, CardHeader } from './Card';
+import { Button } from './Button';
+import type { Group, UserCreate } from '../api/client';
+
+interface AdvancedAuthFormData extends UserCreate {
+  group_ids: number[];
+  confirmPassword: string;
+  email?: string;
+}
+
+interface CreateUserAdvancedAuthModalProps {
+  formData: AdvancedAuthFormData;
+  setFormData: (data: AdvancedAuthFormData) => void;
+  groups: Group[];
+  onClose: () => void;
+  onCreate: () => void;
+  isCreating: boolean;
+  isCreateButtonDisabled: boolean;
+}
+
+export function CreateUserAdvancedAuthModal({
+  formData,
+  setFormData,
+  groups,
+  onClose,
+  onCreate,
+  isCreating,
+  isCreateButtonDisabled,
+}: CreateUserAdvancedAuthModalProps) {
+  const { t } = useTranslation();
+
+  // Close modal on Escape key
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onClose();
+      }
+    };
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [onClose]);
+
+  const toggleGroup = (groupId: number) => {
+    setFormData({
+      ...formData,
+      group_ids: formData.group_ids.includes(groupId)
+        ? formData.group_ids.filter(id => id !== groupId)
+        : [...formData.group_ids, groupId],
+    });
+  };
+
+  return (
+    <div
+      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
+      onClick={onClose}
+    >
+      <Card
+        className="w-full max-w-md"
+        onClick={(e: React.MouseEvent) => e.stopPropagation()}
+      >
+        <CardHeader>
+          <div className="flex items-center justify-between">
+            <div className="flex flex-col gap-1">
+              <div className="flex items-center gap-2">
+                <UsersIcon className="w-5 h-5 text-bambu-green" />
+                <h2 className="text-lg font-semibold text-white">{t('users.modal.createUser')}</h2>
+              </div>
+              <p className="text-sm text-bambu-gray ml-7">{t('users.modal.advancedAuthSubtitle') || 'with Advanced Authentication'}</p>
+            </div>
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={onClose}
+            >
+              <X className="w-5 h-5" />
+            </Button>
+          </div>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            {/* Username Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.username')} <span className="text-red-400">*</span>
+              </label>
+              <input
+                type="text"
+                value={formData.username}
+                onChange={(e) => setFormData({ ...formData, username: 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.usernamePlaceholder')}
+                autoComplete="username"
+                required
+              />
+            </div>
+
+            {/* Email Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.email') || 'Email'} <span className="text-red-400">*</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'}
+                required
+              />
+            </div>
+
+            {/* Info box about auto-generated password */}
+            <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>
+
+            {/* Groups Field */}
+            <div>
+              <label className="block text-sm font-medium text-white mb-2">
+                {t('users.form.groups')}
+              </label>
+              <div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
+                {groups.map(group => (
+                  <label
+                    key={group.id}
+                    className="flex items-center gap-3 px-2 py-1.5 rounded hover:bg-bambu-dark-tertiary cursor-pointer"
+                  >
+                    <input
+                      type="checkbox"
+                      checked={formData.group_ids.includes(group.id)}
+                      onChange={() => toggleGroup(group.id)}
+                      className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark"
+                    />
+                    <span className="text-sm text-white">{group.name}</span>
+                    {group.is_system && (
+                      <span className="text-xs text-yellow-400">({t('users.system')})</span>
+                    )}
+                  </label>
+                ))}
+                {groups.length === 0 && (
+                  <p className="text-sm text-bambu-gray">{t('users.noGroupsAvailable')}</p>
+                )}
+              </div>
+            </div>
+          </div>
+
+          {/* Action Buttons */}
+          <div className="mt-6 flex justify-end gap-3">
+            <Button
+              variant="secondary"
+              onClick={onClose}
+            >
+              {t('users.modal.cancel')}
+            </Button>
+            <Button
+              onClick={onCreate}
+              disabled={isCreateButtonDisabled}
+            >
+              {isCreating ? (
+                <>
+                  <Loader2 className="w-4 h-4 animate-spin" />
+                  {t('users.modal.creating')}
+                </>
+              ) : (
+                <>
+                  <Plus className="w-4 h-4" />
+                  {t('users.modal.createUser')}
+                </>
+              )}
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

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

@@ -0,0 +1,401 @@
+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_security: 'starttls',
+    smtp_auth_enabled: 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(t('settings.email.success.settingsSaved'), '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_from_email) {
+      showToast(t('settings.email.errors.requiredFields'), 'error');
+      return;
+    }
+    // Validate auth fields when authentication is enabled
+    if (smtpSettings.smtp_auth_enabled && (!smtpSettings.smtp_username)) {
+      showToast(t('settings.email.errors.usernameRequired'), 'error');
+      return;
+    }
+    saveMutation.mutate(smtpSettings);
+  };
+
+  const handleTest = () => {
+    if (!testEmail) {
+      showToast(t('settings.email.errors.enterTestEmail'), 'error');
+      return;
+    }
+    if (!smtpSettings.smtp_host || !smtpSettings.smtp_from_email) {
+      showToast(t('settings.email.errors.smtpServerAndEmail'), 'error');
+      return;
+    }
+    // Validate auth fields when authentication is enabled
+    if (smtpSettings.smtp_auth_enabled && (!smtpSettings.smtp_username || !smtpSettings.smtp_password)) {
+      showToast(t('settings.email.errors.usernamePasswordRequired'), '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_security: smtpSettings.smtp_security,
+      smtp_auth_enabled: smtpSettings.smtp_auth_enabled,
+      smtp_from_email: smtpSettings.smtp_from_email,
+      test_recipient: testEmail,
+    });
+  };
+
+  const handleToggleAdvancedAuth = () => {
+    if (!advancedAuthStatus?.advanced_auth_enabled && !advancedAuthStatus?.smtp_configured) {
+      showToast(t('settings.email.errors.configureSmtpFirst'), '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">
+      {/* 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') || 'SMTP Port'}
+                </label>
+                <input
+                  type="number"
+                  value={smtpSettings.smtp_port}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_port: parseInt(e.target.value) || 587 })}
+                  placeholder="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 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.security') || 'Security'}
+                </label>
+                <select
+                  value={smtpSettings.smtp_security}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_security: e.target.value as 'starttls' | 'ssl' | 'none' })}
+                  className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
+                >
+                  <option value="starttls">{t('settings.email.securityOptions.starttls')}</option>
+                  <option value="ssl">{t('settings.email.securityOptions.ssl')}</option>
+                  <option value="none">{t('settings.email.securityOptions.none')}</option>
+                </select>
+              </div>
+              <div>
+                <label className="block text-sm font-medium text-white mb-2">
+                  {t('settings.email.authentication') || 'Authentication'}
+                </label>
+                <select
+                  value={smtpSettings.smtp_auth_enabled ? 'true' : 'false'}
+                  onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_auth_enabled: e.target.value === 'true' })}
+                  className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
+                >
+                  <option value="true">{t('settings.email.authOptions.enabled')}</option>
+                  <option value="false">{t('settings.email.authOptions.disabled')}</option>
+                </select>
+              </div>
+            </div>
+
+            {smtpSettings.smtp_auth_enabled && (
+              <>
+                <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 ? '••••••••' : 'App 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="your@email.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 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>
+
+      {/* Advanced Authentication Toggle - Only show when SMTP is configured */}
+      {advancedAuthStatus?.smtp_configured && (
+        <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') || 'Enable advanced authentication to activate email-based features for user management.'}
+                      </p>
+                    </div>
+                  </div>
+                </div>
+              )}
+            </div>
+          </CardContent>
+        </Card>
+      )}
+    </div>
+  );
+}

+ 5 - 3
frontend/src/components/ExternalLinksSettings.tsx

@@ -1,6 +1,7 @@
 import { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { ExternalLink } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
@@ -10,6 +11,7 @@ import { ConfirmModal } from './ConfirmModal';
 import { getIconByName } from './IconPicker';
 
 export function ExternalLinksSettings() {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const [showAddModal, setShowAddModal] = useState(false);
   const [editingLink, setEditingLink] = useState<ExternalLink | null>(null);
@@ -132,7 +134,7 @@ export function ExternalLinksSettings() {
                       <button
                         onClick={() => setEditingLink(link)}
                         className="p-2 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
-                        title="Edit"
+                        title={t('common.edit')}
                       >
                         <Pencil className="w-4 h-4" />
                       </button>
@@ -140,7 +142,7 @@ export function ExternalLinksSettings() {
                         onClick={() => handleDelete(link)}
                         disabled={deleteMutation.isPending}
                         className="p-2 rounded-lg hover:bg-red-500/20 text-bambu-gray hover:text-red-400 transition-colors disabled:opacity-50"
-                        title="Delete"
+                        title={t('externalLinks.deleteLink')}
                       >
                         <Trash2 className="w-4 h-4" />
                       </button>
@@ -152,7 +154,7 @@ export function ExternalLinksSettings() {
           ) : (
             <div className="text-center py-8 text-bambu-gray">
               <Link2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
-              <p>No external links configured</p>
+              <p>{t('externalLinks.noLinksConfigured')}</p>
               <p className="text-sm">Click "Add Link" to add one</p>
             </div>
           )}

+ 12 - 10
frontend/src/components/RichTextEditor.tsx

@@ -18,6 +18,7 @@ import {
   Link as LinkIcon,
   Unlink,
 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 
 interface RichTextEditorProps {
   content: string;
@@ -26,6 +27,7 @@ interface RichTextEditorProps {
 }
 
 export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
+  const { t } = useTranslation();
   const editor = useEditor({
     extensions: [
       StarterKit.configure({
@@ -105,21 +107,21 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleBold().run()}
           isActive={editor.isActive('bold')}
-          title="Bold"
+          title={t('richTextEditor.bold')}
         >
           <Bold className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleItalic().run()}
           isActive={editor.isActive('italic')}
-          title="Italic"
+          title={t('richTextEditor.italic')}
         >
           <Italic className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleUnderline().run()}
           isActive={editor.isActive('underline')}
-          title="Underline"
+          title={t('richTextEditor.underline')}
         >
           <UnderlineIcon className="w-4 h-4" />
         </ToolbarButton>
@@ -129,14 +131,14 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleBulletList().run()}
           isActive={editor.isActive('bulletList')}
-          title="Bullet List"
+          title={t('richTextEditor.bulletList')}
         >
           <List className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleOrderedList().run()}
           isActive={editor.isActive('orderedList')}
-          title="Numbered List"
+          title={t('richTextEditor.numberedList')}
         >
           <ListOrdered className="w-4 h-4" />
         </ToolbarButton>
@@ -146,21 +148,21 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('left').run()}
           isActive={editor.isActive({ textAlign: 'left' })}
-          title="Align Left"
+          title={t('richTextEditor.alignLeft')}
         >
           <AlignLeft className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('center').run()}
           isActive={editor.isActive({ textAlign: 'center' })}
-          title="Align Center"
+          title={t('richTextEditor.alignCenter')}
         >
           <AlignCenter className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('right').run()}
           isActive={editor.isActive({ textAlign: 'right' })}
-          title="Align Right"
+          title={t('richTextEditor.alignRight')}
         >
           <AlignRight className="w-4 h-4" />
         </ToolbarButton>
@@ -170,14 +172,14 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={setLink}
           isActive={editor.isActive('link')}
-          title="Add Link"
+          title={t('richTextEditor.addLink')}
         >
           <LinkIcon className="w-4 h-4" />
         </ToolbarButton>
         {editor.isActive('link') && (
           <ToolbarButton
             onClick={() => editor.chain().focus().unsetLink().run()}
-            title="Remove Link"
+            title={t('richTextEditor.removeLink')}
           >
             <Unlink className="w-4 h-4" />
           </ToolbarButton>

+ 10 - 8
frontend/src/components/SmartPlugCard.tsx

@@ -1,6 +1,7 @@
 import { useState } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { SmartPlug, SmartPlugUpdate } from '../api/client';
 import { Card, CardContent } from './Card';
@@ -14,6 +15,7 @@ interface SmartPlugCardProps {
 }
 
 export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -171,7 +173,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
               ) : (
                 <div className="flex items-center gap-1 text-sm text-status-error">
                   <WifiOff className="w-4 h-4" />
-                  <span>Offline</span>
+                  <span>{t('smartPlugs.offline')}</span>
                 </div>
               )}
               {/* Admin page link - only for Tasmota */}
@@ -181,10 +183,10 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   target="_blank"
                   rel="noopener noreferrer"
                   className="flex items-center gap-1 px-2 py-0.5 bg-bambu-dark hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white text-xs rounded-full transition-colors"
-                  title="Open plug admin page"
+                  title={t('smartPlugs.openPlugAdminPage')}
                 >
                   <ExternalLink className="w-3 h-3" />
-                  Admin
+                  {t('smartPlugs.admin')}
                 </a>
               )}
             </div>
@@ -449,7 +451,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Delete Confirmation */}
       {showDeleteConfirm && (
         <ConfirmModal
-          title="Delete Smart Plug"
+          title={t('smartPlugs.deleteSmartPlug')}
           message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
           confirmText="Delete"
           variant="danger"
@@ -464,9 +466,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power On Confirmation */}
       {showPowerOnConfirm && (
         <ConfirmModal
-          title="Turn On Smart Plug"
+          title={t('smartPlugs.turnOnSmartPlug')}
           message={`Are you sure you want to turn on "${plug.name}"?`}
-          confirmText="Turn On"
+          confirmText={t('smartPlugs.turnOn')}
           variant="default"
           onConfirm={() => {
             controlMutation.mutate('on');
@@ -479,9 +481,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power Off Confirmation */}
       {showPowerOffConfirm && (
         <ConfirmModal
-          title="Turn Off Smart Plug"
+          title={t('smartPlugs.turnOffSmartPlug')}
           message={`Are you sure you want to turn off "${plug.name}"? This will cut power to the connected device.`}
-          confirmText="Turn Off"
+          confirmText={t('smartPlugs.turnOff')}
           variant="danger"
           onConfirm={() => {
             controlMutation.mutate('off');

+ 417 - 0
frontend/src/i18n/locales/de.ts

@@ -1021,6 +1021,60 @@ export default {
       virtualPrinter: 'Virtueller Drucker',
       users: 'Benutzer',
       backup: 'Sicherung',
+      globalEmail: 'Globale E-Mail',
+    },
+    // Email settings
+    email: {
+      smtpSettings: 'SMTP-Konfiguration',
+      smtpHost: 'SMTP-Server',
+      smtpPort: 'SMTP-Port',
+      security: 'Sicherheit',
+      authentication: 'Authentifizierung',
+      username: 'Benutzername',
+      password: 'Passwort',
+      fromEmail: 'Absender-E-Mail',
+      fromName: 'Absendername',
+      testConnection: 'SMTP-Verbindung testen',
+      testRecipient: 'Test-Empfänger-E-Mail',
+      sendTest: 'Test-E-Mail senden',
+      sending: 'Wird gesendet...',
+      save: 'Einstellungen speichern',
+      saving: 'Wird gespeichert...',
+      advancedAuth: 'Erweiterte Authentifizierung',
+      advancedAuthEnabled: 'Erweiterte Authentifizierung ist aktiviert',
+      advancedAuthEnabledDesc: 'E-Mail-basierte Benutzerverwaltungsfunktionen sind aktiv. Neue Benutzer erhalten automatisch generierte Passwörter per E-Mail und können ihr Passwort über die Passwort vergessen Funktion zurücksetzen.',
+      advancedAuthDisabled: 'Erweiterte Authentifizierung ist deaktiviert',
+      advancedAuthDisabledDesc: 'Aktivieren Sie die erweiterte Authentifizierung, um E-Mail-basierte Funktionen für die Benutzerverwaltung zu aktivieren.',
+      enable: 'Aktivieren',
+      disable: 'Deaktivieren',
+      feature1: 'Passwörter werden automatisch generiert und an neue Benutzer gesendet',
+      feature2: 'Benutzer können sich mit Benutzername oder E-Mail anmelden',
+      feature3: 'Passwort vergessen Funktion ist verfügbar',
+      feature4: 'Administratoren können Benutzerpasswörter per E-Mail zurücksetzen',
+      // Error messages
+      errors: {
+        requiredFields: 'Bitte füllen Sie alle Pflichtfelder aus',
+        usernameRequired: 'Benutzername ist erforderlich, wenn Authentifizierung aktiviert ist',
+        enterTestEmail: 'Bitte geben Sie eine Test-E-Mail-Adresse ein',
+        smtpServerAndEmail: 'Bitte füllen Sie SMTP-Server und Absender-E-Mail aus, bevor Sie testen',
+        usernamePasswordRequired: 'Benutzername und Passwort sind erforderlich, wenn Authentifizierung aktiviert ist',
+        configureSmtpFirst: 'Bitte konfigurieren und testen Sie zuerst die SMTP-Einstellungen',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'SMTP-Einstellungen erfolgreich gespeichert',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (Port 587)',
+        ssl: 'SSL/TLS (Port 465)',
+        none: 'Keine (Port 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: 'Aktiviert',
+        disabled: 'Deaktiviert',
+      },
     },
     appearance: 'Erscheinungsbild',
     notifications: 'Benachrichtigungen',
@@ -1439,6 +1493,8 @@ export default {
     subtitle: 'Melden Sie sich bei Ihrem Konto an',
     username: 'Benutzername',
     usernamePlaceholder: 'Benutzername eingeben',
+    usernameOrEmail: 'Benutzername oder E-Mail',
+    usernameOrEmailPlaceholder: 'Benutzername oder @ E-Mail',
     password: 'Passwort',
     passwordPlaceholder: 'Passwort eingeben',
     signIn: 'Anmelden',
@@ -1449,6 +1505,12 @@ export default {
     enterCredentials: 'Bitte Benutzername und Passwort eingeben',
     forgotPasswordTitle: 'Passwort vergessen',
     forgotPasswordMessage: 'Wenn Sie Ihr Passwort vergessen haben, wenden Sie sich bitte an Ihren Systemadministrator.',
+    forgotPasswordEmailMessage: 'Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen ein neues Passwort.',
+    emailAddress: 'E-Mail-Adresse',
+    emailPlaceholder: 'ihre.email@beispiel.de',
+    cancel: 'Abbrechen',
+    sending: 'Wird gesendet...',
+    sendResetEmail: 'Zurücksetzungs-E-Mail senden',
     howToReset: 'So setzen Sie Ihr Passwort zurück:',
     resetStep1: 'Kontaktieren Sie Ihren Bambuddy-Administrator',
     resetStep2: 'Bitten Sie ihn, Ihr Passwort in der Benutzerverwaltung zurückzusetzen',
@@ -1614,10 +1676,13 @@ export default {
       creating: 'Erstellen...',
       saving: 'Speichern...',
       saveChanges: 'Änderungen speichern',
+      advancedAuthSubtitle: 'mit erweiterter Authentifizierung',
     },
     form: {
       username: 'Benutzername',
       usernamePlaceholder: 'Benutzernamen eingeben',
+      email: 'E-Mail',
+      emailPlaceholder: 'benutzer@beispiel.de',
       password: 'Passwort',
       passwordPlaceholder: 'Passwort eingeben',
       confirmPassword: 'Passwort bestätigen',
@@ -1626,6 +1691,11 @@ export default {
       confirmNewPasswordPlaceholder: 'Neues Passwort bestätigen',
       leaveBlankToKeep: 'leer lassen, um das aktuelle zu behalten',
       groups: 'Gruppen',
+      optional: 'optional',
+      autoGeneratedPassword: 'Ein sicheres Passwort wird automatisch generiert und per E-Mail an den Benutzer gesendet.',
+      passwordManagedByAdvancedAuth: 'Das Passwort wird durch erweiterte Authentifizierung verwaltet. Verwenden Sie "Passwort zurücksetzen", um ein neues Passwort per E-Mail an den Benutzer zu senden.',
+      resetPassword: 'Passwort zurücksetzen',
+      resettingPassword: 'Passwort wird zurückgesetzt...',
     },
     deleteModal: {
       title: 'Benutzer löschen',
@@ -2310,6 +2380,9 @@ export default {
     noPrintersAvailable: 'Keine Drucker verfügbar',
     printerBusy: 'Drucker ist beschäftigt',
     printerOffline: 'Drucker ist offline',
+    sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
+    filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
+    openCalendar: 'Kalender öffnen',
   },
 
   // Backup
@@ -2664,4 +2737,348 @@ export default {
     replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
     lubricateLeftNozzleRail: 'Linke Düsenschiene schmieren (H2-Serie)',
   },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'Offline',
+    admin: 'Admin',
+    openPlugAdminPage: 'Plug-Admin-Seite öffnen',
+    deleteSmartPlug: 'Smart Plug löschen',
+    turnOnSmartPlug: 'Smart Plug einschalten',
+    turnOffSmartPlug: 'Smart Plug ausschalten',
+    turnOn: 'Einschalten',
+    turnOff: 'Ausschalten',
+    addSmartPlug: {
+      scanningNetwork: 'Netzwerk wird durchsucht...',
+      chooseEntity: 'Entität auswählen...',
+      connectionFailed: 'Verbindung fehlgeschlagen',
+      searchEntities: 'Entitäten suchen...',
+      searchPowerSensors: 'Leistungssensoren suchen...',
+      searchEnergySensors: 'Energiesensoren suchen...',
+      placeholders: {
+        plugName: 'Wohnzimmer Steckdose',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: 'Gleich wie Leistungs-Topic oder anders',
+      },
+    },
+  },
+
+  // Rich Text Editor
+  richTextEditor: {
+    bold: 'Fett',
+    italic: 'Kursiv',
+    underline: 'Unterstrichen',
+    bulletList: 'Aufzählungsliste',
+    numberedList: 'Nummerierte Liste',
+    alignLeft: 'Linksbündig',
+    alignCenter: 'Zentriert',
+    alignRight: 'Rechtsbündig',
+    addLink: 'Link hinzufügen',
+    removeLink: 'Link entfernen',
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: 'Keine externen Links konfiguriert',
+    deleteLink: 'Link löschen',
+    removeCustomIcon: 'Benutzerdefiniertes Symbol entfernen',
+    placeholders: {
+      linkName: 'Mein Link',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'Tastaturkürzel',
+    navigation: 'Navigation',
+    archivesSection: 'Archive',
+    kProfilesSection: 'K-Profile',
+    generalSection: 'Allgemein',
+    shortcuts: {
+      goToPrinters: 'Zu Drucker gehen',
+      goToArchives: 'Zu Archiv gehen',
+      goToQueue: 'Zur Warteschlange gehen',
+      goToStats: 'Zu Statistiken gehen',
+      goToProfiles: 'Zu Cloud-Profilen gehen',
+      goToSettings: 'Zu Einstellungen gehen',
+      focusSearch: 'Suche fokussieren',
+      openUploadModal: 'Upload-Modal öffnen',
+      clearSelection: 'Auswahl löschen / Eingabe aufheben',
+      contextMenu: 'Kontextmenü auf Karten',
+      refreshProfiles: 'Profile aktualisieren',
+      newProfile: 'Neues Profil',
+      exitSelectionMode: 'Auswahlmodus beenden',
+      showHelp: 'Diese Hilfe anzeigen',
+    },
+    footer: 'Drücken Sie Esc oder klicken Sie außerhalb, um zu schließen',
+  },
+
+  // Notification Log
+  notificationLog: {
+    title: 'Benachrichtigungsprotokoll',
+    events: {
+      printStarted: 'Druck gestartet',
+      printComplete: 'Druck abgeschlossen',
+      printFailed: 'Druck fehlgeschlagen',
+      printStopped: 'Druck gestoppt',
+      progress: 'Fortschritt',
+      printerOffline: 'Drucker offline',
+      printerError: 'Druckerfehler',
+      lowFilament: 'Wenig Filament',
+      maintenanceDue: 'Wartung fällig',
+      test: 'Test',
+    },
+    timeAgo: {
+      justNow: 'Gerade eben',
+      minutesAgo: 'vor {{minutes}}m',
+      hoursAgo: 'vor {{hours}}h',
+    },
+  },
+
+  // Restore/Backup Modal
+  restoreBackup: {
+    title: 'Backup wiederherstellen',
+    restoring: 'Wird wiederhergestellt...',
+    restoreComplete: 'Wiederherstellung abgeschlossen',
+    restoreFailed: 'Wiederherstellung fehlgeschlagen',
+    importSettings: 'Einstellungen aus Backup-Datei importieren',
+    pleaseWait: 'Bitte warten Sie, während Ihre Daten wiederhergestellt werden',
+    clickToSelect: 'Klicken Sie, um Backup-Datei auszuwählen (.json oder .zip)',
+    howDuplicateHandling: 'So funktioniert die Duplikatbehandlung:',
+    categories: {
+      printers: 'Drucker',
+      smartPlugs: 'Smart Plugs',
+      notificationProviders: 'Benachrichtigungsanbieter',
+      filaments: 'Filamente',
+      archives: 'Archive',
+      pendingUploads: 'Ausstehende Uploads',
+      settingsTemplates: 'Einstellungen & Vorlagen',
+    },
+    matchingInfo: {
+      printers: 'abgeglichen nach Seriennummer',
+      smartPlugs: 'abgeglichen nach IP-Adresse',
+      notificationProviders: 'abgeglichen nach Name',
+      filaments: 'abgeglichen nach Name + Typ + Marke',
+      archives: 'abgeglichen nach Inhalts-Hash',
+      pendingUploads: 'abgeglichen nach Dateiname',
+      settingsTemplates: 'immer überschrieben',
+    },
+    replaceExisting: 'Vorhandene Daten ersetzen',
+    keepExisting: 'Vorhandene Daten behalten',
+    replaceDescription: 'Bereits vorhandene Elemente mit Backup-Daten überschreiben',
+    keepDescription: 'Nur Elemente wiederherstellen, die noch nicht existieren',
+    caution: 'Vorsicht:',
+    cautionText: 'Das Überschreiben ersetzt Ihre aktuellen Konfigurationen durch Backup-Daten. Drucker-Zugangscodes werden aus Sicherheitsgründen niemals überschrieben.',
+    itemsRestored: 'Wiederhergestellte Elemente',
+    itemsSkipped: 'Übersprungene Elemente',
+    restored: 'Wiederhergestellt',
+    skipped: 'Übersprungen (existieren bereits)',
+    filesLabel: 'Dateien (3MF, Thumbnails, etc.)',
+    newApiKeysGenerated: 'Neue API-Schlüssel generiert',
+    newApiKeysWarning: 'Diese Schlüssel werden nur einmal angezeigt. Kopieren Sie sie jetzt!',
+    processingBackup: 'Backup-Datei wird verarbeitet...',
+    noDataFound: 'In der Backup-Datei wurden keine wiederherzustellenden Daten gefunden.',
+    failedToRestore: 'Backup konnte nicht wiederhergestellt werden. Bitte überprüfen Sie das Dateiformat.',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'Backup exportieren',
+    selectData: 'Zu exportierende Daten auswählen',
+    selectAll: 'Alle auswählen',
+    selectNone: 'Keine auswählen',
+    categoryDescriptions: {
+      settings: 'Sprache, Theme, Update-Einstellungen',
+      notifications: 'ntfy, Pushover, Discord, usw.',
+      templates: 'Benutzerdefinierte Nachrichtenvorlagen',
+      smartPlugs: 'Tasmota-Plug-Konfigurationen',
+      externalLinks: 'Seitenleiste Links zu externen Diensten',
+      printers: 'Druckerinformationen (Zugangscodes ausgeschlossen)',
+      plateDetection: 'Leere Platten-Referenzbilder',
+      filaments: 'Filamenttypen und -kosten',
+      maintenance: 'Benutzerdefinierte Wartungspläne',
+      archives: 'Alle Druckdaten + Dateien (3MF, Thumbnails, Fotos)',
+      projects: 'Projekte, BOM-Elemente und Anhänge',
+      pendingUploads: 'Virtueller Drucker-Uploads zur Überprüfung',
+      apiKeys: 'Webhook-API-Schlüssel (neue Schlüssel bei Import generiert)',
+    },
+    requiresPrinters: 'Drucker müssen ausgewählt sein',
+    zipFileWarning: 'ZIP-Datei wird erstellt.',
+    zipFileDescription: 'Enthält alle 3MF-Dateien, Thumbnails, Zeitraffer und Fotos. Dies kann eine Weile dauern und zu einer großen Datei führen.',
+    includeAccessCodes: 'Zugangscodes einschließen',
+    includeAccessCodesDescription: 'Für die Übertragung auf eine andere Maschine',
+    includeAccessCodesWarning: 'Zugangscodes werden im Klartext eingeschlossen. Bewahren Sie diese Backup-Datei sicher auf!',
+    categoriesSelected: '{{selectedCount}} Kategorien ausgewählt',
+  },
+
+  // Pending Uploads Panel
+  pendingUploads: {
+    placeholders: {
+      notes: 'Notizen zu diesem Druck hinzufügen...',
+    },
+    discardUpload: 'Upload verwerfen',
+    archiveAllUploads: 'Alle Uploads archivieren',
+    discardAllUploads: 'Alle Uploads verwerfen',
+    archive: 'Archivieren',
+    timeAgo: {
+      justNow: 'Gerade eben',
+      minutesAgo: 'vor {{minutes}}m',
+      hoursAgo: 'vor {{hours}}h',
+      daysAgo: 'vor {{days}}d',
+    },
+  },
+
+  // API Browser
+  apiBrowser: {
+    placeholders: {
+      requestBody: 'JSON-Anforderungstext...',
+      searchEndpoints: 'Endpunkte suchen...',
+    },
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'Voreinstellungen suchen...',
+    colorPlaceholder: 'Farbname oder Hex (z.B. braun, FF8800)',
+    clearCustomColor: 'Benutzerdefinierte Farbe löschen',
+    noCloudPresets: 'Keine Cloud-Voreinstellungen. Melden Sie sich bei Bambu Cloud an, um zu synchronisieren.',
+    noMatchingPresets: 'Keine passenden Voreinstellungen gefunden.',
+    custom: 'Benutzerdefiniert',
+    settingsSentToPrinter: 'Einstellungen an Drucker gesendet',
+    filamentProfile: 'Filamentprofil',
+  },
+
+  // GitHub Backup Settings
+  githubBackup: {
+    title: 'GitHub-Backup',
+    history: 'Verlauf',
+    downloadBackup: 'Backup herunterladen',
+    restoreBackup: 'Backup wiederherstellen',
+    noBackupsYet: 'Noch keine Backups',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'Tags suchen...',
+    renameTag: 'Tag umbenennen',
+    deleteTag: 'Tag löschen',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: 'Benachrichtigungstitel...',
+      body: 'Benachrichtigungstext...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: 'Neuen Tag eingeben...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: 'Foto löschen',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'Spulen-UUID kopieren',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'Hat Notiz',
+    copyProfile: 'Profil kopieren',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'Menü öffnen',
+    noPermissionSystemInfo: 'Sie haben keine Berechtigung zum Anzeigen von Systeminformationen',
+  },
+
+  // Dashboard
+  dashboard: {
+    dragToReorder: 'Ziehen zum Neuordnen',
+    hideWidget: 'Widget ausblenden',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: 'Benachrichtigungsanbieter löschen',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'Dateimanager schließen',
+    sortFiles: 'Dateien sortieren',
+    goToParentFolder: 'Zum übergeordneten Ordner gehen',
+    threeView: '3D-Ansicht',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'Stream aktualisieren',
+    close: 'Schließen',
+    zoomOut: 'Verkleinern',
+    resetZoom: 'Zoom zurücksetzen',
+    zoomIn: 'Vergrößern',
+    dragToResize: 'Ziehen zum Größe ändern',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: '5s zurückspringen',
+    skipForward5s: '5s vorspringen',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      email: 'SMTP-E-Mail-Benachrichtigungen',
+      telegram: 'Benachrichtigungen über Telegram-Bot',
+      discord: 'An Discord-Kanal über Webhook senden',
+      ntfy: 'Kostenlose, selbst hostbare Push-Benachrichtigungen',
+      pushover: 'Einfache, zuverlässige Push-Benachrichtigungen',
+      callmebot: 'Kostenlose WhatsApp-Benachrichtigungen über CallMeBot',
+      webhook: 'Generischer HTTP POST zu beliebiger URL',
+    },
+  },
+
+  // Log Viewer
+  logViewer: {
+    searchPlaceholder: 'Nachricht oder Logger-Name suchen...',
+    noLogEntries: 'Keine Logeinträge gefunden',
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'Keine Schalter in Schalterleiste',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'Titel',
+      designer: 'Designer',
+      license: 'Lizenz',
+      description: 'Beschreibung eingeben...',
+      profileTitle: 'Profil-Titel',
+      profileDescription: 'Profilbeschreibung...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

+ 418 - 0
frontend/src/i18n/locales/en.ts

@@ -1021,6 +1021,60 @@ export default {
       virtualPrinter: 'Virtual Printer',
       users: 'Users',
       backup: 'Backup',
+      globalEmail: 'Global Email',
+    },
+    // Email settings
+    email: {
+      smtpSettings: 'SMTP Configuration',
+      smtpHost: 'SMTP Server',
+      smtpPort: 'SMTP Port',
+      security: 'Security',
+      authentication: 'Authentication',
+      username: 'Username',
+      password: 'Password',
+      fromEmail: 'From Email',
+      fromName: 'From Name',
+      testConnection: 'Test SMTP Connection',
+      testRecipient: 'Test Recipient Email',
+      sendTest: 'Send Test Email',
+      sending: 'Sending...',
+      save: 'Save Settings',
+      saving: 'Saving...',
+      advancedAuth: 'Advanced Authentication',
+      advancedAuthEnabled: 'Advanced Authentication is enabled',
+      advancedAuthEnabledDesc: 'Email-based user management features are active. New users will receive auto-generated passwords via email, and users can reset their passwords through the forgot password feature.',
+      advancedAuthDisabled: 'Advanced Authentication is disabled',
+      advancedAuthDisabledDesc: 'Enable advanced authentication to activate email-based features for user management.',
+      enable: 'Enable',
+      disable: 'Disable',
+      feature1: 'Passwords are auto-generated and emailed to new users',
+      feature2: 'Users can login with username or email',
+      feature3: 'Forgot password feature is available',
+      feature4: 'Admins can reset user passwords via email',
+      // Error messages
+      errors: {
+        requiredFields: 'Please fill in all required fields',
+        usernameRequired: 'Username is required when authentication is enabled',
+        enterTestEmail: 'Please enter a test email address',
+        smtpServerAndEmail: 'Please fill in SMTP Server and From Email before testing',
+        usernamePasswordRequired: 'Username and Password are required when authentication is enabled',
+        configureSmtpFirst: 'Please configure and test SMTP settings first',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'SMTP settings saved successfully',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (Port 587)',
+        ssl: 'SSL/TLS (Port 465)',
+        none: 'None (Port 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: 'Enabled',
+        disabled: 'Disabled',
+      },
     },
     appearance: 'Appearance',
     notifications: 'Notifications',
@@ -1439,6 +1493,8 @@ export default {
     subtitle: 'Sign in to your account',
     username: 'Username',
     usernamePlaceholder: 'Enter your username',
+    usernameOrEmail: 'Username or Email',
+    usernameOrEmailPlaceholder: 'Username or @ Email',
     password: 'Password',
     passwordPlaceholder: 'Enter your password',
     signIn: 'Sign in',
@@ -1449,6 +1505,12 @@ export default {
     enterCredentials: 'Please enter username and password',
     forgotPasswordTitle: 'Forgot Password',
     forgotPasswordMessage: "If you've forgotten your password, please contact your system administrator to reset it.",
+    forgotPasswordEmailMessage: "Enter your email address and we'll send you a new password.",
+    emailAddress: 'Email Address',
+    emailPlaceholder: 'your.email@example.com',
+    cancel: 'Cancel',
+    sending: 'Sending...',
+    sendResetEmail: 'Send Reset Email',
     howToReset: 'How to reset your password:',
     resetStep1: 'Contact your Bambuddy administrator',
     resetStep2: 'Ask them to reset your password in User Management',
@@ -1614,10 +1676,13 @@ export default {
       creating: 'Creating...',
       saving: 'Saving...',
       saveChanges: 'Save Changes',
+      advancedAuthSubtitle: 'with Advanced Authentication',
     },
     form: {
       username: 'Username',
       usernamePlaceholder: 'Enter username',
+      email: 'Email',
+      emailPlaceholder: 'user@example.com',
       password: 'Password',
       passwordPlaceholder: 'Enter password',
       confirmPassword: 'Confirm Password',
@@ -1626,6 +1691,11 @@ export default {
       confirmNewPasswordPlaceholder: 'Confirm new password',
       leaveBlankToKeep: 'leave blank to keep current',
       groups: 'Groups',
+      optional: 'optional',
+      autoGeneratedPassword: 'A secure password will be automatically generated and emailed to the user.',
+      passwordManagedByAdvancedAuth: 'Password is managed by Advanced Authentication. Use "Reset Password" to send a new password to the user via email.',
+      resetPassword: 'Reset Password',
+      resettingPassword: 'Resetting Password...',
     },
     deleteModal: {
       title: 'Delete User',
@@ -2310,6 +2380,9 @@ export default {
     noPrintersAvailable: 'No printers available',
     printerBusy: 'Printer is busy',
     printerOffline: 'Printer is offline',
+    sameTypeDifferentColor: 'Same type, different color',
+    filamentTypeNotLoaded: 'Filament type not loaded',
+    openCalendar: 'Open calendar',
   },
 
   // Backup
@@ -2362,6 +2435,7 @@ export default {
   },
 
   // Edit archive modal
+  // Edit Archive Modal
   editArchive: {
     title: 'Edit Archive',
     name: 'Name',
@@ -2664,4 +2738,348 @@ export default {
     replaceCarbonFilter: 'Replace activated carbon filter',
     lubricateLeftNozzleRail: 'Lubricate left nozzle rail (H2 series)',
   },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'Offline',
+    admin: 'Admin',
+    openPlugAdminPage: 'Open plug admin page',
+    deleteSmartPlug: 'Delete Smart Plug',
+    turnOnSmartPlug: 'Turn On Smart Plug',
+    turnOffSmartPlug: 'Turn Off Smart Plug',
+    turnOn: 'Turn On',
+    turnOff: 'Turn Off',
+    addSmartPlug: {
+      scanningNetwork: 'Scanning network...',
+      chooseEntity: 'Choose an entity...',
+      connectionFailed: 'Connection failed',
+      searchEntities: 'Search entities...',
+      searchPowerSensors: 'Search power sensors...',
+      searchEnergySensors: 'Search energy sensors...',
+      placeholders: {
+        plugName: 'Living Room Plug',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: 'Same as power topic, or different',
+      },
+    },
+  },
+
+  // Rich Text Editor
+  richTextEditor: {
+    bold: 'Bold',
+    italic: 'Italic',
+    underline: 'Underline',
+    bulletList: 'Bullet List',
+    numberedList: 'Numbered List',
+    alignLeft: 'Align Left',
+    alignCenter: 'Align Center',
+    alignRight: 'Align Right',
+    addLink: 'Add Link',
+    removeLink: 'Remove Link',
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: 'No external links configured',
+    deleteLink: 'Delete Link',
+    removeCustomIcon: 'Remove custom icon',
+    placeholders: {
+      linkName: 'My Link',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'Keyboard Shortcuts',
+    navigation: 'Navigation',
+    archivesSection: 'Archives',
+    kProfilesSection: 'K-Profiles',
+    generalSection: 'General',
+    shortcuts: {
+      goToPrinters: 'Go to Printers',
+      goToArchives: 'Go to Archives',
+      goToQueue: 'Go to Queue',
+      goToStats: 'Go to Statistics',
+      goToProfiles: 'Go to Cloud Profiles',
+      goToSettings: 'Go to Settings',
+      focusSearch: 'Focus search',
+      openUploadModal: 'Open upload modal',
+      clearSelection: 'Clear selection / blur input',
+      contextMenu: 'Context menu on cards',
+      refreshProfiles: 'Refresh profiles',
+      newProfile: 'New profile',
+      exitSelectionMode: 'Exit selection mode',
+      showHelp: 'Show this help',
+    },
+    footer: 'Press Esc or click outside to close',
+  },
+
+  // Notification Log
+  notificationLog: {
+    title: 'Notification Log',
+    events: {
+      printStarted: 'Print Started',
+      printComplete: 'Print Complete',
+      printFailed: 'Print Failed',
+      printStopped: 'Print Stopped',
+      progress: 'Progress',
+      printerOffline: 'Printer Offline',
+      printerError: 'Printer Error',
+      lowFilament: 'Low Filament',
+      maintenanceDue: 'Maintenance Due',
+      test: 'Test',
+    },
+    timeAgo: {
+      justNow: 'Just now',
+      minutesAgo: '{{minutes}}m ago',
+      hoursAgo: '{{hours}}h ago',
+    },
+  },
+
+  // Restore/Backup Modal
+  restoreBackup: {
+    title: 'Restore Backup',
+    restoring: 'Restoring...',
+    restoreComplete: 'Restore Complete',
+    restoreFailed: 'Restore Failed',
+    importSettings: 'Import settings from a backup file',
+    pleaseWait: 'Please wait while your data is being restored',
+    clickToSelect: 'Click to select backup file (.json or .zip)',
+    howDuplicateHandling: 'How duplicate handling works:',
+    categories: {
+      printers: 'Printers',
+      smartPlugs: 'Smart Plugs',
+      notificationProviders: 'Notification Providers',
+      filaments: 'Filaments',
+      archives: 'Archives',
+      pendingUploads: 'Pending Uploads',
+      settingsTemplates: 'Settings & Templates',
+    },
+    matchingInfo: {
+      printers: 'matched by serial number',
+      smartPlugs: 'matched by IP address',
+      notificationProviders: 'matched by name',
+      filaments: 'matched by name + type + brand',
+      archives: 'matched by content hash',
+      pendingUploads: 'matched by filename',
+      settingsTemplates: 'always overwritten',
+    },
+    replaceExisting: 'Replace existing data',
+    keepExisting: 'Keep existing data',
+    replaceDescription: 'Overwrite items that already exist with backup data',
+    keepDescription: 'Only restore items that don\'t already exist',
+    caution: 'Caution:',
+    cautionText: 'Overwriting will replace your current configurations with backup data. Printer access codes are never overwritten for security.',
+    itemsRestored: 'Items Restored',
+    itemsSkipped: 'Items Skipped',
+    restored: 'Restored',
+    skipped: 'Skipped (already exist)',
+    filesLabel: 'Files (3MF, thumbnails, etc.)',
+    newApiKeysGenerated: 'New API Keys Generated',
+    newApiKeysWarning: 'These keys are only shown once. Copy them now!',
+    processingBackup: 'Processing backup file...',
+    noDataFound: 'No data was found to restore in the backup file.',
+    failedToRestore: 'Failed to restore backup. Please check the file format.',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'Export Backup',
+    selectData: 'Select data to include',
+    selectAll: 'Select All',
+    selectNone: 'Select None',
+    categoryDescriptions: {
+      settings: 'Language, theme, update preferences',
+      notifications: 'ntfy, Pushover, Discord, etc.',
+      templates: 'Custom message templates',
+      smartPlugs: 'Tasmota plug configurations',
+      externalLinks: 'Sidebar links to external services',
+      printers: 'Printer info (access codes excluded)',
+      plateDetection: 'Empty plate reference images',
+      filaments: 'Filament types and costs',
+      maintenance: 'Custom maintenance schedules',
+      archives: 'All print data + files (3MF, thumbnails, photos)',
+      projects: 'Projects, BOM items, and attachments',
+      pendingUploads: 'Virtual printer uploads awaiting review',
+      apiKeys: 'Webhook API keys (new keys generated on import)',
+    },
+    requiresPrinters: 'Requires Printers to be selected',
+    zipFileWarning: 'ZIP file will be created.',
+    zipFileDescription: 'Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.',
+    includeAccessCodes: 'Include Access Codes',
+    includeAccessCodesDescription: 'For transferring to another machine',
+    includeAccessCodesWarning: 'Access codes will be included in plain text. Keep this backup file secure!',
+    categoriesSelected: '{{selectedCount}} categories selected',
+  },
+
+  // Pending Uploads Panel
+  pendingUploads: {
+    placeholders: {
+      notes: 'Add notes about this print...',
+    },
+    discardUpload: 'Discard Upload',
+    archiveAllUploads: 'Archive All Uploads',
+    discardAllUploads: 'Discard All Uploads',
+    archive: 'Archive',
+    timeAgo: {
+      justNow: 'Just now',
+      minutesAgo: '{{minutes}}m ago',
+      hoursAgo: '{{hours}}h ago',
+      daysAgo: '{{days}}d ago',
+    },
+  },
+
+  // API Browser
+  apiBrowser: {
+    placeholders: {
+      requestBody: 'JSON request body...',
+      searchEndpoints: 'Search endpoints...',
+    },
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'Search presets...',
+    colorPlaceholder: 'Color name or hex (e.g., brown, FF8800)',
+    clearCustomColor: 'Clear custom color',
+    noCloudPresets: 'No cloud presets. Login to Bambu Cloud to sync.',
+    noMatchingPresets: 'No matching presets found.',
+    custom: 'Custom',
+    settingsSentToPrinter: 'Settings sent to printer',
+    filamentProfile: 'Filament Profile',
+  },
+
+  // GitHub Backup Settings
+  githubBackup: {
+    title: 'GitHub Backup',
+    history: 'History',
+    downloadBackup: 'Download Backup',
+    restoreBackup: 'Restore Backup',
+    noBackupsYet: 'No backups yet',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'Search tags...',
+    renameTag: 'Rename tag',
+    deleteTag: 'Delete tag',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: 'Notification title...',
+      body: 'Notification body...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: 'Enter new tag...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: 'Delete Photo',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'Copy spool UUID',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'Has note',
+    copyProfile: 'Copy profile',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'Open menu',
+    noPermissionSystemInfo: 'You do not have permission to view system information',
+  },
+
+  // Dashboard
+  dashboard: {
+    dragToReorder: 'Drag to reorder',
+    hideWidget: 'Hide widget',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: 'Delete Notification Provider',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'Close file manager',
+    sortFiles: 'Sort files',
+    goToParentFolder: 'Go to parent folder',
+    threeView: '3D View',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'Refresh stream',
+    close: 'Close',
+    zoomOut: 'Zoom out',
+    resetZoom: 'Reset zoom',
+    zoomIn: 'Zoom in',
+    dragToResize: 'Drag to resize',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: 'Skip back 5s',
+    skipForward5s: 'Skip forward 5s',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      email: 'SMTP email notifications',
+      telegram: 'Notifications via Telegram bot',
+      discord: 'Send to Discord channel via webhook',
+      ntfy: 'Free, self-hostable push notifications',
+      pushover: 'Simple, reliable push notifications',
+      callmebot: 'Free WhatsApp notifications via CallMeBot',
+      webhook: 'Generic HTTP POST to any URL',
+    },
+  },
+
+  // Log Viewer
+  logViewer: {
+    searchPlaceholder: 'Search message or logger name...',
+    noLogEntries: 'No log entries found',
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'No switches in switchbar',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'Title',
+      designer: 'Designer',
+      license: 'License',
+      description: 'Enter description...',
+      profileTitle: 'Profile Title',
+      profileDescription: 'Profile description...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

+ 335 - 3
frontend/src/i18n/locales/ja.ts

@@ -1068,6 +1068,7 @@ export default {
       filament: 'フィラメント',
       network: 'ネットワーク',
       virtualPrinter: '仮想プリンター',
+      globalEmail: 'グローバルメール',
     },
     appearance: '外観',
     notifications: '通知',
@@ -1132,6 +1133,55 @@ export default {
       turnOn: '電源オン',
       turnOff: '電源オフ',
     },
+    // Email settings (Advanced Auth)
+    email: {
+      smtpSettings: 'SMTP設定',
+      smtpHost: 'SMTPサーバー',
+      smtpPort: 'SMTPポート',
+      security: 'セキュリティ',
+      authentication: '認証',
+      username: 'ユーザー名',
+      password: 'パスワード',
+      fromEmail: '送信元メールアドレス',
+      fromName: '送信者名',
+      testConnection: 'SMTP接続テスト',
+      testRecipient: 'テスト受信者メール',
+      sendTest: 'テストメール送信',
+      sending: '送信中...',
+      save: '設定を保存',
+      saving: '保存中...',
+      advancedAuth: '高度な認証',
+      advancedAuthEnabled: '高度な認証が有効です',
+      advancedAuthEnabledDesc: 'メールベースのユーザー管理機能が有効になっています。新規ユーザーには自動生成されたパスワードがメールで送信され、ユーザーはパスワード忘れ機能でパスワードをリセットできます。',
+      advancedAuthDisabled: '高度な認証が無効です',
+      advancedAuthDisabledDesc: '高度な認証を有効にして、ユーザー管理のメールベース機能を有効化してください。',
+      enable: '有効にする',
+      disable: '無効にする',
+      feature1: 'パスワードは自動生成され、新規ユーザーにメールで送信されます',
+      feature2: 'ユーザーはユーザー名またはメールでログインできます',
+      feature3: 'パスワード忘れ機能が利用可能です',
+      feature4: '管理者はメールでユーザーパスワードをリセットできます',
+      errors: {
+        requiredFields: 'すべての必須フィールドに入力してください',
+        usernameRequired: '認証が有効な場合、ユーザー名は必須です',
+        enterTestEmail: 'テストメールアドレスを入力してください',
+        smtpServerAndEmail: 'テストする前にSMTPサーバーと送信元メールを入力してください',
+        usernamePasswordRequired: '認証が有効な場合、ユーザー名とパスワードは必須です',
+        configureSmtpFirst: '最初にSMTP設定を構成してテストしてください',
+      },
+      success: {
+        settingsSaved: 'SMTP設定を保存しました',
+      },
+      securityOptions: {
+        starttls: 'STARTTLS (ポート 587)',
+        ssl: 'SSL/TLS (ポート 465)',
+        none: 'なし (ポート 25)',
+      },
+      authOptions: {
+        enabled: '有効',
+        disabled: '無効',
+      },
+    },
     noDefaultPrinter: 'デフォルトなし(毎回選択)',
     sidebarOrder: 'サイドバーの順序',
     saveThumbnails: 'サムネイルを保存',
@@ -1451,6 +1501,15 @@ export default {
     resetStep3: '管理者が新しい仮パスワードを設定',
     resetStep4: '新しいパスワードでログインし、設定で変更',
     gotIt: '了解',
+    // Advanced Auth (email-based login/reset)
+    usernameOrEmail: 'ユーザー名またはメール',
+    usernameOrEmailPlaceholder: 'ユーザー名または @ メール',
+    forgotPasswordEmailMessage: 'メールアドレスを入力すると、新しいパスワードを送信します。',
+    emailAddress: 'メールアドレス',
+    emailPlaceholder: 'your.email@example.com',
+    cancel: 'キャンセル',
+    sending: '送信中...',
+    sendResetEmail: 'リセットメールを送信',
   },
   setup: {
     title: 'Bambuddy セットアップ',
@@ -1591,6 +1650,7 @@ export default {
       creating: '作成中...',
       saving: '保存中...',
       saveChanges: '変更を保存',
+      advancedAuthSubtitle: '高度な認証を使用',
     },
     form: {
       username: 'ユーザー名',
@@ -1603,11 +1663,18 @@ export default {
       usernamePlaceholder: 'ユーザー名を入力',
       passwordPlaceholder: 'パスワードを入力',
       newPasswordPlaceholder: '新しいパスワードを入力',
+      email: 'メール',
+      emailPlaceholder: 'user@example.com',
+      optional: 'オプション',
+      autoGeneratedPassword: '安全なパスワードが自動的に生成され、ユーザーにメールで送信されます。',
+      passwordManagedByAdvancedAuth: 'パスワードは高度な認証によって管理されています。「パスワードのリセット」を使用して、メールで新しいパスワードをユーザーに送信してください。',
+      resetPassword: 'パスワードのリセット',
+      resettingPassword: 'パスワードをリセット中...',
     },
     deleteModal: {
-      title: 'ユーザー管理',
-      confirm: '確認',
-      message: 'このユーザーを削除しますか?この操作は元に戻せません。',
+      title: 'ユーザーを削除',
+      confirm: 'ユーザーを削除',
+      message: 'このユーザーを削除してもよろしいですか?この操作は元に戻せません。',
     },
     subtitle: 'ユーザーとBambuddyインスタンスへのアクセスを管理',
     active: 'アクティブ',
@@ -2590,4 +2657,269 @@ export default {
     collectItem11: 'データベース健全性チェック',
     collectItem12: 'Docker環境の詳細',
   },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'プリセットを検索...',
+    colorPlaceholder: '色名またはHex(例: 茶色、FF8800)',
+    clearCustomColor: 'カスタム色をクリア',
+    noCloudPresets: 'クラウドプリセットがありません。Bambu Cloudにログインして同期してください。',
+    noMatchingPresets: '一致するプリセットが見つかりません。',
+    custom: 'カスタム',
+    settingsSentToPrinter: '設定をプリンターに送信しました',
+    filamentProfile: 'フィラメントプロファイル',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Smart Plugs (accessibility)
+  smartPlugs: {
+    offline: 'オフライン',
+    admin: '管理',
+    openPlugAdminPage: 'プラグ管理ページを開く',
+    deleteSmartPlug: 'スマートプラグを削除',
+    turnOnSmartPlug: 'スマートプラグをオンにする',
+    turnOffSmartPlug: 'スマートプラグをオフにする',
+    turnOn: 'オンにする',
+    turnOff: 'オフにする',
+    addSmartPlug: {
+      scanningNetwork: 'ネットワークをスキャン中...',
+      chooseEntity: 'エンティティを選択...',
+      connectionFailed: '接続失敗',
+      searchEntities: 'エンティティを検索...',
+      searchPowerSensors: '電力センサーを検索...',
+      searchEnergySensors: 'エネルギーセンサーを検索...',
+      placeholders: {
+        plugName: 'リビングルームプラグ',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: '電力トピックと同じ、または異なる',
+      },
+    },
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: '外部リンクが設定されていません',
+    deleteLink: 'リンクを削除',
+    removeCustomIcon: 'カスタムアイコンを削除',
+    placeholders: {
+      linkName: 'マイリンク',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'キーボードショートカット',
+    navigation: 'ナビゲーション',
+    archivesSection: 'アーカイブ',
+    kProfilesSection: 'Kプロファイル',
+    generalSection: '全般',
+    shortcuts: {
+      goToPrinters: 'プリンターへ移動',
+      goToArchives: 'アーカイブへ移動',
+      goToQueue: 'キューへ移動',
+      goToStats: '統計へ移動',
+      goToProfiles: 'クラウドプロファイルへ移動',
+      goToSettings: '設定へ移動',
+      focusSearch: '検索にフォーカス',
+      openUploadModal: 'アップロードモーダルを開く',
+      clearSelection: '選択をクリア / 入力をぼかす',
+      contextMenu: 'カードのコンテキストメニュー',
+      refreshProfiles: 'プロファイルを更新',
+      newProfile: '新しいプロファイル',
+      exitSelectionMode: '選択モードを終了',
+      showHelp: 'このヘルプを表示',
+    },
+    footer: 'Escキーを押すか外側をクリックして閉じます',
+  },
+
+  // Restore Backup Modal
+  restoreBackup: {
+    title: 'バックアップを復元',
+    restoring: '復元中...',
+    restoreComplete: '復元完了',
+    restoreFailed: '復元失敗',
+    importSettings: 'バックアップファイルから設定をインポート',
+    pleaseWait: 'データの復元中です。しばらくお待ちください',
+    clickToSelect: 'クリックしてバックアップファイルを選択(.jsonまたは.zip)',
+    howDuplicateHandling: '重複の処理方法:',
+    categories: {
+      printers: 'プリンター',
+      smartPlugs: 'スマートプラグ',
+      notificationProviders: '通知プロバイダー',
+      filaments: 'フィラメント',
+      archives: 'アーカイブ',
+      pendingUploads: '保留中のアップロード',
+      settingsTemplates: '設定とテンプレート',
+    },
+    matchingInfo: {
+      printers: 'シリアル番号で照合',
+      smartPlugs: 'IPアドレスで照合',
+      notificationProviders: '名前で照合',
+      filaments: '名前+タイプ+ブランドで照合',
+      archives: 'コンテンツハッシュで照合',
+      pendingUploads: 'ファイル名で照合',
+      settingsTemplates: '常に上書き',
+    },
+    replaceExisting: '既存データを置き換え',
+    keepExisting: '既存データを保持',
+    replaceDescription: '既に存在するアイテムをバックアップデータで上書き',
+    keepDescription: 'まだ存在しないアイテムのみを復元',
+    caution: '注意:',
+    cautionText: '上書きすると現在の構成がバックアップデータに置き換えられます。セキュリティ上の理由から、プリンターのアクセスコードは上書きされません。',
+    itemsRestored: '復元されたアイテム',
+    itemsSkipped: 'スキップされたアイテム',
+    restored: '復元済み',
+    skipped: 'スキップ(既に存在)',
+    filesLabel: 'ファイル(3MF、サムネイルなど)',
+    newApiKeysGenerated: '新しいAPIキーが生成されました',
+    newApiKeysWarning: 'これらのキーは一度だけ表示されます。今すぐコピーしてください!',
+    processingBackup: 'バックアップファイルを処理中...',
+    noDataFound: 'バックアップファイルに復元するデータが見つかりませんでした。',
+    failedToRestore: 'バックアップの復元に失敗しました。ファイル形式を確認してください。',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'バックアップをエクスポート',
+    selectData: '含めるデータを選択',
+    selectAll: 'すべて選択',
+    selectNone: 'なし',
+    categoryDescriptions: {
+      settings: '言語、テーマ、更新設定',
+      notifications: 'ntfy、Pushover、Discordなど',
+      templates: 'カスタムメッセージテンプレート',
+      smartPlugs: 'Tasmotaプラグ設定',
+      externalLinks: 'サイドバーの外部サービスへのリンク',
+      printers: 'プリンター情報(アクセスコード除外)',
+      plateDetection: '空プレート参照画像',
+      filaments: 'フィラメントの種類とコスト',
+      maintenance: 'カスタムメンテナンススケジュール',
+      archives: 'すべての印刷データ+ファイル(3MF、サムネイル、写真)',
+      projects: 'プロジェクト、BOMアイテム、添付ファイル',
+      pendingUploads: '仮想プリンターアップロード待機中',
+      apiKeys: 'Webhook APIキー(インポート時に新しいキーが生成されます)',
+    },
+    requiresPrinters: 'プリンターを選択する必要があります',
+    zipFileWarning: 'ZIPファイルが作成されます。',
+    zipFileDescription: 'すべての3MFファイル、サムネイル、タイムラプス、写真が含まれます。これには時間がかかり、大きなファイルになる可能性があります。',
+    includeAccessCodes: 'アクセスコードを含める',
+    includeAccessCodesDescription: '別のマシンへの転送用',
+    includeAccessCodesWarning: 'アクセスコードはプレーンテキストで含まれます。このバックアップファイルを安全に保管してください!',
+    categoriesSelected: '{{selectedCount}}カテゴリー選択済み',
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'タグを検索...',
+    renameTag: 'タグ名を変更',
+    deleteTag: 'タグを削除',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: '通知タイトル...',
+      body: '通知本文...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: '新しいタグを入力...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: '写真を削除',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'スプールUUIDをコピー',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'メモあり',
+    copyProfile: 'プロファイルをコピー',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'メニューを開く',
+    noPermissionSystemInfo: 'システム情報を表示する権限がありません',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: '通知プロバイダーを削除',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'ファイルマネージャーを閉じる',
+    sortFiles: 'ファイルを並べ替え',
+    goToParentFolder: '親フォルダーへ移動',
+    threeView: '3Dビュー',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'ストリームを更新',
+    close: '閉じる',
+    zoomOut: 'ズームアウト',
+    resetZoom: 'ズームをリセット',
+    zoomIn: 'ズームイン',
+    dragToResize: 'ドラッグしてサイズ変更',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: '5秒戻る',
+    skipForward5s: '5秒進む',
+  },
+
+  // Notification Providers (descriptions)
+  notificationProviders: {
+    descriptions: {
+      email: 'SMTP電子メール通知',
+      telegram: 'Telegramボット経由の通知',
+      discord: 'Webhookを介してDiscordチャンネルに送信',
+      ntfy: '無料でセルフホスト可能なプッシュ通知',
+      pushover: 'シンプルで信頼性の高いプッシュ通知',
+      callmebot: 'CallMeBot経由の無料WhatsApp通知',
+      webhook: '任意のURLへのジェネリックHTTP POST',
+    },
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'スイッチバーにスイッチがありません',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'タイトル',
+      designer: 'デザイナー',
+      license: 'ライセンス',
+      description: '説明を入力...',
+      profileTitle: 'プロファイルタイトル',
+      profileDescription: 'プロファイルの説明...',
+    },
+  },
+
+  // GitHub Backup Settings
+  githubBackupSettings: {},
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

+ 141 - 50
frontend/src/pages/LoginPage.tsx

@@ -1,11 +1,14 @@
 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';
+import { Card, CardHeader, CardContent } from '../components/Card';
+import { Button } from '../components/Button';
 
 export function LoginPage() {
   const navigate = useNavigate();
@@ -16,6 +19,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 +38,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 +59,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 +91,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 +102,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,64 +136,120 @@ 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/70 flex items-center justify-center z-50 p-4"
           onClick={() => setShowForgotPassword(false)}
         >
-          <div
-            className="w-full max-w-md bg-bambu-card rounded-xl border border-bambu-dark-tertiary shadow-lg p-6"
-            onClick={(e) => e.stopPropagation()}
+          <Card
+            className="w-full max-w-md"
+            onClick={(e: React.MouseEvent) => e.stopPropagation()}
           >
-            <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" />
-                <h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
+            <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('login.forgotPasswordTitle')}</h2>
+                </div>
+                <Button
+                  variant="ghost"
+                  size="sm"
+                  onClick={() => {
+                    setShowForgotPassword(false);
+                    setForgotEmail('');
+                  }}
+                >
+                  <X className="w-5 h-5" />
+                </Button>
               </div>
-              <button
-                onClick={() => setShowForgotPassword(false)}
-                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>
+            </CardHeader>
+            <CardContent>
+              {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>
 
-            <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>
+                  <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>
 
-              <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 className="flex gap-2">
+                    <Button
+                      type="button"
+                      variant="secondary"
+                      className="flex-1"
+                      onClick={() => {
+                        setShowForgotPassword(false);
+                        setForgotEmail('');
+                      }}
+                    >
+                      {t('login.cancel') || 'Cancel'}
+                    </Button>
+                    <Button
+                      type="submit"
+                      className="flex-1"
+                      disabled={forgotPasswordMutation.isPending}
+                    >
+                      {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
+                    variant="secondary"
+                    className="w-full"
+                    onClick={() => setShowForgotPassword(false)}
+                  >
+                    {t('login.gotIt')}
+                  </Button>
+                </div>
+              )}
+            </CardContent>
+          </Card>
         </div>
       )}
     </div>

+ 203 - 53
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';
@@ -15,10 +15,12 @@ import { AddNotificationModal } from '../components/AddNotificationModal';
 import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor';
 import { NotificationLogViewer } from '../components/NotificationLogViewer';
 import { ConfirmModal } from '../components/ConfirmModal';
+import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal';
 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 +30,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', 'email', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const;
 type TabType = typeof validTabs[number];
 
 export function SettingsPage() {
@@ -99,13 +101,15 @@ export function SettingsPage() {
   const [deleteUserLoading, setDeleteUserLoading] = useState(false);
   const [userFormData, setUserFormData] = useState<{
     username: string;
-    password: string;
+    password?: string;
+    email?: string;
     confirmPassword: string;
     role: string;
     group_ids: number[];
   }>({
     username: '',
     password: '',
+    email: '',
     confirmPassword: '',
     role: 'user',
     group_ids: [],
@@ -307,6 +311,12 @@ export function SettingsPage() {
     queryFn: api.getCloudStatus,
   });
 
+  // Advanced auth status for user creation
+  const { data: advancedAuthStatus = { advanced_auth_enabled: false, smtp_configured: false } } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
+
   // User management queries and mutations
   const { hasPermission } = useAuth();
 
@@ -334,7 +344,7 @@ export function SettingsPage() {
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowCreateUserModal(false);
-      setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('settings.toast.userCreated'));
     },
     onError: (error: Error) => {
@@ -349,7 +359,7 @@ export function SettingsPage() {
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowEditUserModal(false);
       setEditingUserId(null);
-      setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('settings.toast.userUpdated'));
     },
     onError: (error: Error) => {
@@ -370,6 +380,16 @@ export function SettingsPage() {
     },
   });
 
+  const resetPasswordMutation = useMutation({
+    mutationFn: (userId: number) => api.resetUserPassword({ user_id: userId }),
+    onSuccess: (response) => {
+      showToast(response.message, 'success');
+    },
+    onError: (error: Error) => {
+      showToast(error.message, 'error');
+    },
+  });
+
   // Function to initiate user deletion with item count check
   const handleDeleteUserClick = async (userId: number) => {
     setDeleteUserId(userId);
@@ -424,21 +444,40 @@ export function SettingsPage() {
 
   // User management handlers
   const handleCreateUser = () => {
-    if (!userFormData.username || !userFormData.password) {
+    // Use the status from the query hook
+    const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false;
+    
+    if (!userFormData.username) {
       showToast(t('settings.toast.fillRequiredFields'), 'error');
       return;
     }
-    if (userFormData.password !== userFormData.confirmPassword) {
-      showToast(t('settings.toast.passwordsDoNotMatch'), 'error');
+    
+    // Email is required when advanced auth is enabled
+    if (advancedAuthEnabled && !userFormData.email) {
+      showToast('Email is required when advanced authentication is enabled', 'error');
       return;
     }
-    if (userFormData.password.length < 6) {
-      showToast(t('settings.toast.passwordTooShort'), 'error');
-      return;
+    
+    // Password validation only when advanced auth is disabled
+    if (!advancedAuthEnabled) {
+      if (!userFormData.password) {
+        showToast(t('settings.toast.fillRequiredFields'), 'error');
+        return;
+      }
+      if (userFormData.password !== userFormData.confirmPassword) {
+        showToast(t('settings.toast.passwordsDoNotMatch'), 'error');
+        return;
+      }
+      if (userFormData.password.length < 6) {
+        showToast(t('settings.toast.passwordTooShort'), 'error');
+        return;
+      }
     }
+    
     createUserMutation.mutate({
       username: userFormData.username,
-      password: userFormData.password,
+      password: advancedAuthEnabled ? undefined : userFormData.password,
+      email: userFormData.email || undefined,
       role: userFormData.role,
       group_ids: userFormData.group_ids.length > 0 ? userFormData.group_ids : undefined,
     });
@@ -472,6 +511,7 @@ export function SettingsPage() {
     setUserFormData({
       username: userToEdit.username,
       password: '',
+      email: userToEdit.email || '',
       confirmPassword: '',
       role: userToEdit.role,
       group_ids: userToEdit.groups?.map(g => g.id) || [],
@@ -919,7 +959,7 @@ export function SettingsPage() {
       </div>
 
       {/* Tab Navigation */}
-      <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary overflow-x-auto">
+      <div className="flex flex-wrap gap-1 mb-6 border-b border-bambu-dark-tertiary">
         <button
           onClick={() => handleTabChange('general')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
@@ -946,6 +986,17 @@ export function SettingsPage() {
             </span>
           )}
         </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.globalEmail') || 'Global Email'}
+        </button>
         <button
           onClick={() => handleTabChange('notifications')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
@@ -3456,6 +3507,25 @@ export function SettingsPage() {
             </CardContent>
           </Card>
 
+          {/* Advanced Authentication Notice Box */}
+          {advancedAuthStatus?.advanced_auth_enabled && (
+            <Card className="border-blue-500/30 bg-blue-500/5">
+              <CardContent className="py-4">
+                <div className="flex items-start gap-3">
+                  <div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-500/20 flex-shrink-0">
+                    <Mail className="w-5 h-5 text-blue-400" />
+                  </div>
+                  <div>
+                    <h3 className="text-white font-medium">{t('settings.email.advancedAuthEnabled')}</h3>
+                    <p className="text-sm text-bambu-gray mt-1">
+                      {t('settings.email.advancedAuthEnabledDesc')}
+                    </p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          )}
+
           {authEnabled && (
             <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
               {/* Left Column: Current User + User List */}
@@ -3521,7 +3591,7 @@ export function SettingsPage() {
                           size="sm"
                           onClick={() => {
                             setShowCreateUserModal(true);
-                            setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                           }}
                         >
                           <Plus className="w-4 h-4" />
@@ -3707,12 +3777,12 @@ export function SettingsPage() {
       )}
 
       {/* Create User Modal */}
-      {showCreateUserModal && (
+      {showCreateUserModal && !advancedAuthStatus?.advanced_auth_enabled && (
         <div
           className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
           onClick={() => {
             setShowCreateUserModal(false);
-            setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
         >
           <Card
@@ -3730,7 +3800,7 @@ export function SettingsPage() {
                   size="sm"
                   onClick={() => {
                     setShowCreateUserModal(false);
-                    setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                 >
                   <X className="w-5 h-5" />
@@ -3812,7 +3882,7 @@ export function SettingsPage() {
                   variant="secondary"
                   onClick={() => {
                     setShowCreateUserModal(false);
-                    setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                 >
                   Cancel
@@ -3839,6 +3909,22 @@ export function SettingsPage() {
         </div>
       )}
 
+      {/* Create User Modal - Advanced Authentication */}
+      {showCreateUserModal && advancedAuthStatus?.advanced_auth_enabled && (
+        <CreateUserAdvancedAuthModal
+          formData={userFormData}
+          setFormData={setUserFormData}
+          groups={groupsData}
+          onClose={() => {
+            setShowCreateUserModal(false);
+            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
+          }}
+          onCreate={handleCreateUser}
+          isCreating={createUserMutation.isPending}
+          isCreateButtonDisabled={createUserMutation.isPending || !userFormData.username || !userFormData.email}
+        />
+      )}
+
       {/* Edit User Modal */}
       {showEditUserModal && editingUserId !== null && (
         <div
@@ -3846,7 +3932,7 @@ export function SettingsPage() {
           onClick={() => {
             setShowEditUserModal(false);
             setEditingUserId(null);
-            setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
         >
           <Card
@@ -3865,7 +3951,7 @@ export function SettingsPage() {
                   onClick={() => {
                     setShowEditUserModal(false);
                     setEditingUserId(null);
-                    setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                 >
                   <X className="w-5 h-5" />
@@ -3874,8 +3960,11 @@ export function SettingsPage() {
             </CardHeader>
             <CardContent>
               <div className="space-y-4">
+                {/* Username Field */}
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">{t('settings.username')}</label>
+                  <label className="block text-sm font-medium text-white mb-2">
+                    {t('settings.username')} {advancedAuthStatus?.advanced_auth_enabled && <span className="text-red-400">*</span>}
+                  </label>
                   <input
                     type="text"
                     value={userFormData.username}
@@ -3885,43 +3974,94 @@ export function SettingsPage() {
                     autoComplete="username"
                   />
                 </div>
+
+                {/* Email Field */}
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
-                    Password <span className="text-bambu-gray font-normal">(leave blank to keep current)</span>
+                    {t('users.form.email') || 'Email'} {advancedAuthStatus?.advanced_auth_enabled ? <span className="text-red-400">*</span> : <span className="text-bambu-gray font-normal">({t('users.form.optional') || 'optional'})</span>}
                   </label>
                   <input
-                    type="password"
-                    value={userFormData.password}
-                    onChange={(e) => setUserFormData({ ...userFormData, password: e.target.value, confirmPassword: '' })}
+                    type="email"
+                    value={userFormData.email}
+                    onChange={(e) => setUserFormData({ ...userFormData, 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('settings.enterNewPassword')}
-                    autoComplete="new-password"
-                    minLength={6}
+                    placeholder={t('users.form.emailPlaceholder') || 'user@example.com'}
+                    required={advancedAuthStatus?.advanced_auth_enabled}
                   />
                 </div>
-                {userFormData.password && (
-                  <div>
-                    <label className="block text-sm font-medium text-white mb-2">{t('settings.confirmPassword')}</label>
-                    <input
-                      type="password"
-                      value={userFormData.confirmPassword}
-                      onChange={(e) => setUserFormData({ ...userFormData, 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 ${
-                        userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword
-                          ? 'border-red-500'
-                          : 'border-bambu-dark-tertiary'
-                      }`}
-                      placeholder={t('settings.confirmNewPassword')}
-                      autoComplete="new-password"
-                      minLength={6}
-                    />
-                    {userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword && (
-                      <p className="text-red-400 text-xs mt-1">{t('settings.passwordsDoNotMatch')}</p>
+
+                {/* Password Fields - only show when Advanced Auth is disabled */}
+                {!advancedAuthStatus?.advanced_auth_enabled && (
+                  <>
+                    <div>
+                      <label className="block text-sm font-medium text-white mb-2">
+                        {t('users.form.password') || 'Password'} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep') || 'leave blank to keep current'})</span>
+                      </label>
+                      <input
+                        type="password"
+                        value={userFormData.password}
+                        onChange={(e) => setUserFormData({ ...userFormData, password: e.target.value, confirmPassword: '' })}
+                        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('settings.enterNewPassword')}
+                        autoComplete="new-password"
+                        minLength={6}
+                      />
+                    </div>
+                    {userFormData.password && (
+                      <div>
+                        <label className="block text-sm font-medium text-white mb-2">{t('settings.confirmPassword')}</label>
+                        <input
+                          type="password"
+                          value={userFormData.confirmPassword}
+                          onChange={(e) => setUserFormData({ ...userFormData, 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 ${
+                            userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword
+                              ? 'border-red-500'
+                              : 'border-bambu-dark-tertiary'
+                          }`}
+                          placeholder={t('settings.confirmNewPassword')}
+                          autoComplete="new-password"
+                          minLength={6}
+                        />
+                        {userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword && (
+                          <p className="text-red-400 text-xs mt-1">{t('settings.passwordsDoNotMatch')}</p>
+                        )}
+                      </div>
                     )}
+                  </>
+                )}
+
+                {/* Info box about auto-generated password when Advanced Auth is enabled */}
+                {advancedAuthStatus?.advanced_auth_enabled && (
+                  <div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3 space-y-3">
+                    <p className="text-sm text-bambu-gray">
+                      {t('users.form.passwordManagedByAdvancedAuth') || 'Password is managed by Advanced Authentication. Use "Reset Password" to send a new password to the user via email.'}
+                    </p>
+                    <Button
+                      variant="secondary"
+                      size="sm"
+                      onClick={() => editingUserId && resetPasswordMutation.mutate(editingUserId)}
+                      disabled={resetPasswordMutation.isPending || !userFormData.email}
+                      className="w-full"
+                    >
+                      {resetPasswordMutation.isPending ? (
+                        <>
+                          <Loader2 className="w-4 h-4 animate-spin" />
+                          {t('users.form.resettingPassword') || 'Resetting Password...'}
+                        </>
+                      ) : (
+                        <>
+                          <RotateCcw className="w-4 h-4" />
+                          {t('users.form.resetPassword') || 'Reset Password'}
+                        </>
+                      )}
+                    </Button>
                   </div>
                 )}
+
+                {/* Groups Field */}
                 <div>
-                  <label className="block text-sm font-medium text-white mb-2">Groups</label>
+                  <label className="block text-sm font-medium text-white mb-2">{t('users.form.groups') || 'Groups'}</label>
                   <div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
                     {groupsData.map(group => (
                       <label
@@ -3936,7 +4076,7 @@ export function SettingsPage() {
                         />
                         <span className="text-sm text-white">{group.name}</span>
                         {group.is_system && (
-                          <span className="text-xs text-yellow-400">(System)</span>
+                          <span className="text-xs text-yellow-400">({t('users.system') || 'System'})</span>
                         )}
                       </label>
                     ))}
@@ -3949,24 +4089,29 @@ export function SettingsPage() {
                   onClick={() => {
                     setShowEditUserModal(false);
                     setEditingUserId(null);
-                    setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                 >
-                  Cancel
+                  {t('users.modal.cancel') || 'Cancel'}
                 </Button>
                 <Button
                   onClick={() => handleUpdateUser(editingUserId)}
-                  disabled={updateUserMutation.isPending || !userFormData.username || !!(userFormData.password && (userFormData.password !== userFormData.confirmPassword || userFormData.password.length < 6))}
+                  disabled={
+                    updateUserMutation.isPending || 
+                    !userFormData.username || 
+                    (advancedAuthStatus?.advanced_auth_enabled && !userFormData.email) ||
+                    Boolean(!advancedAuthStatus?.advanced_auth_enabled && userFormData.password && (userFormData.password !== userFormData.confirmPassword || userFormData.password.length < 6))
+                  }
                 >
                   {updateUserMutation.isPending ? (
                     <>
                       <Loader2 className="w-4 h-4 animate-spin" />
-                      Saving...
+                      {t('users.modal.saving') || 'Saving...'}
                     </>
                   ) : (
                     <>
                       <Save className="w-4 h-4" />
-                      Save Changes
+                      {t('users.modal.saveChanges') || 'Save Changes'}
                     </>
                   )}
                 </Button>
@@ -4249,6 +4394,11 @@ export function SettingsPage() {
         />
       )}
 
+      {/* Email Tab */}
+      {activeTab === 'email' && (
+        <EmailSettings />
+      )}
+
       {/* Backup Tab */}
       {activeTab === 'backup' && (
         <GitHubBackupSettings />

+ 130 - 21
frontend/src/pages/UsersPage.tsx

@@ -1,8 +1,8 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } 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';
@@ -10,10 +10,12 @@ import { useToast } from '../contexts/ToastContext';
 import { Button } from '../components/Button';
 import { Card, CardContent, CardHeader } from '../components/Card';
 import { ConfirmModal } from '../components/ConfirmModal';
+import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal';
 
 interface FormData extends UserCreate {
   group_ids: number[];
   confirmPassword: string;
+  email?: string;
 }
 
 export function UsersPage() {
@@ -29,23 +31,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 = { advanced_auth_enabled: false, smtp_configured: false } } = 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 +80,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 +95,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 +114,77 @@ 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');
+    },
+  });
+
+  // Validation for create user button
+  const isCreateButtonDisabled = useMemo(() => {
+    if (createMutation.isPending || !formData.username) {
+      return true;
+    }
+    if (advancedAuthStatus?.advanced_auth_enabled) {
+      // When advanced auth is enabled, require email (password is auto-generated)
+      return !formData.email;
+    }
+    // When advanced auth is disabled, require valid password
+    return !formData.password || formData.password !== formData.confirmPassword || formData.password.length < 6;
+  }, [
+    createMutation.isPending,
+    formData.username,
+    formData.email,
+    formData.password,
+    formData.confirmPassword,
+    advancedAuthStatus?.advanced_auth_enabled
+  ]);
+
   const handleCreate = () => {
-    if (!formData.username || !formData.password) {
-      showToast(t('users.toast.fillRequired'), 'error');
+    // Use the status from the query hook
+    const advancedAuthEnabled = advancedAuthStatus?.advanced_auth_enabled || false;
+    
+    if (!formData.username) {
+      const errorMsg = t('users.toast.fillRequired');
+      showToast(errorMsg, 'error');
+      if (advancedAuthEnabled) {
+        console.error('[Advanced Auth] Create user failed: Username is required');
+      }
       return;
     }
-    if (formData.password !== formData.confirmPassword) {
-      showToast(t('users.toast.passwordsDoNotMatch'), 'error');
+    
+    // Email is required when advanced auth is enabled
+    if (advancedAuthEnabled && !formData.email) {
+      const errorMsg = 'Email is required when advanced authentication is enabled';
+      showToast(errorMsg, 'error');
+      console.error('[Advanced Auth] Create user failed: Email is required when advanced authentication is enabled');
       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 +205,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 +213,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 +229,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 +240,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 +291,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 +386,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.form.resetPassword') || 'Reset Password'}
+                          </Button>
+                        )}
                       </div>
                     </td>
                   </tr>
@@ -327,12 +408,12 @@ export function UsersPage() {
       )}
 
       {/* Create User Modal */}
-      {showCreateModal && (
+      {showCreateModal && !advancedAuthStatus?.advanced_auth_enabled && (
         <div
           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 +431,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" />
@@ -440,14 +521,14 @@ 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')}
                 </Button>
                 <Button
                   onClick={handleCreate}
-                  disabled={createMutation.isPending || !formData.username || !formData.password || formData.password !== formData.confirmPassword || formData.password.length < 6}
+                  disabled={isCreateButtonDisabled}
                 >
                   {createMutation.isPending ? (
                     <>
@@ -467,6 +548,22 @@ export function UsersPage() {
         </div>
       )}
 
+      {/* Create User Modal - Advanced Authentication */}
+      {showCreateModal && advancedAuthStatus?.advanced_auth_enabled && (
+        <CreateUserAdvancedAuthModal
+          formData={formData}
+          setFormData={setFormData}
+          groups={groups}
+          onClose={() => {
+            setShowCreateModal(false);
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
+          }}
+          onCreate={handleCreate}
+          isCreating={createMutation.isPending}
+          isCreateButtonDisabled={isCreateButtonDisabled}
+        />
+      )}
+
       {/* Edit User Modal */}
       {showEditModal && editingUserId !== null && (
         <div
@@ -507,6 +604,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>

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


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


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