feat: add user settings page (change username/password) and remove public registration

This commit is contained in:
2026-07-05 22:01:24 +08:00
parent 5607485c6a
commit ae2ed2d961
12 changed files with 127 additions and 348 deletions

View File

@@ -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"}

View File

@@ -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)