Skip to content

Commit 1754c1a

Browse files
committed
feat(mgr-api): 添加可用性检查接口 & 更新认证白名单
- 新增 GET /user/availability/mail(免认证,邮箱可用性) - 新增 GET /user/availability/username(免认证,用户名可用性) - 新增 GET /account/availability/slug(需登录,Slug 可用性) - 更新 AdminAuthMiddleware 免认证前缀白名单 - ruff + black 格式化
1 parent 2f72a16 commit 1754c1a

File tree

8 files changed

+117
-6
lines changed

8 files changed

+117
-6
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""账户 Slug 可用性检查端点。
2+
3+
供已登录用户创建账户时校验 Slug 是否可用,需身份认证。
4+
校验流程:格式合法性 → 保留名检查 → 唯一性查询。
5+
"""
6+
7+
from fastapi import APIRouter, Depends, Query
8+
from sqlalchemy import select
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
11+
from app.models.account import Account
12+
from app.models.session import get_db
13+
from app.services.user.name_validator import (
14+
is_name_reserved,
15+
validate_slug_format,
16+
)
17+
18+
router = APIRouter()
19+
20+
21+
@router.get("/slug")
22+
async def check_slug_availability(
23+
value: str = Query(..., min_length=3, max_length=64, description="待检查的 Slug"),
24+
db: AsyncSession = Depends(get_db),
25+
):
26+
"""检查账户 Slug 是否可用。
27+
28+
依次校验格式、保留名和唯一性,返回结果与不可用原因。
29+
"""
30+
if not validate_slug_format(value):
31+
return {"available": False, "reason": "Slug 格式不合法"}
32+
if await is_name_reserved(value, db):
33+
return {"available": False, "reason": "该名称为系统保留名"}
34+
result = await db.execute(select(Account.id).where(Account.slug == value))
35+
if result.scalar_one_or_none() is not None:
36+
return {"available": False, "reason": "该 Slug 已被占用"}
37+
return {"available": True}

app/api/management/account_pre_reg_crud.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""预注册客户端 CRUD 路由。"""
22

3-
43
from fastapi import APIRouter, Depends
54
from sqlalchemy import select
65
from sqlalchemy.ext.asyncio import AsyncSession

app/api/management/account_router.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@
2525
from .account_invitation import router as inv_r
2626
from .account_invitation_create import router as inv_c_r
2727
from .account_invitation_revoke import router as inv_v_r
28+
from .account_availability_slug import router as avail_slug_r
2829
from .bulk import router as bulk_r
2930

3031
router = APIRouter()
3132

33+
# /account/availability/*(需认证)
34+
router.include_router(
35+
avail_slug_r, prefix="/account/availability", tags=["Availability"]
36+
)
37+
3238
# /account 顶层
3339
router.include_router(list_r, prefix="/account", tags=["Account"])
3440
router.include_router(search_r, prefix="/account", tags=["Account"])

app/api/management/router.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from .token_deactivate import router as deact_r
1111
from .user_apply import router as apply_r
1212
from .user_auth import router as auth_r
13+
from .user_availability_mail import router as avail_mail_r
14+
from .user_availability_username import router as avail_uname_r
1315
from .user_info import router as info_r
1416
from .user_totp import router as totp_r
1517
from .user_info_email import router as email_r
@@ -29,6 +31,10 @@
2931
router.include_router(info_r, prefix="/user", tags=["User"])
3032
router.include_router(totp_r, prefix="/user/2fa/totp", tags=["2FA"])
3133

34+
# /user/availability/*(无需认证)
35+
router.include_router(avail_mail_r, prefix="/user/availability", tags=["Availability"])
36+
router.include_router(avail_uname_r, prefix="/user/availability", tags=["Availability"])
37+
3238
# /user/info/*
3339
router.include_router(email_r, prefix="/user/info", tags=["UserInfo"])
3440
router.include_router(uname_r, prefix="/user/info", tags=["UserInfo"])
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""邮箱可用性检查端点。
2+
3+
供注册页前端实时校验邮箱是否已被占用,无需身份认证。
4+
"""
5+
6+
from fastapi import APIRouter, Query
7+
from pydantic import EmailStr
8+
from sqlalchemy import select
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
from fastapi import Depends
11+
12+
from app.models.session import get_db
13+
from app.models.user import User
14+
15+
router = APIRouter()
16+
17+
18+
@router.get("/mail")
19+
async def check_mail_availability(
20+
value: EmailStr = Query(..., description="待检查的邮箱地址"),
21+
db: AsyncSession = Depends(get_db),
22+
):
23+
"""检查邮箱是否可用(未被注册)。"""
24+
result = await db.execute(select(User.id).where(User.email == value))
25+
exists = result.scalar_one_or_none() is not None
26+
return {"available": not exists}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""用户名可用性检查端点。
2+
3+
供注册页前端实时校验用户名是否可用,无需身份认证。
4+
校验流程:格式合法性 → 保留名检查 → 唯一性查询。
5+
"""
6+
7+
from fastapi import APIRouter, Depends, Query
8+
from sqlalchemy import select
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
11+
from app.models.session import get_db
12+
from app.models.user import User
13+
from app.services.user.name_validator import (
14+
is_name_reserved,
15+
validate_username_format,
16+
)
17+
18+
router = APIRouter()
19+
20+
21+
@router.get("/username")
22+
async def check_username_availability(
23+
value: str = Query(..., min_length=3, max_length=64, description="待检查的用户名"),
24+
db: AsyncSession = Depends(get_db),
25+
):
26+
"""检查用户名是否可用。
27+
28+
依次校验格式、保留名和唯一性,返回结果与不可用原因。
29+
"""
30+
if not validate_username_format(value):
31+
return {"available": False, "reason": "用户名格式不合法"}
32+
if await is_name_reserved(value, db):
33+
return {"available": False, "reason": "该名称为系统保留名"}
34+
result = await db.execute(select(User.id).where(User.username == value))
35+
if result.scalar_one_or_none() is not None:
36+
return {"available": False, "reason": "该用户名已被占用"}
37+
return {"available": True}

app/api/schemas/account.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ class AccountCreate(BaseModel):
2424
def _check_slug(cls, v: Optional[str]) -> Optional[str]:
2525
"""Slug 格式校验(若提供)。"""
2626
if v is not None and not _SLUG_RE.match(v):
27-
raise ValueError(
28-
"Slug 需 3~64 位小写字母数字连字符," "不以连字符开头/结尾"
29-
)
27+
raise ValueError("Slug 需 3~64 位小写字母数字连字符,不以连字符开头/结尾")
3028
return v
3129

3230

app/core/auth/admin_middleware.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
logger = logging.getLogger(__name__)
2222

23-
# 免认证路径白名单
23+
# 免认证路径白名单(精确匹配)
2424
_EXEMPT = {"/", "/user/auth", "/user/apply"}
25+
# 免认证路径前缀(前缀匹配)
26+
_EXEMPT_PREFIXES = ("/user/availability/",)
2527
_DENY = JSONResponse(status_code=403, content={"detail": "权限不足"})
2628
_NO_AUTH = JSONResponse(status_code=401, content={"detail": "未认证"})
2729

@@ -37,7 +39,7 @@ async def dispatch(
3739
if request.method == "OPTIONS":
3840
return await call_next(request)
3941
path = request.url.path
40-
if path in _EXEMPT:
42+
if path in _EXEMPT or path.startswith(_EXEMPT_PREFIXES):
4143
return await call_next(request)
4244
# 提取 Bearer Token
4345
auth = request.headers.get("authorization", "")

0 commit comments

Comments
 (0)