feat: 完善接口
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# 运行环境标识,development、testing 或 production
|
||||
ENVIRONMENT=development
|
||||
|
||||
# 是否开放用户注册接口,关闭后注册请求返回 403
|
||||
REGISTRATION_ENABLED=true
|
||||
|
||||
# MySQL 连接地址,mysql+asyncmy://用户名:密码@主机:端口/数据库名
|
||||
DATABASE_URL=mysql+asyncmy://center:change-me@127.0.0.1:3306/center
|
||||
|
||||
|
||||
@@ -23,35 +23,45 @@ Python 3.11+ · FastAPI · SQLAlchemy 2.0 · MySQL · Redis · Alembic · uv
|
||||
uv sync
|
||||
Copy-Item .env.example .env
|
||||
uv run alembic upgrade head
|
||||
uv run fastapi dev app/main.py
|
||||
uv run fastapi dev app/main.py --port 8100
|
||||
```
|
||||
|
||||
启动前请在 `.env` 中配置数据库连接、Redis 连接及可信回跳来源。`DATABASE_URL`、`REDIS_URL` 和
|
||||
`REDIS_KEY_PREFIX` 为必填项,缺失时应用会拒绝启动。完整配置和注释见 [`.env.example`](.env.example)。
|
||||
`REDIS_KEY_PREFIX` 为必填项,缺失时应用会拒绝启动。将 `REGISTRATION_ENABLED` 设置为 `false`
|
||||
可关闭公开注册接口。完整配置和注释见 [`.env.example`](.env.example)。
|
||||
|
||||
服务默认运行在 `http://127.0.0.1:8000`,交互式 API 文档 `http://127.0.0.1:8000/docs`
|
||||
服务默认运行在 `http://127.0.0.1:8100`,交互式 API 文档 `http://127.0.0.1:8100/docs`
|
||||
|
||||
## API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|--------|----------------------|--------------------|
|
||||
| `GET` | `/health` | 检查服务及依赖状态 |
|
||||
| `POST` | `/api/auth/register` | 注册用户 |
|
||||
| `POST` | `/api/auth/login` | 登录并签发 Token |
|
||||
| `POST` | `/api/auth/verify` | 验证 Token |
|
||||
| `POST` | `/api/auth/logout` | 注销当前 Token |
|
||||
| `GET` | `/api/users/me` | 获取当前用户信息 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
|--------|----------------------|------------------------------|
|
||||
| `GET` | `/health` | 检查服务、依赖及注册开关状态 |
|
||||
| `POST` | `/api/auth/register` | 注册用户 |
|
||||
| `POST` | `/api/auth/login` | 登录并签发 Token |
|
||||
| `POST` | `/api/auth/verify` | 验证 Token 与回跳地址 |
|
||||
| `POST` | `/api/auth/logout` | 注销当前 Token |
|
||||
| `GET` | `/api/users/me` | 获取当前用户信息 |
|
||||
|
||||
登录示例:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/auth/login \
|
||||
curl -X POST http://127.0.0.1:8100/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"account":"alice","password":"password123","from_url":"https://app.example.com/callback"}'
|
||||
```
|
||||
|
||||
受保护接口使用 `Authorization: Bearer <token>` 请求头。
|
||||
|
||||
已登录回跳需要把待校验地址交给后端,并且只使用响应中的 `data.from_url`:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8100/api/auth/verify \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"from_url":"https://app.example.com/callback"}'
|
||||
```
|
||||
|
||||
## 响应协议
|
||||
|
||||
所有接口返回统一结构:
|
||||
|
||||
+15
-6
@@ -10,9 +10,10 @@ from app.api.dependencies import get_bearer_token, get_current_user
|
||||
from app.common.response import ApiResponse, success_response
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.database import get_db
|
||||
from app.core.exceptions import BizException
|
||||
from app.core.redis import get_redis
|
||||
from app.model.user import User
|
||||
from app.schema.auth import LoginRequest, LoginResponse
|
||||
from app.schema.auth import LoginRequest, LoginResponse, VerifyRequest, VerifyResponse
|
||||
from app.schema.user import UserRegisterRequest, UserResponse
|
||||
from app.service.auth_service import AuthService
|
||||
from app.service.token_service import TokenService
|
||||
@@ -25,8 +26,11 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
async def register_user(
|
||||
request: UserRegisterRequest,
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> ApiResponse[UserResponse]:
|
||||
"""注册新的中心平台用户"""
|
||||
if not settings.registration_enabled:
|
||||
raise BizException(403, "注册已关闭")
|
||||
user = await UserService(session).register(request)
|
||||
return success_response(UserResponse.model_validate(user))
|
||||
|
||||
@@ -43,12 +47,17 @@ async def login_user(
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@router.post("/verify", response_model=ApiResponse[UserResponse])
|
||||
@router.post("/verify", response_model=ApiResponse[VerifyResponse])
|
||||
async def verify_token(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
) -> ApiResponse[UserResponse]:
|
||||
"""验证 Token 并返回当前用户身份"""
|
||||
return success_response(UserResponse.model_validate(current_user))
|
||||
request: VerifyRequest,
|
||||
token: Annotated[str, Depends(get_bearer_token)],
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
redis_client: Annotated[Redis, Depends(get_redis)],
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> ApiResponse[VerifyResponse]:
|
||||
"""验证 Token 和回跳地址并返回当前用户身份"""
|
||||
result = await AuthService(session, redis_client, settings).verify(token, request)
|
||||
return success_response(result)
|
||||
|
||||
|
||||
@router.post("/logout", response_model=ApiResponse[dict[str, object]])
|
||||
|
||||
+16
-3
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.api.auth import router as auth_router
|
||||
from app.api.user import router as user_router
|
||||
from app.common.response import ApiResponse, success_response
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.database import get_db
|
||||
from app.core.redis import get_redis
|
||||
|
||||
@@ -18,12 +19,24 @@ api_router.include_router(auth_router)
|
||||
api_router.include_router(user_router)
|
||||
|
||||
|
||||
@api_router.get("/health", response_model=ApiResponse[dict[str, str]], tags=["系统"])
|
||||
@api_router.get(
|
||||
"/health",
|
||||
response_model=ApiResponse[dict[str, str | bool]],
|
||||
tags=["系统"],
|
||||
)
|
||||
async def health_check(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
redis_client: Annotated[Redis, Depends(get_redis)],
|
||||
) -> ApiResponse[dict[str, str]]:
|
||||
settings: Annotated[Settings, Depends(get_settings)],
|
||||
) -> ApiResponse[dict[str, str | bool]]:
|
||||
"""检查应用及外部依赖的健康状态"""
|
||||
await session.execute(text("SELECT 1"))
|
||||
await redis_client.ping()
|
||||
return success_response({"status": "ok", "database": "ok", "redis": "ok"})
|
||||
return success_response(
|
||||
{
|
||||
"status": "ok",
|
||||
"database": "ok",
|
||||
"redis": "ok",
|
||||
"registration_enabled": settings.registration_enabled,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ def _split_origins(value: str) -> list[str]:
|
||||
|
||||
class Settings(BaseSettings):
|
||||
environment: str = "development"
|
||||
registration_enabled: bool = True
|
||||
database_url: str = Field(min_length=1)
|
||||
redis_url: str = Field(min_length=1)
|
||||
redis_key_prefix: str = Field(
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@ from fastapi import FastAPI
|
||||
|
||||
ERROR_RESPONSES: dict[str, tuple[int, ...]] = {
|
||||
"/health": (500,),
|
||||
"/api/auth/register": (400, 500),
|
||||
"/api/auth/register": (400, 403, 500),
|
||||
"/api/auth/login": (400, 401, 500),
|
||||
"/api/auth/verify": (401, 403, 500),
|
||||
"/api/auth/verify": (400, 401, 403, 500),
|
||||
"/api/auth/logout": (401, 403, 500),
|
||||
"/api/users/me": (401, 403, 500),
|
||||
}
|
||||
|
||||
+11
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schema.user import UserResponse
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
account: str = Field(min_length=1, max_length=255)
|
||||
@@ -11,6 +13,14 @@ class LoginRequest(BaseModel):
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
token_type: str = "Bearer"
|
||||
expires_in: int
|
||||
from_url: str | None = None
|
||||
|
||||
|
||||
class VerifyRequest(BaseModel):
|
||||
from_url: str = Field(min_length=1, max_length=2048)
|
||||
|
||||
|
||||
class VerifyResponse(BaseModel):
|
||||
user: UserResponse
|
||||
from_url: str
|
||||
|
||||
@@ -8,7 +8,8 @@ from app.core.exceptions import BizException
|
||||
from app.core.security import validate_from_url, verify_password
|
||||
from app.model.user import User
|
||||
from app.repository.user_repository import UserRepository
|
||||
from app.schema.auth import LoginRequest, LoginResponse
|
||||
from app.schema.auth import LoginRequest, LoginResponse, VerifyRequest, VerifyResponse
|
||||
from app.schema.user import UserResponse
|
||||
from app.service.token_service import TokenService
|
||||
|
||||
|
||||
@@ -73,3 +74,17 @@ class AuthService:
|
||||
await self.tokens.delete(token)
|
||||
raise BizException(403)
|
||||
return user
|
||||
|
||||
async def verify(self, token: str, request: VerifyRequest) -> VerifyResponse:
|
||||
"""验证 Token 后校验并返回可信回跳地址"""
|
||||
user = await self.authenticate(token)
|
||||
from_url = validate_from_url(
|
||||
request.from_url,
|
||||
self.settings.trusted_redirect_origin_list,
|
||||
)
|
||||
if from_url is None:
|
||||
raise BizException(400, "参数错误")
|
||||
return VerifyResponse(
|
||||
user=UserResponse.model_validate(user),
|
||||
from_url=from_url,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user