diff --git a/app/auth.py b/app/auth.py index 46d8116..339a771 100644 --- a/app/auth.py +++ b/app/auth.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session as DbSession from app.config import SECRET_KEY, SESSION_COOKIE_NAME from app.database import get_db -from app.models import ApiKey, User, UserSession +from app.models import User, UserSession # ---------- Password ---------- @@ -81,22 +81,3 @@ def redirect_if_not_logged_in(request: Request, db: DbSession) -> User | None: if uid is None: return None return db.query(User).filter(User.id == uid).first() - - -# ---------- API Dependencies ---------- - -def get_current_user_api( - request: Request, - db: DbSession = Depends(get_db), -) -> User: - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - raise HTTPException(status_code=401, detail="Missing or invalid Authorization header") - token = auth_header[7:] - api_key = db.query(ApiKey).filter(ApiKey.key == token).first() - if api_key is None: - raise HTTPException(status_code=401, detail="Invalid API key") - user = db.query(User).filter(User.id == api_key.user_id).first() - if user is None: - raise HTTPException(status_code=401, detail="User not found") - return user diff --git a/app/main.py b/app/main.py index dce863f..55a14c8 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles from app.config import UPLOAD_DIR from app.database import init_db -from app.routers import api, web +from app.routers import web app = FastAPI(title="Postcard Manager") @@ -14,7 +14,6 @@ app.mount("/static", StaticFiles(directory=str(Path(__file__).resolve().parent / app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads") app.include_router(web.router) -app.include_router(api.router) init_db() diff --git a/app/models.py b/app/models.py index 8a3f74d..d1d6469 100644 --- a/app/models.py +++ b/app/models.py @@ -1,7 +1,6 @@ -import uuid from datetime import datetime, timezone -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, event +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.orm import relationship, validates from app.database import Base @@ -19,7 +18,6 @@ class User(Base): password_hash = Column(String(128), nullable=False) created_at = Column(DateTime, default=_utcnow) - api_keys = relationship("ApiKey", back_populates="user", cascade="all, delete-orphan") profiles = relationship("Profile", back_populates="user", cascade="all, delete-orphan") sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan") @@ -35,21 +33,6 @@ class UserSession(Base): user = relationship("User", back_populates="sessions") -class ApiKey(Base): - __tablename__ = "api_keys" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) - key = Column(String(64), unique=True, nullable=False, index=True) - created_at = Column(DateTime, default=_utcnow) - - user = relationship("User", back_populates="api_keys") - - @staticmethod - def generate_key() -> str: - return uuid.uuid4().hex - - class Profile(Base): __tablename__ = "profiles" @@ -68,7 +51,7 @@ class Postcard(Base): id = Column(Integer, primary_key=True, index=True) profile_id = Column(Integer, ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False) card_number = Column(String(64), nullable=False) - status = Column(String(16), nullable=False) # sent / delivered / received + status = Column(String(16), nullable=False) # sent & delivered recipient_name = Column(String(64)) country = Column(String(64)) diff --git a/app/routers/api.py b/app/routers/api.py deleted file mode 100644 index f887030..0000000 --- a/app/routers/api.py +++ /dev/null @@ -1,209 +0,0 @@ -from pathlib import Path -from uuid import uuid4 - -from fastapi import APIRouter, Depends, File, HTTPException, UploadFile -from sqlalchemy.orm import Session - -from app.auth import get_current_user_api, hash_password, verify_password -from app.config import UPLOAD_DIR -from app.database import get_db -from app.models import ApiKey, Postcard, Profile, User -from app.schemas import ( - ApiKeyOut, - LoginRequest, - PostcardCreate, - PostcardOut, - PostcardUpdate, - ProfileCreate, - ProfileOut, - RegisterRequest, -) - -router = APIRouter(prefix="/api", tags=["api"]) - - -# ---------- Auth ---------- - -@router.post("/register", response_model=dict) -def api_register(body: RegisterRequest, db: Session = Depends(get_db)): - if db.query(User).filter(User.username == body.username).first(): - raise HTTPException(400, "Username already exists") - user = User(username=body.username, password_hash=hash_password(body.password)) - db.add(user) - db.commit() - return {"message": "ok"} - - -@router.post("/login", response_model=dict) -def api_login(body: LoginRequest, db: Session = Depends(get_db)): - user = db.query(User).filter(User.username == body.username).first() - if not user or not verify_password(body.password, user.password_hash): - raise HTTPException(401, "Invalid credentials") - api_key = ApiKey(user_id=user.id, key=ApiKey.generate_key()) - db.add(api_key) - db.commit() - return {"api_key": api_key.key} - - -# ---------- Profile ---------- - -def _get_profile(profile_id: int, user: User, db: Session) -> Profile: - p = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first() - if not p: - raise HTTPException(404, "Profile not found") - return p - - -@router.get("/profiles", response_model=list[ProfileOut]) -def api_list_profiles(user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - return db.query(Profile).filter(Profile.user_id == user.id).all() - - -@router.post("/profiles", response_model=ProfileOut, status_code=201) -def api_create_profile(body: ProfileCreate, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - p = Profile(user_id=user.id, nickname=body.nickname) - db.add(p) - db.commit() - db.refresh(p) - return p - - -@router.put("/profiles/{profile_id}", response_model=ProfileOut) -def api_update_profile(profile_id: int, body: ProfileCreate, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - p = _get_profile(profile_id, user, db) - p.nickname = body.nickname - db.commit() - db.refresh(p) - return p - - -@router.delete("/profiles/{profile_id}") -def api_delete_profile(profile_id: int, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - p = _get_profile(profile_id, user, db) - db.delete(p) - db.commit() - return {"message": "ok"} - - -# ---------- Postcard ---------- - -def _get_postcard(postcard_id: int, user: User, db: Session) -> Postcard: - pc = ( - db.query(Postcard) - .join(Profile) - .filter(Postcard.id == postcard_id, Profile.user_id == user.id) - .first() - ) - if not pc: - raise HTTPException(404, "Postcard not found") - return pc - - -@router.get("/profiles/{profile_id}/postcards", response_model=list[PostcardOut]) -def api_list_postcards( - profile_id: int, - status: str | None = None, - country: str | None = None, - user: User = Depends(get_current_user_api), - db: Session = Depends(get_db), -): - _get_profile(profile_id, user, db) - q = db.query(Postcard).filter(Postcard.profile_id == profile_id) - if status: - q = q.filter(Postcard.status == status) - if country: - q = q.filter(Postcard.country.ilike(f"%{country}%")) - return q.order_by(Postcard.created_at.desc()).all() - - -@router.post("/profiles/{profile_id}/postcards", response_model=PostcardOut, status_code=201) -def api_create_postcard( - profile_id: int, - body: PostcardCreate, - user: User = Depends(get_current_user_api), - db: Session = Depends(get_db), -): - _get_profile(profile_id, user, db) - pc = Postcard(profile_id=profile_id, **body.model_dump()) - db.add(pc) - db.commit() - db.refresh(pc) - return pc - - -@router.get("/postcards/{postcard_id}", response_model=PostcardOut) -def api_get_postcard(postcard_id: int, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - return _get_postcard(postcard_id, user, db) - - -@router.put("/postcards/{postcard_id}", response_model=PostcardOut) -def api_update_postcard( - postcard_id: int, - body: PostcardUpdate, - user: User = Depends(get_current_user_api), - db: Session = Depends(get_db), -): - pc = _get_postcard(postcard_id, user, db) - for k, v in body.model_dump(exclude_unset=True).items(): - setattr(pc, k, v) - db.commit() - db.refresh(pc) - return pc - - -@router.delete("/postcards/{postcard_id}") -def api_delete_postcard(postcard_id: int, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - pc = _get_postcard(postcard_id, user, db) - db.delete(pc) - db.commit() - return {"message": "ok"} - - -# ---------- Image Upload ---------- - -@router.post("/postcards/{postcard_id}/image/{side}", response_model=PostcardOut) -async def api_upload_image( - postcard_id: int, - side: str, - file: UploadFile = File(...), - user: User = Depends(get_current_user_api), - db: Session = Depends(get_db), -): - if side not in ("front", "back"): - raise HTTPException(400, "side must be 'front' or 'back'") - pc = _get_postcard(postcard_id, user, db) - ext = Path(file.filename or "upload.jpg").suffix or ".jpg" - filename = f"{uuid4().hex}{ext}" - dest = UPLOAD_DIR / filename - content = await file.read() - dest.write_bytes(content) - setattr(pc, f"image_{side}", f"/uploads/{filename}") - db.commit() - db.refresh(pc) - return pc - - -# ---------- API Key ---------- - -@router.get("/api-keys", response_model=list[ApiKeyOut]) -def api_list_keys(user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - return db.query(ApiKey).filter(ApiKey.user_id == user.id).all() - - -@router.post("/api-keys", response_model=ApiKeyOut, status_code=201) -def api_create_key(user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - ak = ApiKey(user_id=user.id, key=ApiKey.generate_key()) - db.add(ak) - db.commit() - db.refresh(ak) - return ak - - -@router.delete("/api-keys/{key_id}") -def api_delete_key(key_id: int, user: User = Depends(get_current_user_api), db: Session = Depends(get_db)): - ak = db.query(ApiKey).filter(ApiKey.id == key_id, ApiKey.user_id == user.id).first() - if not ak: - raise HTTPException(404, "API key not found") - db.delete(ak) - db.commit() - return {"message": "ok"} diff --git a/app/routers/web.py b/app/routers/web.py index 696c600..ef0561d 100644 --- a/app/routers/web.py +++ b/app/routers/web.py @@ -17,7 +17,7 @@ from app.auth import ( ) from app.config import SECRET_KEY, SESSION_COOKIE_NAME, UPLOAD_DIR from app.database import get_db -from app.models import ApiKey, Postcard, Profile, User +from app.models import Postcard, Profile, User router = APIRouter(tags=["web"]) templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates")) @@ -71,35 +71,69 @@ def login_submit( return resp -@router.get("/register", response_class=HTMLResponse) -def register_page(request: Request): - return templates.TemplateResponse("register.html", {"request": request}) +# ---------- Settings ---------- - -@router.post("/register") -def register_submit( +@router.get("/settings", response_class=HTMLResponse) +def settings_page( request: Request, - username: str = Form(...), + user: User = Depends(get_current_user_web), +): + return templates.TemplateResponse("settings.html", {"request": request, "user": user}) + + +@router.post("/settings/change-username") +def change_username( + request: Request, + new_username: str = Form(...), password: str = Form(...), - password2: str = Form(...), + user: User = Depends(get_current_user_web), db: Session = Depends(get_db), ): - if password != password2: + if not verify_password(password, user.password_hash): return templates.TemplateResponse( - "register.html", {"request": request, "error": "两次密码不一致"}, status_code=400 + "settings.html", {"request": request, "user": user, "error": "密码错误"}, status_code=400 ) - if db.query(User).filter(User.username == username).first(): + if db.query(User).filter(User.username == new_username, User.id != user.id).first(): return templates.TemplateResponse( - "register.html", {"request": request, "error": "用户名已存在"}, status_code=400 + "settings.html", {"request": request, "user": user, "error": "用户名已存在"}, status_code=400 ) - user = User(username=username, password_hash=hash_password(password)) - db.add(user) + user.username = new_username db.commit() - resp = _redirect("/dashboard") - resp.set_cookie(SESSION_COOKIE_NAME, create_session(user.id), httponly=True) - return resp + return templates.TemplateResponse( + "settings.html", {"request": request, "user": user, "success": "用户名修改成功"} + ) +@router.post("/settings/change-password") +def change_password( + request: Request, + old_password: str = Form(...), + new_password: str = Form(...), + confirm_password: str = Form(...), + user: User = Depends(get_current_user_web), + db: Session = Depends(get_db), +): + if not verify_password(old_password, user.password_hash): + return templates.TemplateResponse( + "settings.html", {"request": request, "user": user, "error": "当前密码错误"}, status_code=400 + ) + if new_password != confirm_password: + return templates.TemplateResponse( + "settings.html", {"request": request, "user": user, "error": "两次输入的新密码不一致"}, status_code=400 + ) + if len(new_password) < 6: + return templates.TemplateResponse( + "settings.html", {"request": request, "user": user, "error": "新密码长度至少6位"}, status_code=400 + ) + user.password_hash = hash_password(new_password) + db.commit() + return templates.TemplateResponse( + "settings.html", {"request": request, "user": user, "success": "密码修改成功"} + ) + + +# ---------- Auth ---------- + @router.get("/logout") def logout(request: Request, db: Session = Depends(get_db)): token = request.cookies.get(SESSION_COOKIE_NAME) diff --git a/app/schemas.py b/app/schemas.py index c44d73c..4bd06f7 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -3,18 +3,6 @@ from datetime import datetime from pydantic import BaseModel -# ---------- Auth ---------- - -class RegisterRequest(BaseModel): - username: str - password: str - - -class LoginRequest(BaseModel): - username: str - password: str - - # ---------- Profile ---------- class ProfileCreate(BaseModel): @@ -33,7 +21,7 @@ class ProfileOut(BaseModel): class PostcardCreate(BaseModel): card_number: str - status: str # sent / delivered / received + status: str recipient_name: str | None = None country: str | None = None send_time: datetime | None = None @@ -50,7 +38,7 @@ class PostcardUpdate(BaseModel): send_time: datetime | None = None arrival_time: datetime | None = None sender_name: str | None = None - receive_time: datetime | None = None + receive_time: str | None = None class PostcardOut(BaseModel): @@ -70,13 +58,3 @@ class PostcardOut(BaseModel): updated_at: datetime model_config = {"from_attributes": True} - - -# ---------- ApiKey ---------- - -class ApiKeyOut(BaseModel): - id: int - key: str - created_at: datetime - - model_config = {"from_attributes": True} diff --git a/app/templates/api_keys.html b/app/templates/api_keys.html deleted file mode 100644 index e169a14..0000000 --- a/app/templates/api_keys.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base.html" %} -{% block title %}API Keys - Postcard Manager{% endblock %} -{% block content %} -
| Key | 创建时间 | 操作 |
|---|---|---|
{{ k.key }} |
- {{ k.created_at.strftime('%Y-%m-%d %H:%M') }} | -- - | -
还没有 API Key,点击上方按钮创建一个。
-{% endif %} -curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8000/api/profiles-
没有账号?注册
{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html deleted file mode 100644 index 5554fba..0000000 --- a/app/templates/register.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} -{% block title %}注册 - Postcard Manager{% endblock %} -{% block content %} - -{% endblock %} diff --git a/app/templates/settings.html b/app/templates/settings.html new file mode 100644 index 0000000..940d096 --- /dev/null +++ b/app/templates/settings.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}用户设置 - Postcard Manager{% endblock %} +{% block content %} +