""" 郵件帳號管理 API 符合 WebMail 設計規範 - 員工只能使用 HR Portal 授權的郵件帳號 """ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status, Request from sqlalchemy.orm import Session, joinedload from sqlalchemy import or_ from app.db.session import get_db from app.models.employee import Employee from app.models.email_account import EmailAccount from app.schemas.email_account import ( EmailAccountCreate, EmailAccountUpdate, EmailAccountResponse, EmailAccountListItem, EmailAccountQuotaUpdate, ) from app.schemas.base import PaginationParams, PaginatedResponse from app.schemas.response import SuccessResponse, MessageResponse from app.api.deps import get_pagination_params from app.services.audit_service import audit_service router = APIRouter() def get_current_tenant_id() -> int: """取得當前租戶 ID (暫時寫死為 1, 未來從 JWT token 取得)""" return 1 @router.get("/", response_model=PaginatedResponse) def get_email_accounts( db: Session = Depends(get_db), pagination: PaginationParams = Depends(get_pagination_params), employee_id: Optional[int] = Query(None, description="員工 ID 篩選"), is_active: Optional[bool] = Query(None, description="狀態篩選"), search: Optional[str] = Query(None, description="搜尋郵件地址"), ): """ 獲取郵件帳號列表 支援: - 分頁 - 員工篩選 - 狀態篩選 - 郵件地址搜尋 """ tenant_id = get_current_tenant_id() query = db.query(EmailAccount).filter(EmailAccount.tenant_id == tenant_id) # 員工篩選 if employee_id: query = query.filter(EmailAccount.employee_id == employee_id) # 狀態篩選 if is_active is not None: query = query.filter(EmailAccount.is_active == is_active) # 搜尋 if search: search_pattern = f"%{search}%" query = query.filter(EmailAccount.email_address.ilike(search_pattern)) # 總數 total = query.count() # 分頁 offset = (pagination.page - 1) * pagination.page_size email_accounts = ( query.options(joinedload(EmailAccount.employee)) .offset(offset) .limit(pagination.page_size) .all() ) # 計算總頁數 total_pages = (total + pagination.page_size - 1) // pagination.page_size # 組裝回應資料 items = [] for account in email_accounts: item = EmailAccountListItem.model_validate(account) item.employee_name = account.employee.legal_name if account.employee else None item.employee_number = account.employee.employee_id if account.employee else None items.append(item) return PaginatedResponse( total=total, page=pagination.page, page_size=pagination.page_size, total_pages=total_pages, items=items, ) @router.get("/{email_account_id}", response_model=EmailAccountResponse) def get_email_account( email_account_id: int, db: Session = Depends(get_db), ): """ 獲取郵件帳號詳情 """ tenant_id = get_current_tenant_id() account = ( db.query(EmailAccount) .options(joinedload(EmailAccount.employee)) .filter( EmailAccount.id == email_account_id, EmailAccount.tenant_id == tenant_id, ) .first() ) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Email account with id {email_account_id} not found", ) # 組裝回應資料 response = EmailAccountResponse.model_validate(account) response.employee_name = account.employee.legal_name if account.employee else None response.employee_number = account.employee.employee_id if account.employee else None return response @router.post("/", response_model=EmailAccountResponse, status_code=status.HTTP_201_CREATED) def create_email_account( account_data: EmailAccountCreate, request: Request, db: Session = Depends(get_db), ): """ 創建郵件帳號 注意: - 郵件地址必須唯一 - 員工必須存在 - 配額範圍: 1GB - 100GB """ tenant_id = get_current_tenant_id() # 檢查員工是否存在 employee = db.query(Employee).filter( Employee.id == account_data.employee_id, Employee.tenant_id == tenant_id, ).first() if not employee: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Employee with id {account_data.employee_id} not found", ) # 檢查郵件地址是否已存在 existing = db.query(EmailAccount).filter( EmailAccount.email_address == account_data.email_address ).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Email address '{account_data.email_address}' already exists", ) # 創建郵件帳號 account = EmailAccount( tenant_id=tenant_id, employee_id=account_data.employee_id, email_address=account_data.email_address, quota_mb=account_data.quota_mb, forward_to=account_data.forward_to, auto_reply=account_data.auto_reply, is_active=account_data.is_active, ) db.add(account) db.commit() db.refresh(account) # 記錄審計日誌 audit_service.log_action( request=request, db=db, action="create_email_account", resource_type="email_account", resource_id=account.id, details={ "email_address": account.email_address, "employee_id": account.employee_id, "quota_mb": account.quota_mb, }, ) # 組裝回應資料 response = EmailAccountResponse.model_validate(account) response.employee_name = employee.legal_name response.employee_number = employee.employee_id return response @router.put("/{email_account_id}", response_model=EmailAccountResponse) def update_email_account( email_account_id: int, account_data: EmailAccountUpdate, request: Request, db: Session = Depends(get_db), ): """ 更新郵件帳號 可更新: - 配額 - 轉寄地址 - 自動回覆 - 啟用狀態 """ tenant_id = get_current_tenant_id() account = ( db.query(EmailAccount) .options(joinedload(EmailAccount.employee)) .filter( EmailAccount.id == email_account_id, EmailAccount.tenant_id == tenant_id, ) .first() ) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Email account with id {email_account_id} not found", ) # 記錄變更前的值 changes = {} # 更新欄位 update_data = account_data.model_dump(exclude_unset=True) for field, value in update_data.items(): old_value = getattr(account, field) if old_value != value: changes[field] = {"from": old_value, "to": value} setattr(account, field, value) if changes: db.commit() db.refresh(account) # 記錄審計日誌 audit_service.log_action( request=request, db=db, action="update_email_account", resource_type="email_account", resource_id=account.id, details={ "email_address": account.email_address, "changes": changes, }, ) # 組裝回應資料 response = EmailAccountResponse.model_validate(account) response.employee_name = account.employee.legal_name if account.employee else None response.employee_number = account.employee.employee_id if account.employee else None return response @router.patch("/{email_account_id}/quota", response_model=EmailAccountResponse) def update_email_quota( email_account_id: int, quota_data: EmailAccountQuotaUpdate, request: Request, db: Session = Depends(get_db), ): """ 更新郵件配額 快速更新配額的端點 """ tenant_id = get_current_tenant_id() account = ( db.query(EmailAccount) .options(joinedload(EmailAccount.employee)) .filter( EmailAccount.id == email_account_id, EmailAccount.tenant_id == tenant_id, ) .first() ) if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Email account with id {email_account_id} not found", ) old_quota = account.quota_mb account.quota_mb = quota_data.quota_mb db.commit() db.refresh(account) # 記錄審計日誌 audit_service.log_action( request=request, db=db, action="update_email_quota", resource_type="email_account", resource_id=account.id, details={ "email_address": account.email_address, "old_quota_mb": old_quota, "new_quota_mb": quota_data.quota_mb, }, ) # 組裝回應資料 response = EmailAccountResponse.model_validate(account) response.employee_name = account.employee.legal_name if account.employee else None response.employee_number = account.employee.employee_id if account.employee else None return response @router.delete("/{email_account_id}", response_model=MessageResponse) def delete_email_account( email_account_id: int, request: Request, db: Session = Depends(get_db), ): """ 刪除郵件帳號 注意: - 軟刪除: 設為停用 - 需要記錄審計日誌 """ tenant_id = get_current_tenant_id() account = db.query(EmailAccount).filter( EmailAccount.id == email_account_id, EmailAccount.tenant_id == tenant_id, ).first() if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Email account with id {email_account_id} not found", ) # 軟刪除: 設為停用 account.is_active = False db.commit() # 記錄審計日誌 audit_service.log_action( request=request, db=db, action="delete_email_account", resource_type="email_account", resource_id=account.id, details={ "email_address": account.email_address, "employee_id": account.employee_id, }, ) return MessageResponse( message=f"Email account {account.email_address} has been deactivated" ) @router.get("/employees/{employee_id}/email-accounts") def get_employee_email_accounts( employee_id: int, db: Session = Depends(get_db), ): """ 取得員工授權的郵件帳號列表 符合 WebMail 設計規範 (HR Portal設計文件 §2): - 員工不可自行新增郵件帳號 - 只能使用 HR Portal 授予的帳號 - 支援多帳號切換 (ISO 帳號管理流程) 回傳格式: { "user_id": "porsche.chen", "email_accounts": [ { "email": "porsche.chen@porscheworld.tw", "quota_mb": 5120, "status": "active", ... } ] } """ tenant_id = get_current_tenant_id() # 檢查員工是否存在 employee = db.query(Employee).filter( Employee.id == employee_id, Employee.tenant_id == tenant_id, ).first() if not employee: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Employee with id {employee_id} not found", ) # 查詢員工的所有啟用郵件帳號 accounts = ( db.query(EmailAccount) .filter( EmailAccount.employee_id == employee_id, EmailAccount.tenant_id == tenant_id, EmailAccount.is_active == True, ) .all() ) # 組裝符合 WebMail 設計規範的回應格式 email_accounts = [] for account in accounts: email_accounts.append({ "email": account.email_address, "quota_mb": account.quota_mb, "status": "active" if account.is_active else "inactive", "forward_to": account.forward_to, "auto_reply": account.auto_reply, }) return { "user_id": employee.username_base, "email_accounts": email_accounts, }