完善邮件验证与密码重置安全流程
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_roles
|
||||
from app.schemas.email_settings import (
|
||||
EmailDomainCreateRequest,
|
||||
EmailSettingsListResponse,
|
||||
EmailSettingsRead,
|
||||
EmailSettingsUpdate,
|
||||
EmailTestRequest,
|
||||
)
|
||||
from app.services import email_service
|
||||
|
||||
router = APIRouter(prefix="/email-settings")
|
||||
|
||||
|
||||
@router.get("/", response_model=EmailSettingsListResponse)
|
||||
async def read_email_settings(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> EmailSettingsListResponse:
|
||||
items = [email_service.to_email_settings_read(row) for row in await email_service.list_email_settings(db)]
|
||||
return EmailSettingsListResponse(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.post("/", response_model=EmailSettingsRead)
|
||||
async def create_email_domain(
|
||||
payload: EmailDomainCreateRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> EmailSettingsRead:
|
||||
settings_row = await email_service.create_email_domain(db, payload.register_domain, updated_by=current_user.id)
|
||||
return email_service.to_email_settings_read(settings_row)
|
||||
|
||||
|
||||
@router.get("/{register_domain}", response_model=EmailSettingsRead)
|
||||
async def read_email_domain_settings(
|
||||
register_domain: str,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> EmailSettingsRead:
|
||||
return email_service.to_email_settings_read(await email_service.get_email_settings(db, register_domain))
|
||||
|
||||
|
||||
@router.put("/{register_domain}", response_model=EmailSettingsRead)
|
||||
async def update_email_settings(
|
||||
register_domain: str,
|
||||
payload: EmailSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> EmailSettingsRead:
|
||||
settings_row = await email_service.upsert_email_settings(db, register_domain, payload, updated_by=current_user.id)
|
||||
return email_service.to_email_settings_read(settings_row)
|
||||
|
||||
|
||||
@router.delete("/{register_domain}")
|
||||
async def delete_email_domain(
|
||||
register_domain: str,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
):
|
||||
await email_service.delete_email_domain(db, register_domain)
|
||||
return {"message": "邮箱后缀已删除"}
|
||||
|
||||
|
||||
@router.post("/{register_domain}/test")
|
||||
async def test_email_settings(
|
||||
register_domain: str,
|
||||
payload: EmailTestRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
):
|
||||
await email_service.send_test_email(db, register_domain, str(payload.recipient_email))
|
||||
return {"message": "测试邮件已发送"}
|
||||
+107
-2
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import File, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
@@ -12,7 +12,19 @@ from app.core.security import create_access_token, decode_token_allow_expired, o
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import UserStatus
|
||||
from app.schemas.email_settings import (
|
||||
EmailCodeResponse,
|
||||
EmailCodeVerifyResponse,
|
||||
PasswordResetCodeVerifyRequest,
|
||||
PasswordResetCodeVerifyResponse,
|
||||
PasswordResetLinkSendRequest,
|
||||
PasswordResetRequest,
|
||||
PasswordResetTokenRequest,
|
||||
RegisterEmailCodeSendRequest,
|
||||
RegisterEmailCodeVerifyRequest,
|
||||
)
|
||||
from app.schemas.user import Token, UserRead, UserRegisterRequest, UserSelfUpdate, UserUpdate
|
||||
from app.services import email_service
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
@@ -39,6 +51,14 @@ class ExtendResponse(BaseModel):
|
||||
expiresAt: datetime
|
||||
|
||||
|
||||
class EmailAvailabilityResponse(BaseModel):
|
||||
available: bool
|
||||
|
||||
|
||||
class EmailDomainsResponse(BaseModel):
|
||||
items: list[str]
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
AVATAR_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads" / "avatars"
|
||||
AVATAR_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
@@ -108,6 +128,90 @@ def ensure_user_active(db_user) -> None:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/email-domains", response_model=EmailDomainsResponse)
|
||||
async def read_email_domains(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailDomainsResponse:
|
||||
rows = await email_service.list_email_settings(db)
|
||||
return EmailDomainsResponse(items=[row.register_domain for row in rows])
|
||||
|
||||
|
||||
@router.get("/register/email-availability", response_model=EmailAvailabilityResponse)
|
||||
async def check_register_email_availability(
|
||||
email: EmailStr = Query(...),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailAvailabilityResponse:
|
||||
existing = await user_crud.get_by_email(db, str(email))
|
||||
return EmailAvailabilityResponse(available=existing is None)
|
||||
|
||||
|
||||
@router.post("/register/email-code/send", response_model=EmailCodeResponse)
|
||||
async def send_register_email_code(
|
||||
payload: RegisterEmailCodeSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_register_code(db, str(payload.email))
|
||||
return EmailCodeResponse(message="验证码已发送")
|
||||
|
||||
|
||||
@router.post("/register/email-code/verify", response_model=EmailCodeVerifyResponse)
|
||||
async def verify_register_email_code(
|
||||
payload: RegisterEmailCodeVerifyRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeVerifyResponse:
|
||||
await email_service.verify_register_code(db, str(payload.email), payload.code)
|
||||
return EmailCodeVerifyResponse(verified=True)
|
||||
|
||||
|
||||
@router.post("/password-reset/email-code/send", response_model=EmailCodeResponse)
|
||||
async def send_password_reset_email_code(
|
||||
payload: PasswordResetLinkSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_password_reset_code(db, str(payload.email))
|
||||
return EmailCodeResponse(message="验证码发送成功,请查收邮箱")
|
||||
|
||||
|
||||
@router.post("/password-reset/email-code/verify", response_model=PasswordResetCodeVerifyResponse)
|
||||
async def verify_password_reset_email_code(
|
||||
payload: PasswordResetCodeVerifyRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> PasswordResetCodeVerifyResponse:
|
||||
reset_token = await email_service.verify_password_reset_code(db, str(payload.email), payload.code)
|
||||
return PasswordResetCodeVerifyResponse(verified=True, reset_token=reset_token)
|
||||
|
||||
|
||||
@router.post("/password-reset-link/send", response_model=EmailCodeResponse)
|
||||
async def send_password_reset_link(
|
||||
payload: PasswordResetLinkSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_password_reset_link(
|
||||
db,
|
||||
str(payload.email),
|
||||
frontend_origin=settings.FRONTEND_PUBLIC_URL,
|
||||
)
|
||||
return EmailCodeResponse(message="如果账号存在,重置链接已发送")
|
||||
|
||||
|
||||
@router.post("/password-reset", response_model=EmailCodeResponse)
|
||||
async def reset_password(
|
||||
payload: PasswordResetRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.reset_password_with_code(db, str(payload.email), payload.code, payload.password)
|
||||
return EmailCodeResponse(message="密码已重置,请返回登录")
|
||||
|
||||
|
||||
@router.post("/password-reset-link", response_model=EmailCodeResponse)
|
||||
async def reset_password_with_link(
|
||||
payload: PasswordResetTokenRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.reset_password_with_token(db, payload.token, payload.password)
|
||||
return EmailCodeResponse(message="密码已重置,请返回登录")
|
||||
|
||||
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
payload: UserRegisterRequest,
|
||||
@@ -116,6 +220,7 @@ async def register(
|
||||
existing = await user_crud.get_by_email(db, payload.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="邮箱已注册")
|
||||
await email_service.ensure_register_email_verified(db, str(payload.email))
|
||||
await user_crud.create_registered_user(db, payload)
|
||||
return {"message": "注册成功,请登录"}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
|
||||
from app.api.v1 import auth, users, admin_email_settings, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(admin_email_settings.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
|
||||
api_router.include_router(overview.router, prefix="/studies/{study_id}", tags=["overview"])
|
||||
|
||||
Reference in New Issue
Block a user