mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Merge branch 'feature_user_authentication' of https://github.com/cadtoolbox/bambuddy into test-merge
# Conflicts: # frontend/src/components/ConfigureAmsSlotModal.tsx # frontend/src/i18n/locales/ja.ts # static/index.html
This commit is contained in:
@@ -5,19 +5,43 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from backend.app.api.routes.settings import get_external_login_url
|
||||
from backend.app.core.auth import (
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
authenticate_user,
|
||||
authenticate_user_by_email,
|
||||
create_access_token,
|
||||
get_current_active_user,
|
||||
get_password_hash,
|
||||
get_user_by_email,
|
||||
get_user_by_username,
|
||||
)
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.models.group import Group
|
||||
from backend.app.models.settings import Settings
|
||||
from backend.app.models.user import User
|
||||
from backend.app.schemas.auth import GroupBrief, LoginRequest, LoginResponse, SetupRequest, SetupResponse, UserResponse
|
||||
from backend.app.schemas.auth import (
|
||||
ForgotPasswordRequest,
|
||||
ForgotPasswordResponse,
|
||||
GroupBrief,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
ResetPasswordRequest,
|
||||
ResetPasswordResponse,
|
||||
SetupRequest,
|
||||
SetupResponse,
|
||||
SMTPSettings,
|
||||
TestSMTPRequest,
|
||||
TestSMTPResponse,
|
||||
UserResponse,
|
||||
)
|
||||
from backend.app.services.email_service import (
|
||||
create_password_reset_email_from_template,
|
||||
generate_secure_password,
|
||||
get_smtp_settings,
|
||||
save_smtp_settings,
|
||||
send_email,
|
||||
)
|
||||
|
||||
|
||||
def _user_to_response(user: User) -> UserResponse:
|
||||
@@ -25,6 +49,7 @@ def _user_to_response(user: User) -> UserResponse:
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
is_admin=user.is_admin,
|
||||
@@ -46,6 +71,27 @@ async def is_auth_enabled(db: AsyncSession) -> bool:
|
||||
return setting.value.lower() == "true"
|
||||
|
||||
|
||||
async def is_advanced_auth_enabled(db: AsyncSession) -> bool:
|
||||
"""Check if advanced authentication is enabled."""
|
||||
result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
|
||||
setting = result.scalar_one_or_none()
|
||||
if setting is None:
|
||||
return False
|
||||
return setting.value.lower() == "true"
|
||||
|
||||
|
||||
async def set_advanced_auth_enabled(db: AsyncSession, enabled: bool) -> None:
|
||||
"""Set advanced authentication enabled status."""
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
|
||||
stmt = sqlite_insert(Settings).values(key="advanced_auth_enabled", value="true" if enabled else "false")
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
|
||||
)
|
||||
await db.execute(stmt)
|
||||
|
||||
|
||||
async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
|
||||
"""Set authentication enabled status."""
|
||||
from sqlalchemy import func
|
||||
@@ -216,7 +262,10 @@ async def disable_auth(
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Login and get access token."""
|
||||
"""Login and get access token.
|
||||
|
||||
Supports username or email-based login. Username lookup is case-insensitive.
|
||||
"""
|
||||
# Check if auth is enabled
|
||||
auth_enabled = await is_auth_enabled(db)
|
||||
if not auth_enabled:
|
||||
@@ -225,7 +274,15 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
detail="Authentication is not enabled",
|
||||
)
|
||||
|
||||
# Try username-based authentication first
|
||||
user = await authenticate_user(db, request.username, request.password)
|
||||
|
||||
# If username auth failed and advanced auth is enabled, try email-based authentication
|
||||
if not user:
|
||||
advanced_auth = await is_advanced_auth_enabled(db)
|
||||
if advanced_auth:
|
||||
user = await authenticate_user_by_email(db, request.username, request.password)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -263,3 +320,332 @@ async def get_current_user_info(
|
||||
async def logout():
|
||||
"""Logout (client should discard token)."""
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
# Advanced Authentication Endpoints
|
||||
|
||||
|
||||
@router.post("/smtp/test", response_model=TestSMTPResponse)
|
||||
async def test_smtp_connection(
|
||||
test_request: TestSMTPRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Test SMTP connection with provided settings (admin only)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
user = result.scalar_one()
|
||||
|
||||
if not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can test SMTP settings",
|
||||
)
|
||||
|
||||
try:
|
||||
smtp_settings = SMTPSettings(
|
||||
smtp_host=test_request.smtp_host,
|
||||
smtp_port=test_request.smtp_port,
|
||||
smtp_username=test_request.smtp_username,
|
||||
smtp_password=test_request.smtp_password,
|
||||
smtp_security=test_request.smtp_security,
|
||||
smtp_auth_enabled=test_request.smtp_auth_enabled,
|
||||
smtp_from_email=test_request.smtp_from_email,
|
||||
)
|
||||
|
||||
# Send test email
|
||||
send_email(
|
||||
smtp_settings=smtp_settings,
|
||||
to_email=test_request.test_recipient,
|
||||
subject="BamBuddy SMTP Test",
|
||||
body_text="This is a test email from BamBuddy. If you received this, your SMTP settings are working correctly!",
|
||||
body_html="<p>This is a test email from <strong>BamBuddy</strong>.</p><p>If you received this, your SMTP settings are working correctly!</p>",
|
||||
)
|
||||
|
||||
logger.info(f"Test email sent successfully to {test_request.test_recipient}")
|
||||
return TestSMTPResponse(success=True, message="Test email sent successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send test email: {e}")
|
||||
return TestSMTPResponse(success=False, message=f"Failed to send test email: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/smtp", response_model=SMTPSettings | None)
|
||||
async def get_smtp_config(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get SMTP settings (admin only). Password is not returned."""
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
user = result.scalar_one()
|
||||
|
||||
if not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can view SMTP settings",
|
||||
)
|
||||
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if smtp_settings:
|
||||
# Don't return password in response
|
||||
smtp_settings.smtp_password = None
|
||||
return smtp_settings
|
||||
|
||||
|
||||
@router.post("/smtp", response_model=dict)
|
||||
async def save_smtp_config(
|
||||
smtp_settings: SMTPSettings,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Save SMTP settings (admin only)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
user = result.scalar_one()
|
||||
|
||||
if not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can update SMTP settings",
|
||||
)
|
||||
|
||||
try:
|
||||
await save_smtp_settings(db, smtp_settings)
|
||||
await db.commit()
|
||||
logger.info(f"SMTP settings updated by admin user: {user.username}")
|
||||
return {"message": "SMTP settings saved successfully"}
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Failed to save SMTP settings: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to save SMTP settings: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/advanced-auth/enable", response_model=dict)
|
||||
async def enable_advanced_auth(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Enable advanced authentication (admin only).
|
||||
|
||||
Requires SMTP settings to be configured and tested first.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
user = result.scalar_one()
|
||||
|
||||
if not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can enable advanced authentication",
|
||||
)
|
||||
|
||||
# Verify SMTP settings are configured
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if not smtp_settings:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="SMTP settings must be configured before enabling advanced authentication",
|
||||
)
|
||||
|
||||
try:
|
||||
await set_advanced_auth_enabled(db, True)
|
||||
await db.commit()
|
||||
logger.info(f"Advanced authentication enabled by admin user: {user.username}")
|
||||
return {"message": "Advanced authentication enabled successfully", "advanced_auth_enabled": True}
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Failed to enable advanced authentication: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to enable advanced authentication: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/advanced-auth/disable", response_model=dict)
|
||||
async def disable_advanced_auth(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Disable advanced authentication (admin only)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
user = result.scalar_one()
|
||||
|
||||
if not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can disable advanced authentication",
|
||||
)
|
||||
|
||||
try:
|
||||
await set_advanced_auth_enabled(db, False)
|
||||
await db.commit()
|
||||
logger.info(f"Advanced authentication disabled by admin user: {user.username}")
|
||||
return {"message": "Advanced authentication disabled successfully", "advanced_auth_enabled": False}
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Failed to disable advanced authentication: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to disable advanced authentication: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/advanced-auth/status")
|
||||
async def get_advanced_auth_status(db: AsyncSession = Depends(get_db)):
|
||||
"""Get advanced authentication status."""
|
||||
advanced_auth_enabled = await is_advanced_auth_enabled(db)
|
||||
smtp_configured = await get_smtp_settings(db) is not None
|
||||
return {
|
||||
"advanced_auth_enabled": advanced_auth_enabled,
|
||||
"smtp_configured": smtp_configured,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/forgot-password", response_model=ForgotPasswordResponse)
|
||||
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Request password reset via email (advanced auth only)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if advanced auth is enabled
|
||||
advanced_auth = await is_advanced_auth_enabled(db)
|
||||
if not advanced_auth:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Advanced authentication is not enabled",
|
||||
)
|
||||
|
||||
# Get SMTP settings
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if not smtp_settings:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Email service is not configured",
|
||||
)
|
||||
|
||||
# Find user by email
|
||||
user = await get_user_by_email(db, request.email)
|
||||
|
||||
# Always return success message to prevent email enumeration
|
||||
# but only send email if user exists
|
||||
if user and user.is_active:
|
||||
try:
|
||||
# Generate new password
|
||||
new_password = generate_secure_password()
|
||||
user.password_hash = get_password_hash(new_password)
|
||||
await db.commit()
|
||||
|
||||
login_url = await get_external_login_url(db)
|
||||
|
||||
# Send password reset email
|
||||
subject, text_body, html_body = await create_password_reset_email_from_template(
|
||||
db, user.username, new_password, login_url
|
||||
)
|
||||
send_email(smtp_settings, user.email, subject, text_body, html_body)
|
||||
|
||||
logger.info(f"Password reset email sent to {user.email}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send password reset email: {e}")
|
||||
# Don't reveal error to user for security
|
||||
|
||||
return ForgotPasswordResponse(
|
||||
message="If the email address is associated with an account, a password reset email has been sent."
|
||||
)
|
||||
|
||||
|
||||
@router.post("/reset-password", response_model=ResetPasswordResponse)
|
||||
async def reset_user_password(
|
||||
request: ResetPasswordRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Reset a user's password and send them an email (admin only, advanced auth only)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Reload user with groups for proper is_admin check
|
||||
result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
|
||||
admin_user = result.scalar_one()
|
||||
|
||||
if not admin_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can reset user passwords",
|
||||
)
|
||||
|
||||
# Check if advanced auth is enabled
|
||||
advanced_auth = await is_advanced_auth_enabled(db)
|
||||
if not advanced_auth:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Advanced authentication is not enabled",
|
||||
)
|
||||
|
||||
# Get SMTP settings
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if not smtp_settings:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Email service is not configured",
|
||||
)
|
||||
|
||||
# Find user to reset
|
||||
result = await db.execute(select(User).where(User.id == request.user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
if not user.email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User does not have an email address configured",
|
||||
)
|
||||
|
||||
try:
|
||||
# Generate new password
|
||||
new_password = generate_secure_password()
|
||||
user.password_hash = get_password_hash(new_password)
|
||||
await db.commit()
|
||||
|
||||
login_url = await get_external_login_url(db)
|
||||
|
||||
# Send password reset email
|
||||
subject, text_body, html_body = await create_password_reset_email_from_template(
|
||||
db, user.username, new_password, login_url
|
||||
)
|
||||
send_email(smtp_settings, user.email, subject, text_body, html_body)
|
||||
|
||||
logger.info(f"Password reset by admin {admin_user.username} for user {user.username}")
|
||||
return ResetPasswordResponse(message=f"Password reset email sent to {user.email}")
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Failed to reset password for user {user.username}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to reset password: {str(e)}",
|
||||
)
|
||||
|
||||
@@ -43,6 +43,9 @@ EVENT_NAMES = {
|
||||
"queue_job_skipped": "Queue Job Skipped",
|
||||
"queue_job_failed": "Queue Job Failed",
|
||||
"queue_completed": "Queue Completed",
|
||||
# User management
|
||||
"user_created": "Welcome Email",
|
||||
"password_reset": "Password Reset",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,27 @@ async def get_setting(db: AsyncSession, key: str) -> str | None:
|
||||
return setting.value if setting else None
|
||||
|
||||
|
||||
async def get_external_login_url(db: AsyncSession) -> str:
|
||||
"""Get the external URL for the login page.
|
||||
|
||||
Uses external_url from settings if available, otherwise falls back to APP_URL env var.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Full URL to the login page
|
||||
"""
|
||||
import os
|
||||
|
||||
external_url = await get_setting(db, "external_url")
|
||||
if external_url:
|
||||
external_url = external_url.rstrip("/")
|
||||
else:
|
||||
external_url = os.environ.get("APP_URL", "http://localhost:5173")
|
||||
return external_url + "/login"
|
||||
|
||||
|
||||
async def set_setting(db: AsyncSession, key: str, value: str) -> None:
|
||||
"""Set a single setting value."""
|
||||
from sqlalchemy import func
|
||||
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from backend.app.api.routes.settings import get_external_login_url
|
||||
from backend.app.core.auth import (
|
||||
RequirePermissionIfAuthEnabled,
|
||||
get_current_user_optional,
|
||||
@@ -15,8 +16,15 @@ from backend.app.models.archive import PrintArchive
|
||||
from backend.app.models.group import Group
|
||||
from backend.app.models.library import LibraryFile
|
||||
from backend.app.models.print_queue import PrintQueueItem
|
||||
from backend.app.models.settings import Settings
|
||||
from backend.app.models.user import User
|
||||
from backend.app.schemas.auth import ChangePasswordRequest, GroupBrief, UserCreate, UserResponse, UserUpdate
|
||||
from backend.app.services.email_service import (
|
||||
create_welcome_email_from_template,
|
||||
generate_secure_password,
|
||||
get_smtp_settings,
|
||||
send_email,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
@@ -26,6 +34,7 @@ def _user_to_response(user: User) -> UserResponse:
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
is_admin=user.is_admin,
|
||||
@@ -54,9 +63,24 @@ async def create_user(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.USERS_CREATE),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a new user."""
|
||||
# Check if username already exists
|
||||
existing_user = await db.execute(select(User).where(User.username == user_data.username))
|
||||
"""Create a new user.
|
||||
|
||||
When advanced authentication is enabled:
|
||||
- Email is required
|
||||
- Password is auto-generated and emailed to user
|
||||
- Admin cannot set or see the password
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if advanced auth is enabled
|
||||
result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
|
||||
advanced_auth_setting = result.scalar_one_or_none()
|
||||
advanced_auth_enabled = advanced_auth_setting and advanced_auth_setting.value.lower() == "true"
|
||||
|
||||
# Check if username already exists (case-insensitive)
|
||||
existing_user = await db.execute(select(User).where(func.lower(User.username) == func.lower(user_data.username)))
|
||||
if existing_user.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -70,9 +94,36 @@ async def create_user(
|
||||
detail="Role must be 'admin' or 'user'",
|
||||
)
|
||||
|
||||
# Advanced auth validation
|
||||
if advanced_auth_enabled:
|
||||
if not user_data.email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email is required when advanced authentication is enabled",
|
||||
)
|
||||
# Check if email already exists (case-insensitive)
|
||||
existing_email = await db.execute(select(User).where(func.lower(User.email) == func.lower(user_data.email)))
|
||||
if existing_email.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already exists",
|
||||
)
|
||||
|
||||
# Generate password if advanced auth enabled, otherwise require password
|
||||
if advanced_auth_enabled:
|
||||
password = generate_secure_password()
|
||||
else:
|
||||
if not user_data.password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Password is required when advanced authentication is disabled",
|
||||
)
|
||||
password = user_data.password
|
||||
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
password_hash=get_password_hash(user_data.password),
|
||||
email=user_data.email,
|
||||
password_hash=get_password_hash(password),
|
||||
role=user_data.role,
|
||||
is_active=True,
|
||||
)
|
||||
@@ -92,6 +143,23 @@ async def create_user(
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
# Send welcome email if advanced auth enabled
|
||||
if advanced_auth_enabled and new_user.email:
|
||||
try:
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if smtp_settings:
|
||||
login_url = await get_external_login_url(db)
|
||||
subject, text_body, html_body = await create_welcome_email_from_template(
|
||||
db, new_user.username, password, login_url
|
||||
)
|
||||
send_email(smtp_settings, new_user.email, subject, text_body, html_body)
|
||||
logger.info(f"Welcome email sent to {new_user.email}")
|
||||
else:
|
||||
logger.warning(f"SMTP not configured, could not send welcome email to {new_user.email}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send welcome email: {e}")
|
||||
# Don't fail user creation if email fails
|
||||
|
||||
return _user_to_response(new_user)
|
||||
|
||||
|
||||
@@ -161,8 +229,10 @@ async def update_user(
|
||||
)
|
||||
|
||||
if user_data.username is not None:
|
||||
# Check if new username already exists
|
||||
existing_user = await db.execute(select(User).where(User.username == user_data.username, User.id != user_id))
|
||||
# Check if new username already exists (case-insensitive)
|
||||
existing_user = await db.execute(
|
||||
select(User).where(func.lower(User.username) == func.lower(user_data.username), User.id != user_id)
|
||||
)
|
||||
if existing_user.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -170,6 +240,18 @@ async def update_user(
|
||||
)
|
||||
user.username = user_data.username
|
||||
|
||||
if user_data.email is not None:
|
||||
# Check if new email already exists (case-insensitive)
|
||||
existing_email = await db.execute(
|
||||
select(User).where(func.lower(User.email) == func.lower(user_data.email), User.id != user_id)
|
||||
)
|
||||
if existing_email.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already exists",
|
||||
)
|
||||
user.email = user_data.email
|
||||
|
||||
if user_data.password is not None:
|
||||
user.password_hash = get_password_hash(user_data.password)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user