feat(auth): switch to bcrypt primary scheme, keep sha256_crypt for migration

- database.py: CryptContext = ['bcrypt', 'sha256_crypt'] with sha256_crypt deprecated
- database.py: add verify_and_update_password() returning (ok, new_hash)
- main.py: login uses verify_and_update; on successful verify transparently
  commits new bcrypt hash when old hash is sha256_crypt
- requirements.txt: passlib[bcrypt]==1.7.4 + bcrypt==4.0.1 (compat with passlib 1.7.4)

Migration: zero-touch. Admin's first login after this deploy will
auto-upgrade the stored hash from $5$... to $2b$...

Avoids the passlib sha256_crypt cross-version verify bug that bit us
this morning. bcrypt is OpenSSL-native and stable across passlib
versions.
This commit is contained in:
IT狗
2026-07-22 12:07:51 +08:00
parent f6712d9d0e
commit acbc767247
3 changed files with 29 additions and 4 deletions
+13 -2
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from typing import Optional, List
import json
from database import get_db, create_access_token, verify_password, get_password_hash, init_db, Base, engine
from database import get_db, create_access_token, verify_password, verify_and_update_password, get_password_hash, init_db, Base, engine
from models import User, AttendanceRecord, Shift, Accident, Holiday, LeaveRecord
from sqlalchemy import func as sqlfunc
from schemas import *
@@ -254,9 +254,20 @@ async def startup():
@app.post("/api/auth/login", response_model=TokenResponse)
async def login(form_data: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == form_data.email).first()
if not user or not verify_password(form_data.password, user.password_hash):
if not user:
logger.warning("login failed", extra={"context": {"email": form_data.email, "reason": "user_not_found"}})
raise HTTPException(status_code=401, detail="Invalid email or password")
# verify_and_update returns (ok, new_hash_if_needs_rehash)
ok, new_hash = verify_and_update_password(form_data.password, user.password_hash)
if not ok:
logger.warning("login failed", extra={"context": {"email": form_data.email, "reason": "invalid_credentials"}})
raise HTTPException(status_code=401, detail="Invalid email or password")
# Migrate hash scheme in place (e.g. sha256_crypt -> bcrypt) without forcing logout
if new_hash:
user.password_hash = new_hash
db.commit()
db.refresh(user)
logger.info("password hash upgraded", extra={"context": {"user_id": user.id, "email": form_data.email}})
set_user_id(user.id)
access_token = create_access_token({"sub": str(user.id)})
logger.info("login success", extra={"context": {"user_id": user.id, "email": form_data.email}})