Hash passwords with PBKDF2-SHA256 instead of storing plaintext
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 11s
Test / e2e-test (push) Successful in 53s
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 11s
Test / e2e-test (push) Successful in 53s
Existing plaintext passwords are auto-upgraded to hashed on next login. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,35 @@ import json
|
||||
import os
|
||||
|
||||
from backend.database import get_db, init_db, seed_defaults, log_audit
|
||||
import hashlib
|
||||
import secrets as _secrets
|
||||
|
||||
app = FastAPI(title="Essential Oil Formula Calculator API")
|
||||
|
||||
|
||||
# ── Password hashing (PBKDF2-SHA256, stdlib) ─────────
|
||||
def hash_password(password: str) -> str:
|
||||
salt = _secrets.token_hex(16)
|
||||
h = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000)
|
||||
return f"{salt}${h.hex()}"
|
||||
|
||||
|
||||
def verify_password(password: str, stored: str) -> bool:
|
||||
if not stored:
|
||||
return False
|
||||
if "$" not in stored:
|
||||
# Legacy plaintext — compare directly
|
||||
return password == stored
|
||||
salt, h = stored.split("$", 1)
|
||||
return hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000).hex() == h
|
||||
|
||||
|
||||
def _upgrade_password_if_needed(conn, user_id: int, password: str, stored: str):
|
||||
"""If stored password is legacy plaintext, upgrade to hashed."""
|
||||
if stored and "$" not in stored:
|
||||
conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(password), user_id))
|
||||
conn.commit()
|
||||
|
||||
# Periodic WAL checkpoint to ensure data is flushed to main DB file
|
||||
import threading, time as _time
|
||||
def _wal_checkpoint_loop():
|
||||
@@ -312,7 +338,6 @@ def symptom_search(body: dict, user=Depends(get_current_user)):
|
||||
# ── Register ────────────────────────────────────────────
|
||||
@app.post("/api/register", status_code=201)
|
||||
def register(body: dict):
|
||||
import secrets
|
||||
username = body.get("username", "").strip()
|
||||
password = body.get("password", "").strip()
|
||||
display_name = body.get("display_name", "").strip()
|
||||
@@ -320,12 +345,12 @@ def register(body: dict):
|
||||
raise HTTPException(400, "用户名至少2个字符")
|
||||
if not password or len(password) < 4:
|
||||
raise HTTPException(400, "密码至少4位")
|
||||
token = secrets.token_hex(24)
|
||||
token = _secrets.token_hex(24)
|
||||
conn = get_db()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)",
|
||||
(username, token, "viewer", display_name or username, password)
|
||||
(username, token, "viewer", display_name or username, hash_password(password))
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
@@ -343,14 +368,19 @@ def login(body: dict):
|
||||
if not username or not password:
|
||||
raise HTTPException(400, "请输入用户名和密码")
|
||||
conn = get_db()
|
||||
user = conn.execute("SELECT token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone()
|
||||
conn.close()
|
||||
user = conn.execute("SELECT id, token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone()
|
||||
if not user:
|
||||
conn.close()
|
||||
raise HTTPException(401, "用户名不存在")
|
||||
if not user["password"]:
|
||||
conn.close()
|
||||
raise HTTPException(401, "该账号未设置密码,请使用链接登录后设置密码")
|
||||
if user["password"] != password:
|
||||
if not verify_password(password, user["password"]):
|
||||
conn.close()
|
||||
raise HTTPException(401, "密码错误")
|
||||
# Auto-upgrade legacy plaintext password to hashed
|
||||
_upgrade_password_if_needed(conn, user["id"], password, user["password"])
|
||||
conn.close()
|
||||
return {"token": user["token"], "display_name": user["display_name"], "role": user["role"]}
|
||||
|
||||
|
||||
@@ -385,11 +415,11 @@ def update_me(body: dict, user=Depends(get_current_user)):
|
||||
raise HTTPException(400, "新密码至少4位")
|
||||
old_pw = body.get("old_password", "").strip()
|
||||
current_pw = user.get("password") or ""
|
||||
if current_pw and old_pw != current_pw:
|
||||
if current_pw and not verify_password(old_pw, current_pw):
|
||||
conn.close()
|
||||
raise HTTPException(400, "当前密码不正确")
|
||||
if pw:
|
||||
conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"]))
|
||||
conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(pw), user["id"]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"ok": True}
|
||||
@@ -404,7 +434,7 @@ def set_password(body: dict, user=Depends(get_current_user)):
|
||||
if not pw or len(pw) < 4:
|
||||
raise HTTPException(400, "密码至少4位")
|
||||
conn = get_db()
|
||||
conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"]))
|
||||
conn.execute("UPDATE users SET password = ? WHERE id = ?", (hash_password(pw), user["id"]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"ok": True}
|
||||
@@ -896,8 +926,7 @@ def list_users(user=Depends(require_role("admin"))):
|
||||
|
||||
@app.post("/api/users", status_code=201)
|
||||
def create_user(body: UserIn, user=Depends(require_role("admin"))):
|
||||
import secrets
|
||||
token = secrets.token_hex(24)
|
||||
token = _secrets.token_hex(24)
|
||||
conn = get_db()
|
||||
try:
|
||||
conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user