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
|
import os
|
||||||
|
|
||||||
from backend.database import get_db, init_db, seed_defaults, log_audit
|
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")
|
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
|
# Periodic WAL checkpoint to ensure data is flushed to main DB file
|
||||||
import threading, time as _time
|
import threading, time as _time
|
||||||
def _wal_checkpoint_loop():
|
def _wal_checkpoint_loop():
|
||||||
@@ -312,7 +338,6 @@ def symptom_search(body: dict, user=Depends(get_current_user)):
|
|||||||
# ── Register ────────────────────────────────────────────
|
# ── Register ────────────────────────────────────────────
|
||||||
@app.post("/api/register", status_code=201)
|
@app.post("/api/register", status_code=201)
|
||||||
def register(body: dict):
|
def register(body: dict):
|
||||||
import secrets
|
|
||||||
username = body.get("username", "").strip()
|
username = body.get("username", "").strip()
|
||||||
password = body.get("password", "").strip()
|
password = body.get("password", "").strip()
|
||||||
display_name = body.get("display_name", "").strip()
|
display_name = body.get("display_name", "").strip()
|
||||||
@@ -320,12 +345,12 @@ def register(body: dict):
|
|||||||
raise HTTPException(400, "用户名至少2个字符")
|
raise HTTPException(400, "用户名至少2个字符")
|
||||||
if not password or len(password) < 4:
|
if not password or len(password) < 4:
|
||||||
raise HTTPException(400, "密码至少4位")
|
raise HTTPException(400, "密码至少4位")
|
||||||
token = secrets.token_hex(24)
|
token = _secrets.token_hex(24)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)",
|
"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()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -343,14 +368,19 @@ def login(body: dict):
|
|||||||
if not username or not password:
|
if not username or not password:
|
||||||
raise HTTPException(400, "请输入用户名和密码")
|
raise HTTPException(400, "请输入用户名和密码")
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
user = conn.execute("SELECT token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone()
|
user = conn.execute("SELECT id, token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone()
|
||||||
conn.close()
|
|
||||||
if not user:
|
if not user:
|
||||||
|
conn.close()
|
||||||
raise HTTPException(401, "用户名不存在")
|
raise HTTPException(401, "用户名不存在")
|
||||||
if not user["password"]:
|
if not user["password"]:
|
||||||
|
conn.close()
|
||||||
raise HTTPException(401, "该账号未设置密码,请使用链接登录后设置密码")
|
raise HTTPException(401, "该账号未设置密码,请使用链接登录后设置密码")
|
||||||
if user["password"] != password:
|
if not verify_password(password, user["password"]):
|
||||||
|
conn.close()
|
||||||
raise HTTPException(401, "密码错误")
|
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"]}
|
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位")
|
raise HTTPException(400, "新密码至少4位")
|
||||||
old_pw = body.get("old_password", "").strip()
|
old_pw = body.get("old_password", "").strip()
|
||||||
current_pw = user.get("password") or ""
|
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()
|
conn.close()
|
||||||
raise HTTPException(400, "当前密码不正确")
|
raise HTTPException(400, "当前密码不正确")
|
||||||
if pw:
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -404,7 +434,7 @@ def set_password(body: dict, user=Depends(get_current_user)):
|
|||||||
if not pw or len(pw) < 4:
|
if not pw or len(pw) < 4:
|
||||||
raise HTTPException(400, "密码至少4位")
|
raise HTTPException(400, "密码至少4位")
|
||||||
conn = get_db()
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -896,8 +926,7 @@ def list_users(user=Depends(require_role("admin"))):
|
|||||||
|
|
||||||
@app.post("/api/users", status_code=201)
|
@app.post("/api/users", status_code=201)
|
||||||
def create_user(body: UserIn, user=Depends(require_role("admin"))):
|
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()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
Reference in New Issue
Block a user