- 认证:Web Session(__Host- Cookie、HttpOnly/Secure/SameSite=Lax、CSPRNG opaque token 数据库只存哈希)、CSRF 双提交、Origin 校验、dev 登录/登出端点;未认证 401、缺 CSRF 403
- 中间件:Hunjing-Request-ID 回显/生成(request_ UUIDv4)+ W3C traceparent 校验透传;JSON 结构化日志与敏感头脱敏
- 迁移 2:conversation/branch/message/run/idempotency_record 五表(含检查约束与复合索引);Session 补 token/csrf 哈希列
- 垂直切片:POST /v1/conversations/{id}/messages 按 Idempotency-Key 幂等(同键同体 200 重放、异体 409),同事务原子写 Message+Run+Audit+Outbox+幂等记录;ContentBlock text/markdown 判别联合(未知类型 422)
- SSE:GET /v1/runs/{id}/events 返回快照与终止状态事件 envelope
- 测试 26 项全绿:IDOR 跨域 404 fail-closed、失败注入回滚无孤儿(Run 崩溃后 message/run/audit/outbox/幂等全零)、SSE 事件序、auth 边界;ruff/mypy strict 干净
- 修复真实问题:asyncpg 跨事件循环(anyio/pytest-asyncio 双 loop、模块级 engine 改为 app 实例持有)、httpx 拒存 http 下 Secure Cookie
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""认证路由:开发档登录/登出(Web Session Cookie)。
|
|
|
|
正式 OAuth 2.1 授权码流程(/oauth2/authorize、/oauth2/token、PKCE)属 WP-01
|
|
后续条目;本路由只建立 Session 边界骨架供垂直切片验证。
|
|
"""
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, Response, status
|
|
|
|
from app.auth.service import (
|
|
SESSION_COOKIE,
|
|
create_session,
|
|
revoke_session,
|
|
)
|
|
from app.db.session import DbSession
|
|
from app.models import PersonalDomain, User
|
|
|
|
router = APIRouter(prefix="/v1/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/sessions", status_code=201)
|
|
async def dev_login(
|
|
request: Request, response: Response, db: DbSession, body: dict[str, str]
|
|
) -> dict[str, str]:
|
|
"""开发档登录:按 email 找到或创建用户与个人域,签发会话。"""
|
|
email = body.get("email", "").strip().lower()
|
|
if not email or "@" not in email:
|
|
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "email 不合法")
|
|
from sqlalchemy import select
|
|
|
|
existing = (
|
|
(await db.execute(select(User).where(User.email == email))).scalar_one_or_none()
|
|
)
|
|
if existing is None:
|
|
user = User(email=email, display_name=email.split("@")[0])
|
|
db.add(user)
|
|
await db.flush()
|
|
domain = PersonalDomain(user_id=user.id, name="默认镜域")
|
|
db.add(domain)
|
|
else:
|
|
user = existing
|
|
_, token, csrf = await create_session(db, user.id)
|
|
await db.commit()
|
|
response.set_cookie(
|
|
SESSION_COOKIE,
|
|
token,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite="lax",
|
|
path="/",
|
|
)
|
|
return {"csrf_token": csrf, "user_id": user.id}
|
|
|
|
|
|
@router.delete("/sessions/current", status_code=204)
|
|
async def logout(request: Request, response: Response, db: DbSession) -> None:
|
|
token = request.cookies.get(SESSION_COOKIE)
|
|
if token is not None and await revoke_session(db, token):
|
|
await db.commit()
|
|
response.delete_cookie(SESSION_COOKIE, path="/")
|