feat: Phase 1 后端骨架主体入库(补 backend/app 主体与 requirements.txt)
上轮 3 次 commit 只入库了修补文件,主体目录漏入库。本次补入 backend/app/、backend/requirements.txt、backend/scripts/__init__.py。同步在 .gitignore 加 homePC/(制片人家用机带过来的参考文件,不进库)。
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
认证路由 — 登录 / 登出 / 当前用户
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.security import verify_password
|
||||
from app.db.session import get_session
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import LoginRequest, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=UserResponse)
|
||||
def login(body: LoginRequest, request: Request, session: Session = Depends(get_session)):
|
||||
"""账号登录。验证成功后写入 session,返回用户信息。"""
|
||||
statement = select(User).where(User.username == body.username)
|
||||
user = session.exec(statement).first()
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
)
|
||||
if not verify_password(body.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
)
|
||||
request.session["user_id"] = user.id
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request):
|
||||
"""退出登录,清空当前 session。"""
|
||||
request.session.clear()
|
||||
return {"message": "已退出登录"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_me(user: User = Depends(get_current_user)):
|
||||
"""获取当前登录用户的信息。"""
|
||||
return user
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
应用配置 — 用 pydantic-settings 的 BaseSettings 读取 .env
|
||||
"""
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str
|
||||
SECRET_KEY: str = "change-me-to-a-random-string-in-production"
|
||||
SESSION_MAX_AGE: int = 86400 # 秒,默认 1 天
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_file_encoding": "utf-8",
|
||||
}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
FastAPI 依赖注入工具 — 获取当前用户、角色鉴权
|
||||
"""
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
def get_current_user(
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""从 Session 中取 user_id,查库返回当前用户。
|
||||
|
||||
未登录或用户不存在 / 已停用时抛出 401。
|
||||
"""
|
||||
user_id = request.session.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="请先登录",
|
||||
)
|
||||
user = session.get(User, user_id)
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已被停用",
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
def require_role(*allowed_roles: UserRole):
|
||||
"""返回一个 Depends 依赖,要求当前用户拥有指定的角色之一。
|
||||
|
||||
使用示例:
|
||||
@router.get("/admin-only")
|
||||
def admin_endpoint(user=Depends(require_role(UserRole.zhipianren))):
|
||||
...
|
||||
|
||||
@router.get("/staff")
|
||||
def staff_endpoint(user=Depends(require_role(UserRole.zhipianren, UserRole.zebian))):
|
||||
...
|
||||
"""
|
||||
|
||||
def check_role(user: User = Depends(get_current_user)) -> User:
|
||||
if user.role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,无法执行此操作",
|
||||
)
|
||||
return user
|
||||
|
||||
return check_role
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
认证安全工具 — passlib + bcrypt
|
||||
"""
|
||||
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
"""用 bcrypt 生成密码哈希。"""
|
||||
return pwd_context.hash(plain)
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
"""验证明文密码与哈希是否匹配。"""
|
||||
return pwd_context.verify(plain, hashed)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
数据库会话 — SQLModel 同步引擎 + Session 工厂
|
||||
"""
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(settings.DATABASE_URL, echo=False)
|
||||
|
||||
|
||||
def init_db():
|
||||
"""创建所有 SQLModel 表(仅开发用,生产由手写 SQL 管理)。"""
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
def get_session():
|
||||
"""FastAPI 依赖注入: 每个请求获取一个 Session,用完后关闭。"""
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
FastAPI 应用入口 — 挂载中间件、路由
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.api.auth import router as auth_router
|
||||
from app.core.config import settings
|
||||
|
||||
app = FastAPI(title="军事科技工作台", version="0.1.0")
|
||||
|
||||
# ---- CORS 中间件 ----
|
||||
# 显式列 origins 不能用 ['*'],否则 credentials=True 时浏览器拒绝携带 cookie
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ---- Session 中间件 ----
|
||||
# 密钥来自 .env,用于签名 session cookie
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=settings.SECRET_KEY,
|
||||
session_cookie="milsci_session",
|
||||
max_age=settings.SESSION_MAX_AGE,
|
||||
same_site="lax",
|
||||
https_only=False, # 本地开发用 HTTP
|
||||
)
|
||||
|
||||
# ---- 注册路由 ----
|
||||
app.include_router(auth_router)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
用户模型 — SQLModel
|
||||
|
||||
角色体系(不再改):
|
||||
- zhipianren = 制片人 / 管理员 = 全部权限
|
||||
- zebian = 责编 = 维护仪表盘 / 知识库 / 排期
|
||||
- biandao = 编导 = 使用 TPS / 查看知识库 / 生成报题单 / 个人热点雷达
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import DateTime as SADateTime
|
||||
from sqlalchemy.sql import func as sa_func
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
"""用户角色枚举,值与数据库 CHECK 约束一致。"""
|
||||
zhipianren = "zhipianren" # 制片人 / 管理员
|
||||
zebian = "zebian" # 责编
|
||||
biandao = "biandao" # 编导
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
username: str = Field(max_length=50, unique=True, index=True)
|
||||
display_name: str = Field(max_length=100)
|
||||
password_hash: str = Field(max_length=128)
|
||||
role: UserRole = Field(default=UserRole.biandao, max_length=20)
|
||||
profile_text: str | None = Field(default=None)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
created_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
|
||||
)
|
||||
updated_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
认证相关 Pydantic Schema — 请求 / 响应模型
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models.user import UserRole
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""返回给前端的用户信息(不含 password_hash)。"""
|
||||
id: int
|
||||
username: str
|
||||
display_name: str
|
||||
role: UserRole
|
||||
profile_text: str | None = None
|
||||
is_active: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user