Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
263 lines
7.1 KiB
Python
263 lines
7.1 KiB
Python
"""
|
|
網路硬碟管理 API
|
|
"""
|
|
from typing import List
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db.session import get_db
|
|
from app.models.employee import Employee
|
|
from app.models.network_drive import NetworkDrive
|
|
from app.schemas.network_drive import (
|
|
NetworkDriveCreate,
|
|
NetworkDriveUpdate,
|
|
NetworkDriveResponse,
|
|
NetworkDriveListItem,
|
|
NetworkDriveQuotaUpdate,
|
|
)
|
|
from app.schemas.response import MessageResponse
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=List[NetworkDriveListItem])
|
|
def get_network_drives(
|
|
db: Session = Depends(get_db),
|
|
is_active: bool = True,
|
|
):
|
|
"""
|
|
獲取網路硬碟列表
|
|
"""
|
|
query = db.query(NetworkDrive)
|
|
|
|
if is_active is not None:
|
|
query = query.filter(NetworkDrive.is_active == is_active)
|
|
|
|
network_drives = query.order_by(NetworkDrive.drive_name).all()
|
|
|
|
return [NetworkDriveListItem.model_validate(nd) for nd in network_drives]
|
|
|
|
|
|
@router.get("/{network_drive_id}", response_model=NetworkDriveResponse)
|
|
def get_network_drive(
|
|
network_drive_id: int,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
獲取網路硬碟詳情
|
|
"""
|
|
network_drive = db.query(NetworkDrive).filter(
|
|
NetworkDrive.id == network_drive_id
|
|
).first()
|
|
|
|
if not network_drive:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Network drive with id {network_drive_id} not found"
|
|
)
|
|
|
|
response = NetworkDriveResponse.model_validate(network_drive)
|
|
response.employee_name = network_drive.employee.legal_name
|
|
response.employee_username = network_drive.employee.username_base
|
|
|
|
return response
|
|
|
|
|
|
@router.post("/", response_model=NetworkDriveResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_network_drive(
|
|
network_drive_data: NetworkDriveCreate,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
創建網路硬碟
|
|
|
|
檢查:
|
|
- 員工是否存在
|
|
- 員工是否已有 NAS 帳號
|
|
- drive_name 唯一性
|
|
"""
|
|
# 檢查員工是否存在
|
|
employee = db.query(Employee).filter(
|
|
Employee.id == network_drive_data.employee_id
|
|
).first()
|
|
|
|
if not employee:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Employee with id {network_drive_data.employee_id} not found"
|
|
)
|
|
|
|
# 檢查員工是否已有 NAS 帳號
|
|
existing = db.query(NetworkDrive).filter(
|
|
NetworkDrive.employee_id == network_drive_data.employee_id
|
|
).first()
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Employee already has a network drive"
|
|
)
|
|
|
|
# 檢查 drive_name 是否已存在
|
|
existing_name = db.query(NetworkDrive).filter(
|
|
NetworkDrive.drive_name == network_drive_data.drive_name
|
|
).first()
|
|
|
|
if existing_name:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Drive name '{network_drive_data.drive_name}' already exists"
|
|
)
|
|
|
|
# 創建網路硬碟
|
|
network_drive = NetworkDrive(**network_drive_data.model_dump())
|
|
|
|
db.add(network_drive)
|
|
db.commit()
|
|
db.refresh(network_drive)
|
|
|
|
# TODO: 創建審計日誌
|
|
# TODO: 在 NAS 上創建實際帳號
|
|
|
|
response = NetworkDriveResponse.model_validate(network_drive)
|
|
response.employee_name = employee.legal_name
|
|
response.employee_username = employee.username_base
|
|
|
|
return response
|
|
|
|
|
|
@router.put("/{network_drive_id}", response_model=NetworkDriveResponse)
|
|
def update_network_drive(
|
|
network_drive_id: int,
|
|
network_drive_data: NetworkDriveUpdate,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
更新網路硬碟
|
|
|
|
可更新: quota_gb, webdav_url, smb_url, is_active
|
|
"""
|
|
network_drive = db.query(NetworkDrive).filter(
|
|
NetworkDrive.id == network_drive_id
|
|
).first()
|
|
|
|
if not network_drive:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Network drive with id {network_drive_id} not found"
|
|
)
|
|
|
|
# 更新欄位
|
|
update_data = network_drive_data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(network_drive, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(network_drive)
|
|
|
|
# TODO: 創建審計日誌
|
|
# TODO: 更新 NAS 配額
|
|
|
|
response = NetworkDriveResponse.model_validate(network_drive)
|
|
response.employee_name = network_drive.employee.legal_name
|
|
response.employee_username = network_drive.employee.username_base
|
|
|
|
return response
|
|
|
|
|
|
@router.patch("/{network_drive_id}/quota", response_model=NetworkDriveResponse)
|
|
def update_network_drive_quota(
|
|
network_drive_id: int,
|
|
quota_data: NetworkDriveQuotaUpdate,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
更新網路硬碟配額
|
|
|
|
專用端點,僅更新配額
|
|
"""
|
|
network_drive = db.query(NetworkDrive).filter(
|
|
NetworkDrive.id == network_drive_id
|
|
).first()
|
|
|
|
if not network_drive:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Network drive with id {network_drive_id} not found"
|
|
)
|
|
|
|
network_drive.quota_gb = quota_data.quota_gb
|
|
|
|
db.commit()
|
|
db.refresh(network_drive)
|
|
|
|
# TODO: 創建審計日誌
|
|
# TODO: 更新 NAS 配額
|
|
|
|
response = NetworkDriveResponse.model_validate(network_drive)
|
|
response.employee_name = network_drive.employee.legal_name
|
|
response.employee_username = network_drive.employee.username_base
|
|
|
|
return response
|
|
|
|
|
|
@router.delete("/{network_drive_id}", response_model=MessageResponse)
|
|
def delete_network_drive(
|
|
network_drive_id: int,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
停用網路硬碟
|
|
|
|
注意: 這是軟刪除,只將 is_active 設為 False
|
|
"""
|
|
network_drive = db.query(NetworkDrive).filter(
|
|
NetworkDrive.id == network_drive_id
|
|
).first()
|
|
|
|
if not network_drive:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Network drive with id {network_drive_id} not found"
|
|
)
|
|
|
|
network_drive.is_active = False
|
|
|
|
db.commit()
|
|
|
|
# TODO: 創建審計日誌
|
|
# TODO: 停用 NAS 帳號 (但保留資料)
|
|
|
|
return MessageResponse(
|
|
message=f"Network drive '{network_drive.drive_name}' has been deactivated"
|
|
)
|
|
|
|
|
|
@router.get("/by-employee/{employee_id}", response_model=NetworkDriveResponse)
|
|
def get_network_drive_by_employee(
|
|
employee_id: int,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
根據員工 ID 獲取網路硬碟
|
|
"""
|
|
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
|
|
|
if not employee:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Employee with id {employee_id} not found"
|
|
)
|
|
|
|
if not employee.network_drive:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Employee does not have a network drive"
|
|
)
|
|
|
|
response = NetworkDriveResponse.model_validate(employee.network_drive)
|
|
response.employee_name = employee.legal_name
|
|
response.employee_username = employee.username_base
|
|
|
|
return response
|