Files
Akiba So 72428dc075
Some checks failed
CI / Test (Server, optional) (push) Failing after 2m10s
Windows Build / Build Windows (x64) (push) Failing after 29s
CI / Test (Flutter, Linux) (push) Has been cancelled
CI / Analyze (Flutter) (push) Has been cancelled
Fix bugs across app + server, optimize UI/UX, add Gitea CI
Bug fixes (Flutter):
- Wrap multi-statement DB writes (insert/update/delete note, deleteDocument,
  deletePageData, OCR FTS merge, migrations) in transactions to prevent data
  loss on interruption and a read-modify-write FTS race.
- Fix PdfDocument leaks on exception (try/finally dispose) and preserve image
  aspect ratio when stamping images onto PDF pages.
- Guard file-picker against empty selection (was .single -> crash).
- Fix eraser ConcurrentModificationError and unmodifiable-list crash on PDF
  pages; capture page synchronously on save to stop wrong-page data loss.
- Fix Riverpod DB-not-ready races, broken pull-to-refresh, settings load race,
  and search N+1; transform stored annotations on PDF page rotation.
- Normalize pen pressure for devices without a pressure range.
- PPT: single source of truth for slide strokes so ink displays and exports.

UI/UX:
- Material 3 typography, theme-aware colors (dark-mode fixes), hover cursors
  and right-click/visible actions on desktop, keyboard shortcuts (undo/redo/
  save/find), toolbar overflow handling, friendlier empty states, semantic OCR
  status badges, relative timestamps, 1-based page indicators, large-deck PPT
  navigation, and a scratchpad-scope label in split view.

Server (optional backend):
- Persist JWT secret (was per-process random), block path traversal in storage,
  fix CORS '*'+credentials, add OCR job ownership checks, last-writer-wins sync
  guard, constant-time login, and split out heavy OCR deps so the API/tests run
  without them.

CI: Gitea workflows for format+analyze+test (Linux, system sqlite) and a
Windows release build; pristine `flutter analyze`, all Flutter and server tests
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 03:18:00 +08:00

74 lines
2.4 KiB
Python

"""JWT authentication utilities for BadNote."""
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from .config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer_scheme = HTTPBearer()
def hash_password(password: str) -> str:
"""Hash a plaintext password with bcrypt."""
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
"""Verify a password against its hash."""
return pwd_context.verify(password, password_hash)
# A precomputed hash used to spend roughly the same time verifying a password
# for a non-existent user as for an existing one, so login response timing does
# not leak whether a username exists.
_DUMMY_HASH = pwd_context.hash("badnote-dummy-password")
def dummy_verify() -> None:
"""Run a throwaway bcrypt verification to equalise login timing."""
pwd_context.verify("badnote-dummy-password", _DUMMY_HASH)
def create_access_token(user_id: str) -> str:
"""Create a JWT access token for the given user_id."""
expire = datetime.now(timezone.utc) + timedelta(hours=settings.jwt_expiry_hours)
payload = {
"sub": user_id,
"exp": expire,
"iat": datetime.now(timezone.utc),
"jti": str(uuid4()),
}
return jwt.encode(payload, settings.jwt_secret, algorithm="HS256")
def decode_access_token(token: str) -> dict:
"""Decode and validate a JWT token. Returns the payload dict."""
try:
payload = jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
return payload
except JWTError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
) from exc
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> str:
"""FastAPI dependency: extract user_id from Bearer token."""
payload = decode_access_token(credentials.credentials)
user_id: str | None = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token missing subject",
)
return user_id