Browse Source

Merge pull request #322 from cadtoolbox/feature_user_authentication

Feature - Advanced Authentication via Email
MartinNYHC 3 months ago
parent
commit
6ecc29de54

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

@@ -5,19 +5,43 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import selectinload
 
 
+from backend.app.api.routes.settings import get_external_login_url
 from backend.app.core.auth import (
 from backend.app.core.auth import (
     ACCESS_TOKEN_EXPIRE_MINUTES,
     ACCESS_TOKEN_EXPIRE_MINUTES,
     authenticate_user,
     authenticate_user,
+    authenticate_user_by_email,
     create_access_token,
     create_access_token,
     get_current_active_user,
     get_current_active_user,
     get_password_hash,
     get_password_hash,
+    get_user_by_email,
     get_user_by_username,
     get_user_by_username,
 )
 )
 from backend.app.core.database import get_db
 from backend.app.core.database import get_db
 from backend.app.models.group import Group
 from backend.app.models.group import Group
 from backend.app.models.settings import Settings
 from backend.app.models.settings import Settings
 from backend.app.models.user import User
 from backend.app.models.user import User
-from backend.app.schemas.auth import GroupBrief, LoginRequest, LoginResponse, SetupRequest, SetupResponse, UserResponse
+from backend.app.schemas.auth import (
+    ForgotPasswordRequest,
+    ForgotPasswordResponse,
+    GroupBrief,
+    LoginRequest,
+    LoginResponse,
+    ResetPasswordRequest,
+    ResetPasswordResponse,
+    SetupRequest,
+    SetupResponse,
+    SMTPSettings,
+    TestSMTPRequest,
+    TestSMTPResponse,
+    UserResponse,
+)
+from backend.app.services.email_service import (
+    create_password_reset_email_from_template,
+    generate_secure_password,
+    get_smtp_settings,
+    save_smtp_settings,
+    send_email,
+)
 
 
 
 
 def _user_to_response(user: User) -> UserResponse:
 def _user_to_response(user: User) -> UserResponse:
@@ -25,6 +49,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
     return UserResponse(
         id=user.id,
         id=user.id,
         username=user.username,
         username=user.username,
+        email=user.email,
         role=user.role,
         role=user.role,
         is_active=user.is_active,
         is_active=user.is_active,
         is_admin=user.is_admin,
         is_admin=user.is_admin,
@@ -46,6 +71,27 @@ async def is_auth_enabled(db: AsyncSession) -> bool:
     return setting.value.lower() == "true"
     return setting.value.lower() == "true"
 
 
 
 
+async def is_advanced_auth_enabled(db: AsyncSession) -> bool:
+    """Check if advanced authentication is enabled."""
+    result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
+    setting = result.scalar_one_or_none()
+    if setting is None:
+        return False
+    return setting.value.lower() == "true"
+
+
+async def set_advanced_auth_enabled(db: AsyncSession, enabled: bool) -> None:
+    """Set advanced authentication enabled status."""
+    from sqlalchemy import func
+    from sqlalchemy.dialects.sqlite import insert as sqlite_insert
+
+    stmt = sqlite_insert(Settings).values(key="advanced_auth_enabled", value="true" if enabled else "false")
+    stmt = stmt.on_conflict_do_update(
+        index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
+    )
+    await db.execute(stmt)
+
+
 async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
 async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
     """Set authentication enabled status."""
     """Set authentication enabled status."""
     from sqlalchemy import func
     from sqlalchemy import func
@@ -216,7 +262,10 @@ async def disable_auth(
 
 
 @router.post("/login", response_model=LoginResponse)
 @router.post("/login", response_model=LoginResponse)
 async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
 async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
-    """Login and get access token."""
+    """Login and get access token.
+
+    Supports username or email-based login. Username lookup is case-insensitive.
+    """
     # Check if auth is enabled
     # Check if auth is enabled
     auth_enabled = await is_auth_enabled(db)
     auth_enabled = await is_auth_enabled(db)
     if not auth_enabled:
     if not auth_enabled:
@@ -225,7 +274,15 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
             detail="Authentication is not enabled",
             detail="Authentication is not enabled",
         )
         )
 
 
+    # Try username-based authentication first
     user = await authenticate_user(db, request.username, request.password)
     user = await authenticate_user(db, request.username, request.password)
+
+    # If username auth failed and advanced auth is enabled, try email-based authentication
+    if not user:
+        advanced_auth = await is_advanced_auth_enabled(db)
+        if advanced_auth:
+            user = await authenticate_user_by_email(db, request.username, request.password)
+
     if not user:
     if not user:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
@@ -263,3 +320,332 @@ async def get_current_user_info(
 async def logout():
 async def logout():
     """Logout (client should discard token)."""
     """Logout (client should discard token)."""
     return {"message": "Logged out successfully"}
     return {"message": "Logged out successfully"}
+
+
+# Advanced Authentication Endpoints
+
+
+@router.post("/smtp/test", response_model=TestSMTPResponse)
+async def test_smtp_connection(
+    test_request: TestSMTPRequest,
+    current_user: User = Depends(get_current_active_user),
+    db: AsyncSession = Depends(get_db),
+):
+    """Test SMTP connection with provided settings (admin only)."""
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Reload user with groups for proper is_admin check
+    result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
+    user = result.scalar_one()
+
+    if not user.is_admin:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Only admins can test SMTP settings",
+        )
+
+    try:
+        smtp_settings = SMTPSettings(
+            smtp_host=test_request.smtp_host,
+            smtp_port=test_request.smtp_port,
+            smtp_username=test_request.smtp_username,
+            smtp_password=test_request.smtp_password,
+            smtp_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_skipped": "Queue Job Skipped",
     "queue_job_failed": "Queue Job Failed",
     "queue_job_failed": "Queue Job Failed",
     "queue_completed": "Queue Completed",
     "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
     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:
 async def set_setting(db: AsyncSession, key: str, value: str) -> None:
     """Set a single setting value."""
     """Set a single setting value."""
     from sqlalchemy import func
     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.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import selectinload
 
 
+from backend.app.api.routes.settings import get_external_login_url
 from backend.app.core.auth import (
 from backend.app.core.auth import (
     RequirePermissionIfAuthEnabled,
     RequirePermissionIfAuthEnabled,
     get_current_user_optional,
     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.group import Group
 from backend.app.models.library import LibraryFile
 from backend.app.models.library import LibraryFile
 from backend.app.models.print_queue import PrintQueueItem
 from backend.app.models.print_queue import PrintQueueItem
+from backend.app.models.settings import Settings
 from backend.app.models.user import User
 from backend.app.models.user import User
 from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
 from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
+from backend.app.services.email_service import (
+    create_welcome_email_from_template,
+    generate_secure_password,
+    get_smtp_settings,
+    send_email,
+)
 
 
 router = APIRouter(prefix="/users", tags=["users"])
 router = APIRouter(prefix="/users", tags=["users"])
 
 
@@ -26,6 +34,7 @@ def _user_to_response(user: User) -> UserResponse:
     return UserResponse(
     return UserResponse(
         id=user.id,
         id=user.id,
         username=user.username,
         username=user.username,
+        email=user.email,
         role=user.role,
         role=user.role,
         is_active=user.is_active,
         is_active=user.is_active,
         is_admin=user.is_admin,
         is_admin=user.is_admin,
@@ -54,9 +63,24 @@ async def create_user(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
     db: AsyncSession = Depends(get_db),
     db: AsyncSession = Depends(get_db),
 ):
 ):
-    """Create a new user."""
-    # Check if username already exists
-    existing_user = await db.execute(select(User).where(User.username == user_data.username))
+    """Create a new user.
+
+    When advanced authentication is enabled:
+    - Email is required
+    - Password is auto-generated and emailed to user
+    - Admin cannot set or see the password
+    """
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    # Check if advanced auth is enabled
+    result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
+    advanced_auth_setting = result.scalar_one_or_none()
+    advanced_auth_enabled = advanced_auth_setting and advanced_auth_setting.value.lower() == "true"
+
+    # Check if username already exists (case-insensitive)
+    existing_user = await db.execute(select(User).where(func.lower(User.username) == func.lower(user_data.username)))
     if existing_user.scalar_one_or_none():
     if existing_user.scalar_one_or_none():
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
@@ -70,9 +94,36 @@ async def create_user(
             detail="Role must be 'admin' or 'user'",
             detail="Role must be 'admin' or 'user'",
         )
         )
 
 
+    # Advanced auth validation
+    if advanced_auth_enabled:
+        if not user_data.email:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email is required when advanced authentication is enabled",
+            )
+        # Check if email already exists (case-insensitive)
+        existing_email = await db.execute(select(User).where(func.lower(User.email) == func.lower(user_data.email)))
+        if existing_email.scalar_one_or_none():
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email already exists",
+            )
+
+    # Generate password if advanced auth enabled, otherwise require password
+    if advanced_auth_enabled:
+        password = generate_secure_password()
+    else:
+        if not user_data.password:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Password is required when advanced authentication is disabled",
+            )
+        password = user_data.password
+
     new_user = User(
     new_user = User(
         username=user_data.username,
         username=user_data.username,
-        password_hash=get_password_hash(user_data.password),
+        email=user_data.email,
+        password_hash=get_password_hash(password),
         role=user_data.role,
         role=user_data.role,
         is_active=True,
         is_active=True,
     )
     )
@@ -92,6 +143,23 @@ async def create_user(
     await db.commit()
     await db.commit()
     await db.refresh(new_user)
     await db.refresh(new_user)
 
 
+    # Send welcome email if advanced auth enabled
+    if advanced_auth_enabled and new_user.email:
+        try:
+            smtp_settings = await get_smtp_settings(db)
+            if smtp_settings:
+                login_url = 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)
     return _user_to_response(new_user)
 
 
 
 
@@ -161,8 +229,10 @@ async def update_user(
             )
             )
 
 
     if user_data.username is not None:
     if user_data.username is not None:
-        # Check if new username already exists
-        existing_user = await db.execute(select(User).where(User.username == user_data.username, User.id != user_id))
+        # Check if new username already exists (case-insensitive)
+        existing_user = await db.execute(
+            select(User).where(func.lower(User.username) == func.lower(user_data.username), User.id != user_id)
+        )
         if existing_user.scalar_one_or_none():
         if existing_user.scalar_one_or_none():
             raise HTTPException(
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 status_code=status.HTTP_400_BAD_REQUEST,
@@ -170,6 +240,18 @@ async def update_user(
             )
             )
         user.username = user_data.username
         user.username = user_data.username
 
 
+    if user_data.email is not None:
+        # Check if new email already exists (case-insensitive)
+        existing_email = await db.execute(
+            select(User).where(func.lower(User.email) == func.lower(user_data.email), User.id != user_id)
+        )
+        if existing_email.scalar_one_or_none():
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="Email already exists",
+            )
+        user.email = user_data.email
+
     if user_data.password is not None:
     if user_data.password is not None:
         user.password_hash = get_password_hash(user_data.password)
         user.password_hash = get_password_hash(user_data.password)
 
 

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

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

+ 2 - 0
backend/app/main.py

@@ -2764,6 +2764,8 @@ PUBLIC_API_ROUTES = {
     "/api/v1/auth/status",
     "/api/v1/auth/status",
     "/api/v1/auth/login",
     "/api/v1/auth/login",
     "/api/v1/auth/setup",  # Needed for initial setup and recovery
     "/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)
     # Version check for updates (no sensitive data)
     "/api/v1/updates/version",
     "/api/v1/updates/version",
     # Metrics endpoint handles its own prometheus_token authentication
     # 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",
         "title_template": "Queue Complete",
         "body_template": "All {completed_count} queued jobs have finished",
         "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)
     id: Mapped[int] = mapped_column(primary_key=True)
     username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
     username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
+    email: Mapped[str | None] = mapped_column(String(255), unique=True, index=True, nullable=True)
     password_hash: Mapped[str] = mapped_column(String(255))
     password_hash: Mapped[str] = mapped_column(String(255))
     role: Mapped[str] = mapped_column(
     role: Mapped[str] = mapped_column(
         String(20), default="user"
         String(20), default="user"

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

@@ -24,7 +24,8 @@ class LoginResponse(BaseModel):
 
 
 class UserCreate(BaseModel):
 class UserCreate(BaseModel):
     username: str
     username: str
-    password: str
+    password: str | None = None  # Optional when advanced auth is enabled
+    email: str | None = None
     role: str = "user"
     role: str = "user"
     group_ids: list[int] | None = None
     group_ids: list[int] | None = None
 
 
@@ -32,6 +33,7 @@ class UserCreate(BaseModel):
 class UserUpdate(BaseModel):
 class UserUpdate(BaseModel):
     username: str | None = None
     username: str | None = None
     password: str | None = None
     password: str | None = None
+    email: str | None = None
     role: str | None = None
     role: str | None = None
     is_active: bool | None = None
     is_active: bool | None = None
     group_ids: list[int] | None = None
     group_ids: list[int] | None = None
@@ -40,6 +42,7 @@ class UserUpdate(BaseModel):
 class UserResponse(BaseModel):
 class UserResponse(BaseModel):
     id: int
     id: int
     username: str
     username: str
+    email: str | None = None
     role: str  # Deprecated, kept for backward compatibility
     role: str  # Deprecated, kept for backward compatibility
     is_active: bool
     is_active: bool
     is_admin: bool  # Computed from role and group membership
     is_admin: bool  # Computed from role and group membership
@@ -65,3 +68,50 @@ class SetupRequest(BaseModel):
 class SetupResponse(BaseModel):
 class SetupResponse(BaseModel):
     auth_enabled: bool
     auth_enabled: bool
     admin_created: bool | None = None
     admin_created: bool | None = None
+
+
+class ForgotPasswordRequest(BaseModel):
+    email: str
+
+
+class ForgotPasswordResponse(BaseModel):
+    message: str
+
+
+class ResetPasswordRequest(BaseModel):
+    user_id: int
+
+
+class ResetPasswordResponse(BaseModel):
+    message: str
+
+
+class SMTPSettings(BaseModel):
+    smtp_host: str
+    smtp_port: int
+    smtp_username: str | 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_skipped": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_job_failed": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_job_failed": ["printer", "job_name", "reason", "timestamp", "app_name"],
     "queue_completed": ["completed_count", "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
 # Sample data for previewing templates
@@ -191,6 +194,21 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 18:30",
         "timestamp": "2024-01-15 18:30",
         "app_name": "Bambuddy",
         "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}"},
             headers={"Authorization": f"Bearer {token}"},
         )
         )
         assert response.status_code == 200
         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

File diff suppressed because it is too large
+ 228 - 198
frontend/package-lock.json


+ 3 - 0
frontend/package.json

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

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

@@ -1906,6 +1906,7 @@ export interface LoginResponse {
 export interface UserResponse {
 export interface UserResponse {
   id: number;
   id: number;
   username: string;
   username: string;
+  email?: string;
   role: string;  // Deprecated, kept for backward compatibility
   role: string;  // Deprecated, kept for backward compatibility
   is_active: boolean;
   is_active: boolean;
   is_admin: boolean;  // Computed from role and group membership
   is_admin: boolean;  // Computed from role and group membership
@@ -1916,7 +1917,8 @@ export interface UserResponse {
 
 
 export interface UserCreate {
 export interface UserCreate {
   username: string;
   username: string;
-  password: string;
+  password?: string;  // Optional when advanced auth is enabled
+  email?: string;
   role: string;
   role: string;
   group_ids?: number[];
   group_ids?: number[];
 }
 }
@@ -1924,6 +1926,7 @@ export interface UserCreate {
 export interface UserUpdate {
 export interface UserUpdate {
   username?: string;
   username?: string;
   password?: string;
   password?: string;
+  email?: string;
   role?: string;
   role?: string;
   is_active?: boolean;
   is_active?: boolean;
   group_ids?: number[];
   group_ids?: number[];
@@ -1935,6 +1938,54 @@ export interface SetupRequest {
   admin_password?: string;
   admin_password?: string;
 }
 }
 
 
+export interface ForgotPasswordRequest {
+  email: string;
+}
+
+export interface ForgotPasswordResponse {
+  message: string;
+}
+
+export interface ResetPasswordRequest {
+  user_id: number;
+}
+
+export interface ResetPasswordResponse {
+  message: string;
+}
+
+export interface SMTPSettings {
+  smtp_host: string;
+  smtp_port: number;
+  smtp_username?: string;
+  smtp_password?: string;
+  smtp_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 {
 export interface SetupResponse {
   auth_enabled: boolean;
   auth_enabled: boolean;
   admin_created?: boolean;
   admin_created?: boolean;
@@ -1968,6 +2019,38 @@ export const api = {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
     request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
       method: 'POST',
       method: 'POST',
     }),
     }),
+  
+  // Advanced Authentication
+  testSMTP: (data: TestSMTPRequest) =>
+    request<TestSMTPResponse>('/auth/smtp/test', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  getSMTPSettings: () => request<SMTPSettings | null>('/auth/smtp'),
+  saveSMTPSettings: (data: SMTPSettings) =>
+    request<{ message: string }>('/auth/smtp', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  enableAdvancedAuth: () =>
+    request<{ message: string; advanced_auth_enabled: boolean }>('/auth/advanced-auth/enable', {
+      method: 'POST',
+    }),
+  disableAdvancedAuth: () =>
+    request<{ message: string; advanced_auth_enabled: boolean }>('/auth/advanced-auth/disable', {
+      method: 'POST',
+    }),
+  getAdvancedAuthStatus: () => request<AdvancedAuthStatus>('/auth/advanced-auth/status'),
+  forgotPassword: (data: ForgotPasswordRequest) =>
+    request<ForgotPasswordResponse>('/auth/forgot-password', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
+  resetUserPassword: (data: ResetPasswordRequest) =>
+    request<ResetPasswordResponse>('/auth/reset-password', {
+      method: 'POST',
+      body: JSON.stringify(data),
+    }),
 
 
   // Users
   // Users
   getUsers: () => request<UserResponse[]>('/users/'),
   getUsers: () => request<UserResponse[]>('/users/'),

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

@@ -1,6 +1,7 @@
 import { useState, useEffect, useRef } from 'react';
 import { useState, useEffect, useRef } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 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 { 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 { api } from '../api/client';
 import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
 import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
 import { Button } from './Button';
 import { Button } from './Button';
@@ -11,6 +12,7 @@ interface AddSmartPlugModalProps {
 }
 }
 
 
 export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const isEditing = !!plug;
   const isEditing = !!plug;
 
 
@@ -469,7 +471,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {isScanning && scanProgress.total > 0 && (
               {isScanning && scanProgress.total > 0 && (
                 <div className="space-y-1">
                 <div className="space-y-1">
                   <div className="flex justify-between text-xs text-bambu-gray">
                   <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>
                     <span>{scanProgress.scanned} / {scanProgress.total}</span>
                   </div>
                   </div>
                   <div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
                   <div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
@@ -538,7 +540,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                       disabled
                       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"
                       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>
                     </select>
                   </div>
                   </div>
                 </div>
                 </div>
@@ -585,7 +587,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                               setIsEntityDropdownOpen(true);
                               setIsEntityDropdownOpen(true);
                               setHaEntitySearch('');
                               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"
                             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 && (
                           {haEntityId && !isEntityDropdownOpen && (
@@ -697,7 +699,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsPowerDropdownOpen(true);
                                   setIsPowerDropdownOpen(true);
                                   setPowerSensorSearch('');
                                   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"
                                 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 && (
                               {haPowerEntity && !isPowerDropdownOpen && (
@@ -781,7 +783,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTodayDropdownOpen(true);
                                   setIsEnergyTodayDropdownOpen(true);
                                   setEnergyTodaySearch('');
                                   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"
                                 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 && (
                               {haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
@@ -865,7 +867,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTotalDropdownOpen(true);
                                   setIsEnergyTotalDropdownOpen(true);
                                   setEnergyTotalSearch('');
                                   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"
                                 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 && (
                               {haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
@@ -1060,7 +1062,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                           type="text"
                           type="text"
                           value={mqttStateOnValue}
                           value={mqttStateOnValue}
                           onChange={(e) => setMqttStateOnValue(e.target.value)}
                           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"
                           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>
                       </div>
@@ -1128,7 +1130,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               ) : (
               ) : (
                 <>
                 <>
                   <WifiOff className="w-5 h-5" />
                   <WifiOff className="w-5 h-5" />
-                  <span>Connection failed</span>
+                  <span>{t('smartPlugs.addSmartPlug.connectionFailed')}</span>
                 </>
                 </>
               )}
               )}
             </div>
             </div>
@@ -1141,7 +1143,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               type="text"
               type="text"
               value={name}
               value={name}
               onChange={(e) => setName(e.target.value)}
               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"
               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>
           </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 { useQuery, useMutation } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { KProfile } from '../api/client';
 import type { KProfile } from '../api/client';
 import { Button } from './Button';
 import { Button } from './Button';
@@ -634,7 +635,7 @@ export function ConfigureAmsSlotModal({
               <div className="text-center space-y-3">
               <div className="text-center space-y-3">
                 <CheckCircle2 className="w-16 h-16 text-bambu-green mx-auto" />
                 <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-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>
             </div>
             </div>
           )}
           )}
@@ -672,7 +673,7 @@ export function ConfigureAmsSlotModal({
                 <div className="relative">
                 <div className="relative">
                   <input
                   <input
                     type="text"
                     type="text"
-                    placeholder="Search presets..."
+                    placeholder={t('configureAmsSlot.searchPresets')}
                     value={searchQuery}
                     value={searchQuery}
                     onChange={(e) => setSearchQuery(e.target.value)}
                     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"
                     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 && (
                               {preset.isUser && (
                                 <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
                                 <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
-                                  Custom
+                                  {t('configureAmsSlot.custom')}
                                 </span>
                                 </span>
                               )}
                               )}
                             </div>
                             </div>
@@ -822,7 +823,7 @@ export function ConfigureAmsSlotModal({
                   />
                   />
                   <input
                   <input
                     type="text"
                     type="text"
-                    placeholder="Color name or hex (e.g., brown, FF8800)"
+                    placeholder={t('configureAmsSlot.colorPlaceholder')}
                     value={colorInput}
                     value={colorInput}
                     onChange={(e) => {
                     onChange={(e) => {
                       const input = e.target.value;
                       const input = e.target.value;
@@ -852,7 +853,7 @@ export function ConfigureAmsSlotModal({
                         setColorInput('');
                         setColorInput('');
                       }}
                       }}
                       className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded"
                       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
                       Clear
                     </button>
                     </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 { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon } from 'lucide-react';
 import { Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { ExternalLink } from '../api/client';
 import type { ExternalLink } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
 import { Card, CardContent, CardHeader } from './Card';
@@ -10,6 +11,7 @@ import { ConfirmModal } from './ConfirmModal';
 import { getIconByName } from './IconPicker';
 import { getIconByName } from './IconPicker';
 
 
 export function ExternalLinksSettings() {
 export function ExternalLinksSettings() {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const [showAddModal, setShowAddModal] = useState(false);
   const [showAddModal, setShowAddModal] = useState(false);
   const [editingLink, setEditingLink] = useState<ExternalLink | null>(null);
   const [editingLink, setEditingLink] = useState<ExternalLink | null>(null);
@@ -132,7 +134,7 @@ export function ExternalLinksSettings() {
                       <button
                       <button
                         onClick={() => setEditingLink(link)}
                         onClick={() => setEditingLink(link)}
                         className="p-2 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
                         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" />
                         <Pencil className="w-4 h-4" />
                       </button>
                       </button>
@@ -140,7 +142,7 @@ export function ExternalLinksSettings() {
                         onClick={() => handleDelete(link)}
                         onClick={() => handleDelete(link)}
                         disabled={deleteMutation.isPending}
                         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"
                         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" />
                         <Trash2 className="w-4 h-4" />
                       </button>
                       </button>
@@ -152,7 +154,7 @@ export function ExternalLinksSettings() {
           ) : (
           ) : (
             <div className="text-center py-8 text-bambu-gray">
             <div className="text-center py-8 text-bambu-gray">
               <Link2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
               <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>
               <p className="text-sm">Click "Add Link" to add one</p>
             </div>
             </div>
           )}
           )}

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

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

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

@@ -1,6 +1,7 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 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 { 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 { api } from '../api/client';
 import type { SmartPlug, SmartPlugUpdate } from '../api/client';
 import type { SmartPlug, SmartPlugUpdate } from '../api/client';
 import { Card, CardContent } from './Card';
 import { Card, CardContent } from './Card';
@@ -14,6 +15,7 @@ interface SmartPlugCardProps {
 }
 }
 
 
 export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
 export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { showToast } = useToast();
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   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">
                 <div className="flex items-center gap-1 text-sm text-status-error">
                   <WifiOff className="w-4 h-4" />
                   <WifiOff className="w-4 h-4" />
-                  <span>Offline</span>
+                  <span>{t('smartPlugs.offline')}</span>
                 </div>
                 </div>
               )}
               )}
               {/* Admin page link - only for Tasmota */}
               {/* Admin page link - only for Tasmota */}
@@ -181,10 +183,10 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   target="_blank"
                   target="_blank"
                   rel="noopener noreferrer"
                   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"
                   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" />
                   <ExternalLink className="w-3 h-3" />
-                  Admin
+                  {t('smartPlugs.admin')}
                 </a>
                 </a>
               )}
               )}
             </div>
             </div>
@@ -449,7 +451,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Delete Confirmation */}
       {/* Delete Confirmation */}
       {showDeleteConfirm && (
       {showDeleteConfirm && (
         <ConfirmModal
         <ConfirmModal
-          title="Delete Smart Plug"
+          title={t('smartPlugs.deleteSmartPlug')}
           message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
           message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
           confirmText="Delete"
           confirmText="Delete"
           variant="danger"
           variant="danger"
@@ -464,9 +466,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power On Confirmation */}
       {/* Power On Confirmation */}
       {showPowerOnConfirm && (
       {showPowerOnConfirm && (
         <ConfirmModal
         <ConfirmModal
-          title="Turn On Smart Plug"
+          title={t('smartPlugs.turnOnSmartPlug')}
           message={`Are you sure you want to turn on "${plug.name}"?`}
           message={`Are you sure you want to turn on "${plug.name}"?`}
-          confirmText="Turn On"
+          confirmText={t('smartPlugs.turnOn')}
           variant="default"
           variant="default"
           onConfirm={() => {
           onConfirm={() => {
             controlMutation.mutate('on');
             controlMutation.mutate('on');
@@ -479,9 +481,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power Off Confirmation */}
       {/* Power Off Confirmation */}
       {showPowerOffConfirm && (
       {showPowerOffConfirm && (
         <ConfirmModal
         <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.`}
           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"
           variant="danger"
           onConfirm={() => {
           onConfirm={() => {
             controlMutation.mutate('off');
             controlMutation.mutate('off');

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

@@ -1021,6 +1021,60 @@ export default {
       virtualPrinter: 'Virtueller Drucker',
       virtualPrinter: 'Virtueller Drucker',
       users: 'Benutzer',
       users: 'Benutzer',
       backup: 'Sicherung',
       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',
     appearance: 'Erscheinungsbild',
     notifications: 'Benachrichtigungen',
     notifications: 'Benachrichtigungen',
@@ -1439,6 +1493,8 @@ export default {
     subtitle: 'Melden Sie sich bei Ihrem Konto an',
     subtitle: 'Melden Sie sich bei Ihrem Konto an',
     username: 'Benutzername',
     username: 'Benutzername',
     usernamePlaceholder: 'Benutzername eingeben',
     usernamePlaceholder: 'Benutzername eingeben',
+    usernameOrEmail: 'Benutzername oder E-Mail',
+    usernameOrEmailPlaceholder: 'Benutzername oder @ E-Mail',
     password: 'Passwort',
     password: 'Passwort',
     passwordPlaceholder: 'Passwort eingeben',
     passwordPlaceholder: 'Passwort eingeben',
     signIn: 'Anmelden',
     signIn: 'Anmelden',
@@ -1449,6 +1505,12 @@ export default {
     enterCredentials: 'Bitte Benutzername und Passwort eingeben',
     enterCredentials: 'Bitte Benutzername und Passwort eingeben',
     forgotPasswordTitle: 'Passwort vergessen',
     forgotPasswordTitle: 'Passwort vergessen',
     forgotPasswordMessage: 'Wenn Sie Ihr Passwort vergessen haben, wenden Sie sich bitte an Ihren Systemadministrator.',
     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:',
     howToReset: 'So setzen Sie Ihr Passwort zurück:',
     resetStep1: 'Kontaktieren Sie Ihren Bambuddy-Administrator',
     resetStep1: 'Kontaktieren Sie Ihren Bambuddy-Administrator',
     resetStep2: 'Bitten Sie ihn, Ihr Passwort in der Benutzerverwaltung zurückzusetzen',
     resetStep2: 'Bitten Sie ihn, Ihr Passwort in der Benutzerverwaltung zurückzusetzen',
@@ -1614,10 +1676,13 @@ export default {
       creating: 'Erstellen...',
       creating: 'Erstellen...',
       saving: 'Speichern...',
       saving: 'Speichern...',
       saveChanges: 'Änderungen speichern',
       saveChanges: 'Änderungen speichern',
+      advancedAuthSubtitle: 'mit erweiterter Authentifizierung',
     },
     },
     form: {
     form: {
       username: 'Benutzername',
       username: 'Benutzername',
       usernamePlaceholder: 'Benutzernamen eingeben',
       usernamePlaceholder: 'Benutzernamen eingeben',
+      email: 'E-Mail',
+      emailPlaceholder: 'benutzer@beispiel.de',
       password: 'Passwort',
       password: 'Passwort',
       passwordPlaceholder: 'Passwort eingeben',
       passwordPlaceholder: 'Passwort eingeben',
       confirmPassword: 'Passwort bestätigen',
       confirmPassword: 'Passwort bestätigen',
@@ -1626,6 +1691,11 @@ export default {
       confirmNewPasswordPlaceholder: 'Neues Passwort bestätigen',
       confirmNewPasswordPlaceholder: 'Neues Passwort bestätigen',
       leaveBlankToKeep: 'leer lassen, um das aktuelle zu behalten',
       leaveBlankToKeep: 'leer lassen, um das aktuelle zu behalten',
       groups: 'Gruppen',
       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: {
     deleteModal: {
       title: 'Benutzer löschen',
       title: 'Benutzer löschen',
@@ -2310,6 +2380,9 @@ export default {
     noPrintersAvailable: 'Keine Drucker verfügbar',
     noPrintersAvailable: 'Keine Drucker verfügbar',
     printerBusy: 'Drucker ist beschäftigt',
     printerBusy: 'Drucker ist beschäftigt',
     printerOffline: 'Drucker ist offline',
     printerOffline: 'Drucker ist offline',
+    sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
+    filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
+    openCalendar: 'Kalender öffnen',
   },
   },
 
 
   // Backup
   // Backup
@@ -2664,4 +2737,348 @@ export default {
     replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
     replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
     lubricateLeftNozzleRail: 'Linke Düsenschiene schmieren (H2-Serie)',
     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',
       virtualPrinter: 'Virtual Printer',
       users: 'Users',
       users: 'Users',
       backup: 'Backup',
       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',
     appearance: 'Appearance',
     notifications: 'Notifications',
     notifications: 'Notifications',
@@ -1439,6 +1493,8 @@ export default {
     subtitle: 'Sign in to your account',
     subtitle: 'Sign in to your account',
     username: 'Username',
     username: 'Username',
     usernamePlaceholder: 'Enter your username',
     usernamePlaceholder: 'Enter your username',
+    usernameOrEmail: 'Username or Email',
+    usernameOrEmailPlaceholder: 'Username or @ Email',
     password: 'Password',
     password: 'Password',
     passwordPlaceholder: 'Enter your password',
     passwordPlaceholder: 'Enter your password',
     signIn: 'Sign in',
     signIn: 'Sign in',
@@ -1449,6 +1505,12 @@ export default {
     enterCredentials: 'Please enter username and password',
     enterCredentials: 'Please enter username and password',
     forgotPasswordTitle: 'Forgot Password',
     forgotPasswordTitle: 'Forgot Password',
     forgotPasswordMessage: "If you've forgotten your password, please contact your system administrator to reset it.",
     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:',
     howToReset: 'How to reset your password:',
     resetStep1: 'Contact your Bambuddy administrator',
     resetStep1: 'Contact your Bambuddy administrator',
     resetStep2: 'Ask them to reset your password in User Management',
     resetStep2: 'Ask them to reset your password in User Management',
@@ -1614,10 +1676,13 @@ export default {
       creating: 'Creating...',
       creating: 'Creating...',
       saving: 'Saving...',
       saving: 'Saving...',
       saveChanges: 'Save Changes',
       saveChanges: 'Save Changes',
+      advancedAuthSubtitle: 'with Advanced Authentication',
     },
     },
     form: {
     form: {
       username: 'Username',
       username: 'Username',
       usernamePlaceholder: 'Enter username',
       usernamePlaceholder: 'Enter username',
+      email: 'Email',
+      emailPlaceholder: 'user@example.com',
       password: 'Password',
       password: 'Password',
       passwordPlaceholder: 'Enter password',
       passwordPlaceholder: 'Enter password',
       confirmPassword: 'Confirm Password',
       confirmPassword: 'Confirm Password',
@@ -1626,6 +1691,11 @@ export default {
       confirmNewPasswordPlaceholder: 'Confirm new password',
       confirmNewPasswordPlaceholder: 'Confirm new password',
       leaveBlankToKeep: 'leave blank to keep current',
       leaveBlankToKeep: 'leave blank to keep current',
       groups: 'Groups',
       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: {
     deleteModal: {
       title: 'Delete User',
       title: 'Delete User',
@@ -2310,6 +2380,9 @@ export default {
     noPrintersAvailable: 'No printers available',
     noPrintersAvailable: 'No printers available',
     printerBusy: 'Printer is busy',
     printerBusy: 'Printer is busy',
     printerOffline: 'Printer is offline',
     printerOffline: 'Printer is offline',
+    sameTypeDifferentColor: 'Same type, different color',
+    filamentTypeNotLoaded: 'Filament type not loaded',
+    openCalendar: 'Open calendar',
   },
   },
 
 
   // Backup
   // Backup
@@ -2362,6 +2435,7 @@ export default {
   },
   },
 
 
   // Edit archive modal
   // Edit archive modal
+  // Edit Archive Modal
   editArchive: {
   editArchive: {
     title: 'Edit Archive',
     title: 'Edit Archive',
     name: 'Name',
     name: 'Name',
@@ -2664,4 +2738,348 @@ export default {
     replaceCarbonFilter: 'Replace activated carbon filter',
     replaceCarbonFilter: 'Replace activated carbon filter',
     lubricateLeftNozzleRail: 'Lubricate left nozzle rail (H2 series)',
     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: 'フィラメント',
       filament: 'フィラメント',
       network: 'ネットワーク',
       network: 'ネットワーク',
       virtualPrinter: '仮想プリンター',
       virtualPrinter: '仮想プリンター',
+      globalEmail: 'グローバルメール',
     },
     },
     appearance: '外観',
     appearance: '外観',
     notifications: '通知',
     notifications: '通知',
@@ -1132,6 +1133,55 @@ export default {
       turnOn: '電源オン',
       turnOn: '電源オン',
       turnOff: '電源オフ',
       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: 'デフォルトなし(毎回選択)',
     noDefaultPrinter: 'デフォルトなし(毎回選択)',
     sidebarOrder: 'サイドバーの順序',
     sidebarOrder: 'サイドバーの順序',
     saveThumbnails: 'サムネイルを保存',
     saveThumbnails: 'サムネイルを保存',
@@ -1451,6 +1501,15 @@ export default {
     resetStep3: '管理者が新しい仮パスワードを設定',
     resetStep3: '管理者が新しい仮パスワードを設定',
     resetStep4: '新しいパスワードでログインし、設定で変更',
     resetStep4: '新しいパスワードでログインし、設定で変更',
     gotIt: '了解',
     gotIt: '了解',
+    // Advanced Auth (email-based login/reset)
+    usernameOrEmail: 'ユーザー名またはメール',
+    usernameOrEmailPlaceholder: 'ユーザー名または @ メール',
+    forgotPasswordEmailMessage: 'メールアドレスを入力すると、新しいパスワードを送信します。',
+    emailAddress: 'メールアドレス',
+    emailPlaceholder: 'your.email@example.com',
+    cancel: 'キャンセル',
+    sending: '送信中...',
+    sendResetEmail: 'リセットメールを送信',
   },
   },
   setup: {
   setup: {
     title: 'Bambuddy セットアップ',
     title: 'Bambuddy セットアップ',
@@ -1591,6 +1650,7 @@ export default {
       creating: '作成中...',
       creating: '作成中...',
       saving: '保存中...',
       saving: '保存中...',
       saveChanges: '変更を保存',
       saveChanges: '変更を保存',
+      advancedAuthSubtitle: '高度な認証を使用',
     },
     },
     form: {
     form: {
       username: 'ユーザー名',
       username: 'ユーザー名',
@@ -1603,11 +1663,18 @@ export default {
       usernamePlaceholder: 'ユーザー名を入力',
       usernamePlaceholder: 'ユーザー名を入力',
       passwordPlaceholder: 'パスワードを入力',
       passwordPlaceholder: 'パスワードを入力',
       newPasswordPlaceholder: '新しいパスワードを入力',
       newPasswordPlaceholder: '新しいパスワードを入力',
+      email: 'メール',
+      emailPlaceholder: 'user@example.com',
+      optional: 'オプション',
+      autoGeneratedPassword: '安全なパスワードが自動的に生成され、ユーザーにメールで送信されます。',
+      passwordManagedByAdvancedAuth: 'パスワードは高度な認証によって管理されています。「パスワードのリセット」を使用して、メールで新しいパスワードをユーザーに送信してください。',
+      resetPassword: 'パスワードのリセット',
+      resettingPassword: 'パスワードをリセット中...',
     },
     },
     deleteModal: {
     deleteModal: {
-      title: 'ユーザー管理',
-      confirm: '確認',
-      message: 'このユーザーを削除しますか?この操作は元に戻せません。',
+      title: 'ユーザーを削除',
+      confirm: 'ユーザーを削除',
+      message: 'このユーザーを削除してもよろしいですか?この操作は元に戻せません。',
     },
     },
     subtitle: 'ユーザーとBambuddyインスタンスへのアクセスを管理',
     subtitle: 'ユーザーとBambuddyインスタンスへのアクセスを管理',
     active: 'アクティブ',
     active: 'アクティブ',
@@ -2590,4 +2657,269 @@ export default {
     collectItem11: 'データベース健全性チェック',
     collectItem11: 'データベース健全性チェック',
     collectItem12: 'Docker環境の詳細',
     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 { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
 import { useToast } from '../contexts/ToastContext';
 import { useToast } from '../contexts/ToastContext';
 import { useTheme } from '../contexts/ThemeContext';
 import { useTheme } from '../contexts/ThemeContext';
-import { HelpCircle, X } from 'lucide-react';
+import { X, Mail } from 'lucide-react';
+import { api } from '../api/client';
+import { Card, CardHeader, CardContent } from '../components/Card';
+import { Button } from '../components/Button';
 
 
 export function LoginPage() {
 export function LoginPage() {
   const navigate = useNavigate();
   const navigate = useNavigate();
@@ -16,6 +19,13 @@ export function LoginPage() {
   const [username, setUsername] = useState('');
   const [username, setUsername] = useState('');
   const [password, setPassword] = useState('');
   const [password, setPassword] = useState('');
   const [showForgotPassword, setShowForgotPassword] = useState(false);
   const [showForgotPassword, setShowForgotPassword] = useState(false);
+  const [forgotEmail, setForgotEmail] = useState('');
+
+  // Check if advanced auth is enabled
+  const { data: advancedAuthStatus } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
 
 
   const loginMutation = useMutation({
   const loginMutation = useMutation({
     mutationFn: () => login(username, password),
     mutationFn: () => login(username, password),
@@ -28,6 +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) => {
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     if (!username || !password) {
     if (!username || !password) {
@@ -37,6 +59,15 @@ export function LoginPage() {
     loginMutation.mutate();
     loginMutation.mutate();
   };
   };
 
 
+  const handleForgotPassword = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!forgotEmail) {
+      showToast('Please enter your email address', 'error');
+      return;
+    }
+    forgotPasswordMutation.mutate(forgotEmail);
+  };
+
   return (
   return (
     <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
     <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
       <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
       <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
@@ -60,7 +91,9 @@ export function LoginPage() {
           <div className="space-y-4">
           <div className="space-y-4">
             <div>
             <div>
               <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
               <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
-                {t('login.username')}
+                {advancedAuthStatus?.advanced_auth_enabled 
+                  ? t('login.usernameOrEmail') || 'Username or Email'
+                  : t('login.username')}
               </label>
               </label>
               <input
               <input
                 id="username"
                 id="username"
@@ -69,7 +102,9 @@ export function LoginPage() {
                 value={username}
                 value={username}
                 onChange={(e) => setUsername(e.target.value)}
                 onChange={(e) => setUsername(e.target.value)}
                 className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
                 className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                placeholder={t('login.usernamePlaceholder')}
+                placeholder={advancedAuthStatus?.advanced_auth_enabled 
+                  ? t('login.usernameOrEmailPlaceholder') || 'Enter your username or email'
+                  : t('login.usernamePlaceholder')}
                 autoComplete="username"
                 autoComplete="username"
               />
               />
             </div>
             </div>
@@ -101,64 +136,120 @@ export function LoginPage() {
             </button>
             </button>
           </div>
           </div>
 
 
-          <div className="text-center">
-            <button
-              type="button"
-              onClick={() => setShowForgotPassword(true)}
-              className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
-            >
-              {t('login.forgotPassword')}
-            </button>
-          </div>
+          {advancedAuthStatus?.advanced_auth_enabled && (
+            <div className="text-center">
+              <button
+                type="button"
+                onClick={() => setShowForgotPassword(true)}
+                className="text-sm text-bambu-gray hover:text-bambu-green transition-colors"
+              >
+                {t('login.forgotPassword')}
+              </button>
+            </div>
+          )}
         </form>
         </form>
       </div>
       </div>
 
 
       {/* Forgot Password Modal */}
       {/* Forgot Password Modal */}
       {showForgotPassword && (
       {showForgotPassword && (
         <div
         <div
-          className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
+          className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           onClick={() => setShowForgotPassword(false)}
           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>
               </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>
       )}
       )}
     </div>
     </div>

+ 203 - 53
frontend/src/pages/SettingsPage.tsx

@@ -1,5 +1,5 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save } from 'lucide-react';
+import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save, Mail } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { api } from '../api/client';
 import { api } from '../api/client';
@@ -15,10 +15,12 @@ import { AddNotificationModal } from '../components/AddNotificationModal';
 import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor';
 import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor';
 import { NotificationLogViewer } from '../components/NotificationLogViewer';
 import { NotificationLogViewer } from '../components/NotificationLogViewer';
 import { ConfirmModal } from '../components/ConfirmModal';
 import { ConfirmModal } from '../components/ConfirmModal';
+import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal';
 import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { ExternalLinksSettings } from '../components/ExternalLinksSettings';
 import { ExternalLinksSettings } from '../components/ExternalLinksSettings';
 import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings';
 import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
 import { GitHubBackupSettings } from '../components/GitHubBackupSettings';
+import { EmailSettings } from '../components/EmailSettings';
 import { APIBrowser } from '../components/APIBrowser';
 import { APIBrowser } from '../components/APIBrowser';
 import { virtualPrinterApi } from '../api/client';
 import { virtualPrinterApi } from '../api/client';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
@@ -28,7 +30,7 @@ import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, t
 import { useState, useEffect, useRef, useCallback } from 'react';
 import { useState, useEffect, useRef, useCallback } from 'react';
 import { Palette } from 'lucide-react';
 import { Palette } from 'lucide-react';
 
 
-const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const;
+const validTabs = ['general', 'network', 'plugs', 'email', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const;
 type TabType = typeof validTabs[number];
 type TabType = typeof validTabs[number];
 
 
 export function SettingsPage() {
 export function SettingsPage() {
@@ -99,13 +101,15 @@ export function SettingsPage() {
   const [deleteUserLoading, setDeleteUserLoading] = useState(false);
   const [deleteUserLoading, setDeleteUserLoading] = useState(false);
   const [userFormData, setUserFormData] = useState<{
   const [userFormData, setUserFormData] = useState<{
     username: string;
     username: string;
-    password: string;
+    password?: string;
+    email?: string;
     confirmPassword: string;
     confirmPassword: string;
     role: string;
     role: string;
     group_ids: number[];
     group_ids: number[];
   }>({
   }>({
     username: '',
     username: '',
     password: '',
     password: '',
+    email: '',
     confirmPassword: '',
     confirmPassword: '',
     role: 'user',
     role: 'user',
     group_ids: [],
     group_ids: [],
@@ -307,6 +311,12 @@ export function SettingsPage() {
     queryFn: api.getCloudStatus,
     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
   // User management queries and mutations
   const { hasPermission } = useAuth();
   const { hasPermission } = useAuth();
 
 
@@ -334,7 +344,7 @@ export function SettingsPage() {
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowCreateUserModal(false);
       setShowCreateUserModal(false);
-      setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('settings.toast.userCreated'));
       showToast(t('settings.toast.userCreated'));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
@@ -349,7 +359,7 @@ export function SettingsPage() {
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowEditUserModal(false);
       setShowEditUserModal(false);
       setEditingUserId(null);
       setEditingUserId(null);
-      setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('settings.toast.userUpdated'));
       showToast(t('settings.toast.userUpdated'));
     },
     },
     onError: (error: Error) => {
     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
   // Function to initiate user deletion with item count check
   const handleDeleteUserClick = async (userId: number) => {
   const handleDeleteUserClick = async (userId: number) => {
     setDeleteUserId(userId);
     setDeleteUserId(userId);
@@ -424,21 +444,40 @@ export function SettingsPage() {
 
 
   // User management handlers
   // User management handlers
   const handleCreateUser = () => {
   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');
       showToast(t('settings.toast.fillRequiredFields'), 'error');
       return;
       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;
       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({
     createUserMutation.mutate({
       username: userFormData.username,
       username: userFormData.username,
-      password: userFormData.password,
+      password: advancedAuthEnabled ? undefined : userFormData.password,
+      email: userFormData.email || undefined,
       role: userFormData.role,
       role: userFormData.role,
       group_ids: userFormData.group_ids.length > 0 ? userFormData.group_ids : undefined,
       group_ids: userFormData.group_ids.length > 0 ? userFormData.group_ids : undefined,
     });
     });
@@ -472,6 +511,7 @@ export function SettingsPage() {
     setUserFormData({
     setUserFormData({
       username: userToEdit.username,
       username: userToEdit.username,
       password: '',
       password: '',
+      email: userToEdit.email || '',
       confirmPassword: '',
       confirmPassword: '',
       role: userToEdit.role,
       role: userToEdit.role,
       group_ids: userToEdit.groups?.map(g => g.id) || [],
       group_ids: userToEdit.groups?.map(g => g.id) || [],
@@ -919,7 +959,7 @@ export function SettingsPage() {
       </div>
       </div>
 
 
       {/* Tab Navigation */}
       {/* 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
         <button
           onClick={() => handleTabChange('general')}
           onClick={() => handleTabChange('general')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
@@ -946,6 +986,17 @@ export function SettingsPage() {
             </span>
             </span>
           )}
           )}
         </button>
         </button>
+        <button
+          onClick={() => handleTabChange('email')}
+          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
+            activeTab === 'email'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray hover:text-gray-900 dark:hover:text-white border-transparent'
+          }`}
+        >
+          <Mail className="w-4 h-4" />
+          {t('settings.tabs.globalEmail') || 'Global Email'}
+        </button>
         <button
         <button
           onClick={() => handleTabChange('notifications')}
           onClick={() => handleTabChange('notifications')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
@@ -3456,6 +3507,25 @@ export function SettingsPage() {
             </CardContent>
             </CardContent>
           </Card>
           </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 && (
           {authEnabled && (
             <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
             <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
               {/* Left Column: Current User + User List */}
               {/* Left Column: Current User + User List */}
@@ -3521,7 +3591,7 @@ export function SettingsPage() {
                           size="sm"
                           size="sm"
                           onClick={() => {
                           onClick={() => {
                             setShowCreateUserModal(true);
                             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" />
                           <Plus className="w-4 h-4" />
@@ -3707,12 +3777,12 @@ export function SettingsPage() {
       )}
       )}
 
 
       {/* Create User Modal */}
       {/* Create User Modal */}
-      {showCreateUserModal && (
+      {showCreateUserModal && !advancedAuthStatus?.advanced_auth_enabled && (
         <div
         <div
           className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
           className="fixed inset-0 bg-black flex items-center justify-center z-50 p-4"
           onClick={() => {
           onClick={() => {
             setShowCreateUserModal(false);
             setShowCreateUserModal(false);
-            setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Card
           <Card
@@ -3730,7 +3800,7 @@ export function SettingsPage() {
                   size="sm"
                   size="sm"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateUserModal(false);
                     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" />
                   <X className="w-5 h-5" />
@@ -3812,7 +3882,7 @@ export function SettingsPage() {
                   variant="secondary"
                   variant="secondary"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateUserModal(false);
                     setShowCreateUserModal(false);
-                    setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                   }}
                 >
                 >
                   Cancel
                   Cancel
@@ -3839,6 +3909,22 @@ export function SettingsPage() {
         </div>
         </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 */}
       {/* Edit User Modal */}
       {showEditUserModal && editingUserId !== null && (
       {showEditUserModal && editingUserId !== null && (
         <div
         <div
@@ -3846,7 +3932,7 @@ export function SettingsPage() {
           onClick={() => {
           onClick={() => {
             setShowEditUserModal(false);
             setShowEditUserModal(false);
             setEditingUserId(null);
             setEditingUserId(null);
-            setUserFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Card
           <Card
@@ -3865,7 +3951,7 @@ export function SettingsPage() {
                   onClick={() => {
                   onClick={() => {
                     setShowEditUserModal(false);
                     setShowEditUserModal(false);
                     setEditingUserId(null);
                     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" />
                   <X className="w-5 h-5" />
@@ -3874,8 +3960,11 @@ export function SettingsPage() {
             </CardHeader>
             </CardHeader>
             <CardContent>
             <CardContent>
               <div className="space-y-4">
               <div className="space-y-4">
+                {/* Username Field */}
                 <div>
                 <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
                   <input
                     type="text"
                     type="text"
                     value={userFormData.username}
                     value={userFormData.username}
@@ -3885,43 +3974,94 @@ export function SettingsPage() {
                     autoComplete="username"
                     autoComplete="username"
                   />
                   />
                 </div>
                 </div>
+
+                {/* Email Field */}
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <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>
                   </label>
                   <input
                   <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"
                     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>
                 </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>
                   </div>
                 )}
                 )}
+
+                {/* Groups Field */}
                 <div>
                 <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">
                   <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 => (
                     {groupsData.map(group => (
                       <label
                       <label
@@ -3936,7 +4076,7 @@ export function SettingsPage() {
                         />
                         />
                         <span className="text-sm text-white">{group.name}</span>
                         <span className="text-sm text-white">{group.name}</span>
                         {group.is_system && (
                         {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>
                       </label>
                     ))}
                     ))}
@@ -3949,24 +4089,29 @@ export function SettingsPage() {
                   onClick={() => {
                   onClick={() => {
                     setShowEditUserModal(false);
                     setShowEditUserModal(false);
                     setEditingUserId(null);
                     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>
                 <Button
                 <Button
                   onClick={() => handleUpdateUser(editingUserId)}
                   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 ? (
                   {updateUserMutation.isPending ? (
                     <>
                     <>
                       <Loader2 className="w-4 h-4 animate-spin" />
                       <Loader2 className="w-4 h-4 animate-spin" />
-                      Saving...
+                      {t('users.modal.saving') || 'Saving...'}
                     </>
                     </>
                   ) : (
                   ) : (
                     <>
                     <>
                       <Save className="w-4 h-4" />
                       <Save className="w-4 h-4" />
-                      Save Changes
+                      {t('users.modal.saveChanges') || 'Save Changes'}
                     </>
                     </>
                   )}
                   )}
                 </Button>
                 </Button>
@@ -4249,6 +4394,11 @@ export function SettingsPage() {
         />
         />
       )}
       )}
 
 
+      {/* Email Tab */}
+      {activeTab === 'email' && (
+        <EmailSettings />
+      )}
+
       {/* Backup Tab */}
       {/* Backup Tab */}
       {activeTab === 'backup' && (
       {activeTab === 'backup' && (
         <GitHubBackupSettings />
         <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 { useNavigate } from 'react-router-dom';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft } from 'lucide-react';
+import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft, RotateCcw } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { UserCreate, UserUpdate, UserResponse } from '../api/client';
 import type { UserCreate, UserUpdate, UserResponse } from '../api/client';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
@@ -10,10 +10,12 @@ import { useToast } from '../contexts/ToastContext';
 import { Button } from '../components/Button';
 import { Button } from '../components/Button';
 import { Card, CardContent, CardHeader } from '../components/Card';
 import { Card, CardContent, CardHeader } from '../components/Card';
 import { ConfirmModal } from '../components/ConfirmModal';
 import { ConfirmModal } from '../components/ConfirmModal';
+import { CreateUserAdvancedAuthModal } from '../components/CreateUserAdvancedAuthModal';
 
 
 interface FormData extends UserCreate {
 interface FormData extends UserCreate {
   group_ids: number[];
   group_ids: number[];
   confirmPassword: string;
   confirmPassword: string;
+  email?: string;
 }
 }
 
 
 export function UsersPage() {
 export function UsersPage() {
@@ -29,23 +31,30 @@ export function UsersPage() {
   const [formData, setFormData] = useState<FormData>({
   const [formData, setFormData] = useState<FormData>({
     username: '',
     username: '',
     password: '',
     password: '',
+    email: '',
     confirmPassword: '',
     confirmPassword: '',
     role: 'user',
     role: 'user',
     group_ids: [],
     group_ids: [],
   });
   });
 
 
+  // Check if advanced auth is enabled
+  const { data: advancedAuthStatus = { advanced_auth_enabled: false, smtp_configured: false } } = useQuery({
+    queryKey: ['advancedAuthStatus'],
+    queryFn: () => api.getAdvancedAuthStatus(),
+  });
+
   // Close modal on Escape key
   // Close modal on Escape key
   useEffect(() => {
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if (e.key === 'Escape') {
       if (e.key === 'Escape') {
         if (showCreateModal) {
         if (showCreateModal) {
           setShowCreateModal(false);
           setShowCreateModal(false);
-          setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+          setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
         }
         }
         if (showEditModal) {
         if (showEditModal) {
           setShowEditModal(false);
           setShowEditModal(false);
           setEditingUserId(null);
           setEditingUserId(null);
-          setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+          setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
         }
         }
       }
       }
     };
     };
@@ -71,7 +80,7 @@ export function UsersPage() {
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['users'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowCreateModal(false);
       setShowCreateModal(false);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.created'));
       showToast(t('users.toast.created'));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
@@ -86,7 +95,7 @@ export function UsersPage() {
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       queryClient.invalidateQueries({ queryKey: ['groups'] });
       setShowEditModal(false);
       setShowEditModal(false);
       setEditingUserId(null);
       setEditingUserId(null);
-      setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+      setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
       showToast(t('users.toast.updated'));
       showToast(t('users.toast.updated'));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
@@ -105,22 +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 = () => {
   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;
       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;
       return;
     }
     }
-    if (formData.password.length < 6) {
-      showToast(t('users.toast.passwordTooShort'), 'error');
-      return;
+    
+    // Password validation only when advanced auth is disabled
+    if (!advancedAuthEnabled) {
+      if (!formData.password) {
+        showToast(t('users.toast.fillRequired'), 'error');
+        return;
+      }
+      if (formData.password !== formData.confirmPassword) {
+        showToast(t('users.toast.passwordsDoNotMatch'), 'error');
+        return;
+      }
+      if (formData.password.length < 6) {
+        showToast(t('users.toast.passwordTooShort'), 'error');
+        return;
+      }
     }
     }
+    
     createMutation.mutate({
     createMutation.mutate({
       username: formData.username,
       username: formData.username,
-      password: formData.password,
+      password: advancedAuthEnabled ? undefined : formData.password,
+      email: formData.email || undefined,
       role: formData.role,
       role: formData.role,
       group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
       group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined,
     });
     });
@@ -141,6 +205,7 @@ export function UsersPage() {
     const updateData: UserUpdate = {
     const updateData: UserUpdate = {
       username: formData.username || undefined,
       username: formData.username || undefined,
       password: formData.password || undefined,
       password: formData.password || undefined,
+      email: formData.email || undefined,
       role: formData.role,
       role: formData.role,
       group_ids: formData.group_ids,
       group_ids: formData.group_ids,
     };
     };
@@ -148,6 +213,10 @@ export function UsersPage() {
     if (!updateData.password) {
     if (!updateData.password) {
       delete updateData.password;
       delete updateData.password;
     }
     }
+    // Remove email if empty
+    if (!updateData.email) {
+      delete updateData.email;
+    }
     updateMutation.mutate({ id, data: updateData });
     updateMutation.mutate({ id, data: updateData });
   };
   };
 
 
@@ -160,6 +229,7 @@ export function UsersPage() {
     setFormData({
     setFormData({
       username: user.username,
       username: user.username,
       password: '',
       password: '',
+      email: user.email || '',
       confirmPassword: '',
       confirmPassword: '',
       role: user.role,
       role: user.role,
       group_ids: user.groups?.map(g => g.id) || [],
       group_ids: user.groups?.map(g => g.id) || [],
@@ -170,7 +240,7 @@ export function UsersPage() {
   const closeEditModal = () => {
   const closeEditModal = () => {
     setShowEditModal(false);
     setShowEditModal(false);
     setEditingUserId(null);
     setEditingUserId(null);
-    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
   };
   };
 
 
   const toggleGroup = (groupId: number) => {
   const toggleGroup = (groupId: number) => {
@@ -221,7 +291,7 @@ export function UsersPage() {
         <Button
         <Button
           onClick={() => {
           onClick={() => {
             setShowCreateModal(true);
             setShowCreateModal(true);
-            setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Plus className="w-4 h-4" />
           <Plus className="w-4 h-4" />
@@ -316,6 +386,17 @@ export function UsersPage() {
                             {t('users.delete')}
                             {t('users.delete')}
                           </Button>
                           </Button>
                         )}
                         )}
+                        {advancedAuthStatus?.advanced_auth_enabled && user.email && user.id !== currentUser?.id && (
+                          <Button
+                            size="sm"
+                            variant="ghost"
+                            onClick={() => resetPasswordMutation.mutate(user.id)}
+                            disabled={resetPasswordMutation.isPending}
+                          >
+                            <RotateCcw className="w-4 h-4" />
+                            {t('users.form.resetPassword') || 'Reset Password'}
+                          </Button>
+                        )}
                       </div>
                       </div>
                     </td>
                     </td>
                   </tr>
                   </tr>
@@ -327,12 +408,12 @@ export function UsersPage() {
       )}
       )}
 
 
       {/* Create User Modal */}
       {/* Create User Modal */}
-      {showCreateModal && (
+      {showCreateModal && !advancedAuthStatus?.advanced_auth_enabled && (
         <div
         <div
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
           onClick={() => {
           onClick={() => {
             setShowCreateModal(false);
             setShowCreateModal(false);
-            setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+            setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
           }}
           }}
         >
         >
           <Card
           <Card
@@ -350,7 +431,7 @@ export function UsersPage() {
                   size="sm"
                   size="sm"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateModal(false);
                     setShowCreateModal(false);
-                    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                   }}
                 >
                 >
                   <X className="w-5 h-5" />
                   <X className="w-5 h-5" />
@@ -440,14 +521,14 @@ export function UsersPage() {
                   variant="secondary"
                   variant="secondary"
                   onClick={() => {
                   onClick={() => {
                     setShowCreateModal(false);
                     setShowCreateModal(false);
-                    setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] });
+                    setFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
                   }}
                   }}
                 >
                 >
                   {t('users.modal.cancel')}
                   {t('users.modal.cancel')}
                 </Button>
                 </Button>
                 <Button
                 <Button
                   onClick={handleCreate}
                   onClick={handleCreate}
-                  disabled={createMutation.isPending || !formData.username || !formData.password || formData.password !== formData.confirmPassword || formData.password.length < 6}
+                  disabled={isCreateButtonDisabled}
                 >
                 >
                   {createMutation.isPending ? (
                   {createMutation.isPending ? (
                     <>
                     <>
@@ -467,6 +548,22 @@ export function UsersPage() {
         </div>
         </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 */}
       {/* Edit User Modal */}
       {showEditModal && editingUserId !== null && (
       {showEditModal && editingUserId !== null && (
         <div
         <div
@@ -507,6 +604,18 @@ export function UsersPage() {
                     autoComplete="username"
                     autoComplete="username"
                   />
                   />
                 </div>
                 </div>
+                <div>
+                  <label className="block text-sm font-medium text-white mb-2">
+                    {t('users.form.email') || 'Email'} <span className="text-bambu-gray font-normal">({t('users.form.optional') || 'optional'})</span>
+                  </label>
+                  <input
+                    type="email"
+                    value={formData.email}
+                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                    className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
+                    placeholder={t('users.form.emailPlaceholder') || 'user@example.com'}
+                  />
+                </div>
                 <div>
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">
                   <label className="block text-sm font-medium text-white mb-2">
                     {t('users.form.password')} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep')})</span>
                     {t('users.form.password')} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep')})</span>

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


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


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