445 lines
15 KiB
Python
445 lines
15 KiB
Python
from datetime import datetime
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.auth import (
|
|
create_session,
|
|
destroy_session,
|
|
get_current_user_web,
|
|
hash_password,
|
|
redirect_if_not_logged_in,
|
|
verify_password,
|
|
)
|
|
from app.config import SECRET_KEY, SESSION_COOKIE_NAME, UPLOAD_DIR
|
|
from app.database import get_db
|
|
from app.models import Postcard, Profile, User
|
|
|
|
router = APIRouter(tags=["web"])
|
|
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
|
|
|
|
|
def _country_flag(code: str) -> str:
|
|
if not code or len(code) != 2:
|
|
return code or ""
|
|
return chr(0x1F1E6 + ord(code[0].upper()) - ord("A")) + chr(0x1F1E6 + ord(code[1].upper()) - ord("A"))
|
|
|
|
|
|
templates.env.filters["flag"] = _country_flag
|
|
|
|
|
|
def _redirect(url: str) -> RedirectResponse:
|
|
return RedirectResponse(url, status_code=303)
|
|
|
|
|
|
# ---------- Auth ----------
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
def login_page(request: Request, db: Session = Depends(get_db)):
|
|
user = redirect_if_not_logged_in(request, db)
|
|
if user:
|
|
return _redirect("/dashboard")
|
|
return templates.TemplateResponse("login.html", {"request": request})
|
|
|
|
|
|
@router.post("/login")
|
|
def login_submit(
|
|
request: Request,
|
|
username: str = Form(...),
|
|
password: str = Form(...),
|
|
remember_me: str = Form(""),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
from app.auth import cleanup_expired_sessions
|
|
cleanup_expired_sessions(db)
|
|
user = db.query(User).filter(User.username == username).first()
|
|
if not user or not verify_password(password, user.password_hash):
|
|
return templates.TemplateResponse(
|
|
"login.html", {"request": request, "error": "用户名或密码错误"}, status_code=401
|
|
)
|
|
is_remember = remember_me == "on"
|
|
token = create_session(user.id, db, remember_me=is_remember)
|
|
resp = _redirect("/dashboard")
|
|
cookie_kwargs = {"httponly": True, "samesite": "lax"}
|
|
if is_remember:
|
|
cookie_kwargs["max_age"] = 365 * 24 * 3600
|
|
resp.set_cookie(SESSION_COOKIE_NAME, token, **cookie_kwargs)
|
|
return resp
|
|
|
|
|
|
# ---------- Settings ----------
|
|
|
|
@router.get("/settings", response_class=HTMLResponse)
|
|
def settings_page(
|
|
request: Request,
|
|
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(...),
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
if db.query(User).filter(User.username == new_username, User.id != user.id).first():
|
|
return templates.TemplateResponse(
|
|
"settings.html", {"request": request, "user": user, "error": "用户名已存在"}, status_code=400
|
|
)
|
|
user.username = new_username
|
|
db.commit()
|
|
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)
|
|
destroy_session(token or "", db)
|
|
resp = _redirect("/login")
|
|
resp.delete_cookie(SESSION_COOKIE_NAME)
|
|
return resp
|
|
|
|
|
|
# ---------- Dashboard ----------
|
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
def root(user: User = Depends(get_current_user_web)):
|
|
return _redirect("/dashboard")
|
|
|
|
|
|
@router.get("/dashboard", response_class=HTMLResponse)
|
|
def dashboard(request: Request, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
profiles = db.query(Profile).filter(Profile.user_id == user.id).all()
|
|
stats = {}
|
|
extras = {}
|
|
for p in profiles:
|
|
cards = db.query(Postcard).filter(Postcard.profile_id == p.id).order_by(Postcard.created_at.desc()).all()
|
|
stats[p.id] = {
|
|
"total": len(cards),
|
|
"sent": sum(1 for c in cards if c.status == "sent"),
|
|
"delivered": sum(1 for c in cards if c.status == "delivered"),
|
|
"received": sum(1 for c in cards if c.status == "received"),
|
|
}
|
|
countries = sorted({c.country for c in cards if c.country})
|
|
recent = cards[:3]
|
|
extras[p.id] = {"countries": countries, "recent": recent}
|
|
return templates.TemplateResponse("dashboard.html", {"request": request, "user": user, "profiles": profiles, "stats": stats, "extras": extras})
|
|
|
|
|
|
# ---------- Profiles ----------
|
|
|
|
@router.get("/profiles", response_class=HTMLResponse)
|
|
def profile_list(request: Request, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
profiles = db.query(Profile).filter(Profile.user_id == user.id).all()
|
|
return templates.TemplateResponse("profiles.html", {"request": request, "user": user, "profiles": profiles})
|
|
|
|
|
|
@router.post("/profiles")
|
|
def profile_create(request: Request, nickname: str = Form(...), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
db.add(Profile(user_id=user.id, nickname=nickname))
|
|
db.commit()
|
|
return _redirect("/profiles")
|
|
|
|
|
|
@router.post("/profiles/{profile_id}/delete")
|
|
def profile_delete(profile_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
p = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
|
if p:
|
|
db.delete(p)
|
|
db.commit()
|
|
return _redirect("/profiles")
|
|
|
|
|
|
@router.post("/profiles/{profile_id}/rename")
|
|
def profile_rename(profile_id: int, nickname: str = Form(...), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
p = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
|
if p:
|
|
p.nickname = nickname
|
|
db.commit()
|
|
return _redirect("/profiles")
|
|
|
|
|
|
# ---------- Postcards ----------
|
|
|
|
@router.get("/profiles/{profile_id}/postcards", response_class=HTMLResponse)
|
|
def postcard_list(
|
|
request: Request,
|
|
profile_id: int,
|
|
status: str = "",
|
|
country: str = "",
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
profile = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
|
if not profile:
|
|
return _redirect("/profiles")
|
|
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}%"))
|
|
postcards = q.order_by(Postcard.created_at.desc()).all()
|
|
return templates.TemplateResponse(
|
|
"postcards.html",
|
|
{"request": request, "user": user, "profile": profile, "postcards": postcards, "status": status, "country": country},
|
|
)
|
|
|
|
|
|
@router.get("/profiles/{profile_id}/postcards/new", response_class=HTMLResponse)
|
|
def postcard_new(request: Request, profile_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
profile = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
|
if not profile:
|
|
return _redirect("/profiles")
|
|
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": profile, "postcard": None})
|
|
|
|
|
|
@router.post("/profiles/{profile_id}/postcards")
|
|
def postcard_create(
|
|
request: Request,
|
|
profile_id: int,
|
|
card_number: str = Form(...),
|
|
status: str = Form(...),
|
|
recipient_name: str = Form(""),
|
|
country: str = Form(""),
|
|
send_time: str = Form(""),
|
|
arrival_time: str = Form(""),
|
|
sender_name: str = Form(""),
|
|
receive_time: str = Form(""),
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
profile = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first()
|
|
if not profile:
|
|
return _redirect("/profiles")
|
|
|
|
def _parse_dt(s: str) -> datetime | None:
|
|
s = s.strip()
|
|
if not s:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(s)
|
|
except ValueError:
|
|
try:
|
|
return datetime.strptime(s, "%Y-%m-%d")
|
|
except ValueError:
|
|
return None
|
|
|
|
pc = Postcard(
|
|
profile_id=profile_id,
|
|
card_number=card_number,
|
|
status=status,
|
|
recipient_name=recipient_name or None,
|
|
country=(country.strip().upper() if country else None),
|
|
send_time=_parse_dt(send_time),
|
|
arrival_time=_parse_dt(arrival_time),
|
|
sender_name=sender_name or None,
|
|
receive_time=_parse_dt(receive_time),
|
|
)
|
|
db.add(pc)
|
|
db.commit()
|
|
return _redirect(f"/profiles/{profile_id}/postcards")
|
|
|
|
|
|
@router.get("/postcards/{postcard_id}", response_class=HTMLResponse)
|
|
def postcard_detail(request: Request, postcard_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
return templates.TemplateResponse("postcard_detail.html", {"request": request, "user": user, "postcard": pc})
|
|
|
|
|
|
@router.get("/postcards/{postcard_id}/edit", response_class=HTMLResponse)
|
|
def postcard_edit(request: Request, postcard_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": pc.profile, "postcard": pc})
|
|
|
|
|
|
@router.post("/postcards/{postcard_id}/edit")
|
|
def postcard_update(
|
|
request: Request,
|
|
postcard_id: int,
|
|
card_number: str = Form(...),
|
|
status: str = Form(...),
|
|
recipient_name: str = Form(""),
|
|
country: str = Form(""),
|
|
send_time: str = Form(""),
|
|
arrival_time: str = Form(""),
|
|
sender_name: str = Form(""),
|
|
receive_time: str = Form(""),
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
|
|
def _parse_dt(s: str) -> datetime | None:
|
|
s = s.strip()
|
|
if not s:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(s)
|
|
except ValueError:
|
|
try:
|
|
return datetime.strptime(s, "%Y-%m-%d")
|
|
except ValueError:
|
|
return None
|
|
|
|
pc.card_number = card_number
|
|
pc.status = status
|
|
pc.recipient_name = recipient_name or None
|
|
pc.country = country or None
|
|
pc.send_time = _parse_dt(send_time)
|
|
pc.arrival_time = _parse_dt(arrival_time)
|
|
pc.sender_name = sender_name or None
|
|
pc.receive_time = _parse_dt(receive_time)
|
|
db.commit()
|
|
return _redirect(f"/postcards/{postcard_id}")
|
|
|
|
|
|
@router.post("/postcards/{postcard_id}/delete")
|
|
def postcard_delete(postcard_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
pid = pc.profile_id
|
|
db.delete(pc)
|
|
db.commit()
|
|
return _redirect(f"/profiles/{pid}/postcards")
|
|
|
|
|
|
@router.post("/postcards/{postcard_id}/mark-delivered")
|
|
def mark_delivered(
|
|
postcard_id: int,
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
pc.status = "delivered"
|
|
if not pc.arrival_time:
|
|
pc.arrival_time = datetime.now()
|
|
db.commit()
|
|
return _redirect(f"/postcards/{postcard_id}")
|
|
|
|
|
|
# ---------- Image Upload ----------
|
|
|
|
@router.post("/postcards/{postcard_id}/image/{side}")
|
|
async def upload_image(
|
|
postcard_id: int,
|
|
side: str,
|
|
file: UploadFile = File(...),
|
|
user: User = Depends(get_current_user_web),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
if side not in ("front", "back"):
|
|
return _redirect("/profiles")
|
|
pc = (
|
|
db.query(Postcard)
|
|
.join(Profile)
|
|
.filter(Postcard.id == postcard_id, Profile.user_id == user.id)
|
|
.first()
|
|
)
|
|
if not pc:
|
|
return _redirect("/profiles")
|
|
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()
|
|
return _redirect(f"/postcards/{postcard_id}")
|
|
|
|
|
|
# ---------- API Keys ----------
|
|
|
|
@router.get("/api-keys", response_class=HTMLResponse)
|
|
def api_keys_page(request: Request, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).all()
|
|
return templates.TemplateResponse("api_keys.html", {"request": request, "user": user, "keys": keys})
|
|
|
|
|
|
@router.post("/api-keys")
|
|
def api_key_create(user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
ak = ApiKey(user_id=user.id, key=ApiKey.generate_key())
|
|
db.add(ak)
|
|
db.commit()
|
|
return _redirect("/api-keys")
|
|
|
|
|
|
@router.post("/api-keys/{key_id}/delete")
|
|
def api_key_delete(key_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
|
ak = db.query(ApiKey).filter(ApiKey.id == key_id, ApiKey.user_id == user.id).first()
|
|
if ak:
|
|
db.delete(ak)
|
|
db.commit()
|
|
return _redirect("/api-keys")
|