fix(auth): implement DB-first auth, persistent bearer token, secure cookies, and CLI reset_password script

This commit is contained in:
Antigravity committed 2026-10-05 15:23:37 +03:00
1 parent a73bdc29fd
commit 99741c386e
5 files changed
+120 -7

No files matched your search

+14 -3
View File
@@ -215,7 +215,12 @@ async def index_page(request: Request):
@app.post("/api/login")
async def api_login(req: LoginRequest, request: Request, response: Response):
ip = request.client.host if request.client else "unknown"
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
ip = forwarded.split(",")[0].strip()
else:
ip = request.headers.get("x-real-ip") or (request.client.host if request.client else "unknown")
if not check_login_rate_limit(ip):
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
@@ -234,15 +239,21 @@ async def api_login(req: LoginRequest, request: Request, response: Response):
user_agent = request.headers.get("User-Agent", "")
session_token = create_session(user["id"], user_agent=user_agent, ip=ip)
is_secure = request.headers.get("x-forwarded-proto", "").lower() == "https" or request.url.scheme == "https"
response.set_cookie(
key=config.SESSION_COOKIE_NAME,
value=session_token,
max_age=config.SESSION_MAX_AGE,
httponly=True,
samesite="lax",
secure=False,
secure=is_secure,
)
return {"status": "ok", "user": {"id": user["id"], "username": user["username"]}}
return {
"status": "ok",
"user": {"id": user["id"], "username": user["username"]},
"token": session_token,
}
@app.post("/api/logout")
+3 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Notes — Персональный тренажер и словарь английского языка</title>
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
<link rel="stylesheet" href="/static/css/style.css?v=2.8">
<link rel="stylesheet" href="/static/css/style.css?v=2.9">
<!-- PWA meta and icons -->
<link rel="manifest" href="/manifest.json">
@@ -928,6 +928,7 @@
<!-- Audio Element for native streaming -->
<audio id="global-audio-player" preload="none"></audio>
<script src="/static/js/app.js?v=2.8"></script>
<script src="/static/js/app.js?v=2.9"></script>
</body>
</html>
+29 -1
View File
@@ -1,5 +1,27 @@
// Notes Application Frontend Logic - Complete SRS, Heatmap, Quiz, and Analytics
document.addEventListener('DOMContentLoaded', () => {
// Persistent session: auto-inject Authorization header if token exists in localStorage
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
const token = localStorage.getItem('notes_token');
if (token && typeof url === 'string' && url.startsWith('/api/')) {
options.headers = options.headers || {};
if (options.headers instanceof Headers) {
if (!options.headers.has('Authorization')) {
options.headers.set('Authorization', `Bearer ${token}`);
}
} else if (Array.isArray(options.headers)) {
if (!options.headers.some(([k]) => k.toLowerCase() === 'authorization')) {
options.headers.push(['Authorization', `Bearer ${token}`]);
}
} else {
if (!options.headers['Authorization']) {
options.headers['Authorization'] = `Bearer ${token}`;
}
}
}
return originalFetch.call(this, url, options);
};
// Register PWA Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
@@ -7,6 +29,7 @@ document.addEventListener('DOMContentLoaded', () => {
.catch(err => console.log('Notes ServiceWorker failed:', err));
}
// App State
let currentUser = null;
let currentLookupData = null;
@@ -385,6 +408,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
const data = await resp.json();
if (resp.ok && data.status === 'ok') {
if (data.token) {
localStorage.setItem('notes_token', data.token);
}
currentUser = data.user;
showApp();
loadInitialData();
@@ -399,11 +425,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
logoutBtn.addEventListener('click', async () => {
localStorage.removeItem('notes_token');
await fetch('/api/logout', { method: 'POST' });
currentUser = null;
window.location.reload();
});
// -----------------------------------------------------------------------
// Data Loading: Cards, Collections, Tags, Stats
// -----------------------------------------------------------------------
+1 -1
View File
@@ -14,5 +14,5 @@ services:
- DEBUG=False
- SECRET_KEY=study-qpid-pro-secret-key-change-in-prod
- ADMIN_USERNAME=kopri
- ADMIN_PASSWORD=kopri2026pass
- DB_PATH=/app/data/notes.db
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
CLI utility to safely reset a user password directly in the database.
Usage:
python3 scripts/reset_password.py <new_password>
python3 scripts/reset_password.py <username> <new_password>
"""
import sys
from pathlib import Path
# Add project root to sys.path
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE_DIR))
from app.database import init_db, get_db
from app.auth import hash_password, verify_password, _login_failures
def reset_password(username: str, new_password: str):
if not new_password or len(new_password) < 6:
print("❌ Ошибка: Пароль должен содержать минимум 6 символов.")
sys.exit(1)
init_db()
clean_user = username.strip()
pwd_hash, salt = hash_password(new_password)
with get_db() as conn:
row = conn.execute(
"SELECT id, username FROM users WHERE username = ? COLLATE NOCASE",
(clean_user,)
).fetchone()
if not row:
# If user does not exist, create it
cursor = conn.execute(
"INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)",
(clean_user, pwd_hash, salt)
)
print(f"✅ Создан новый пользователь '{clean_user}' (ID: {cursor.lastrowid}).")
else:
conn.execute(
"UPDATE users SET password_hash = ?, salt = ? WHERE id = ?",
(pwd_hash, salt, row["id"])
)
print(f"✅ Пароль для пользователя '{row['username']}' (ID: {row['id']}) успешно обновлен.")
# Clear login failures rate limit
_login_failures.clear()
# Verification check
check = conn.execute(
"SELECT password_hash, salt FROM users WHERE username = ? COLLATE NOCASE",
(clean_user,)
).fetchone()
if check and verify_password(new_password, check["password_hash"], check["salt"]):
print("🔒 Проверка хэша PBKDF2-HMAC-SHA256: УСПЕШНО.")
else:
print("⚠️ Предупреждение: Не удалось верифицировать хэш пароля.")
if __name__ == "__main__":
args = sys.argv[1:]
if len(args) == 1:
# Default to user kopri
reset_password("kopri", args[0])
elif len(args) == 2:
reset_password(args[0], args[1])
else:
print("Использование:")
print(" python3 scripts/reset_password.py <новый_пароль>")
print(" python3 scripts/reset_password.py <имя_пользователя> <новый_пароль>")
sys.exit(1)