Нормализовать переводы строк в LF
Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
@@ -1,98 +1,98 @@
|
||||
"""Авторизация в дашборд: admin/admin по умолчанию, сессия 30 дней.
|
||||
|
||||
Хэш пароля — PBKDF2-HMAC-SHA256. Токен сессии — случайный, хранится в БД,
|
||||
передаётся httpOnly-кукой. По ТЗ смена пароля — через интерфейс.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from fastapi import Cookie, HTTPException, Response
|
||||
|
||||
from . import config
|
||||
from .db import store
|
||||
|
||||
_ITERATIONS = 200_000
|
||||
|
||||
|
||||
def _hash_password(password: str, salt: str) -> str:
|
||||
return hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), _ITERATIONS).hex()
|
||||
|
||||
|
||||
def _make_salt() -> str:
|
||||
return secrets.token_hex(16)
|
||||
|
||||
|
||||
def ensure_creds() -> None:
|
||||
"""Создаёт учётные данные по умолчанию, если их нет."""
|
||||
if store.scalar("SELECT count(*) FROM creds") == 0:
|
||||
salt = _make_salt()
|
||||
store.execute(
|
||||
"INSERT INTO creds(login, password_hash, salt) VALUES (?, ?, ?)",
|
||||
[config.BOOTSTRAP_LOGIN, _hash_password(config.BOOTSTRAP_PASSWORD, salt), salt],
|
||||
)
|
||||
|
||||
|
||||
def verify_password(login: str, password: str) -> bool:
|
||||
row = store.query_one("SELECT password_hash, salt FROM creds WHERE login = ?", [login])
|
||||
if not row:
|
||||
return False
|
||||
return hmac.compare_digest(row["password_hash"], _hash_password(password, row["salt"]))
|
||||
|
||||
|
||||
def change_password(login: str, old_password: str, new_password: str) -> bool:
|
||||
if not verify_password(login, old_password):
|
||||
return False
|
||||
if len(new_password) < 4:
|
||||
raise HTTPException(400, "Пароль слишком короткий (минимум 4 символа)")
|
||||
salt = _make_salt()
|
||||
store.execute(
|
||||
"UPDATE creds SET password_hash = ?, salt = ? WHERE login = ?",
|
||||
[_hash_password(new_password, salt), salt, login],
|
||||
)
|
||||
# разлогиниваем все старые сессии пользователя
|
||||
store.execute("DELETE FROM sessions WHERE login = ?", [login])
|
||||
return True
|
||||
|
||||
|
||||
def create_session(login: str) -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires = time.time_ns() // 1_000_000 + config.SESSION_DAYS * 86_400_000
|
||||
store.execute("INSERT INTO sessions(token, login, expires) VALUES (?, ?, ?)", [token, login, expires])
|
||||
return token
|
||||
|
||||
|
||||
def resolve_session(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
row = store.query_one(
|
||||
"SELECT login FROM sessions WHERE token = ? AND expires > ?",
|
||||
[token, time.time_ns() // 1_000_000],
|
||||
)
|
||||
return row["login"] if row else None
|
||||
|
||||
|
||||
def set_session_cookie(response: Response, token: str) -> None:
|
||||
response.set_cookie(
|
||||
key=config.COOKIE_NAME,
|
||||
value=token,
|
||||
max_age=config.SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
# secure включается при HTTPS-проксировании; флаг не критичен локально
|
||||
)
|
||||
|
||||
|
||||
def destroy_session(token: str | None) -> None:
|
||||
if token:
|
||||
store.execute("DELETE FROM sessions WHERE token = ?", [token])
|
||||
|
||||
|
||||
def current_login(token: str | None = Cookie(default=None, alias=config.COOKIE_NAME)) -> str | None:
|
||||
login = resolve_session(token)
|
||||
if not login:
|
||||
raise HTTPException(401, "Требуется авторизация")
|
||||
return login
|
||||
"""Авторизация в дашборд: admin/admin по умолчанию, сессия 30 дней.
|
||||
|
||||
Хэш пароля — PBKDF2-HMAC-SHA256. Токен сессии — случайный, хранится в БД,
|
||||
передаётся httpOnly-кукой. По ТЗ смена пароля — через интерфейс.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from fastapi import Cookie, HTTPException, Response
|
||||
|
||||
from . import config
|
||||
from .db import store
|
||||
|
||||
_ITERATIONS = 200_000
|
||||
|
||||
|
||||
def _hash_password(password: str, salt: str) -> str:
|
||||
return hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), _ITERATIONS).hex()
|
||||
|
||||
|
||||
def _make_salt() -> str:
|
||||
return secrets.token_hex(16)
|
||||
|
||||
|
||||
def ensure_creds() -> None:
|
||||
"""Создаёт учётные данные по умолчанию, если их нет."""
|
||||
if store.scalar("SELECT count(*) FROM creds") == 0:
|
||||
salt = _make_salt()
|
||||
store.execute(
|
||||
"INSERT INTO creds(login, password_hash, salt) VALUES (?, ?, ?)",
|
||||
[config.BOOTSTRAP_LOGIN, _hash_password(config.BOOTSTRAP_PASSWORD, salt), salt],
|
||||
)
|
||||
|
||||
|
||||
def verify_password(login: str, password: str) -> bool:
|
||||
row = store.query_one("SELECT password_hash, salt FROM creds WHERE login = ?", [login])
|
||||
if not row:
|
||||
return False
|
||||
return hmac.compare_digest(row["password_hash"], _hash_password(password, row["salt"]))
|
||||
|
||||
|
||||
def change_password(login: str, old_password: str, new_password: str) -> bool:
|
||||
if not verify_password(login, old_password):
|
||||
return False
|
||||
if len(new_password) < 4:
|
||||
raise HTTPException(400, "Пароль слишком короткий (минимум 4 символа)")
|
||||
salt = _make_salt()
|
||||
store.execute(
|
||||
"UPDATE creds SET password_hash = ?, salt = ? WHERE login = ?",
|
||||
[_hash_password(new_password, salt), salt, login],
|
||||
)
|
||||
# разлогиниваем все старые сессии пользователя
|
||||
store.execute("DELETE FROM sessions WHERE login = ?", [login])
|
||||
return True
|
||||
|
||||
|
||||
def create_session(login: str) -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires = time.time_ns() // 1_000_000 + config.SESSION_DAYS * 86_400_000
|
||||
store.execute("INSERT INTO sessions(token, login, expires) VALUES (?, ?, ?)", [token, login, expires])
|
||||
return token
|
||||
|
||||
|
||||
def resolve_session(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
row = store.query_one(
|
||||
"SELECT login FROM sessions WHERE token = ? AND expires > ?",
|
||||
[token, time.time_ns() // 1_000_000],
|
||||
)
|
||||
return row["login"] if row else None
|
||||
|
||||
|
||||
def set_session_cookie(response: Response, token: str) -> None:
|
||||
response.set_cookie(
|
||||
key=config.COOKIE_NAME,
|
||||
value=token,
|
||||
max_age=config.SESSION_DAYS * 86400,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
# secure включается при HTTPS-проксировании; флаг не критичен локально
|
||||
)
|
||||
|
||||
|
||||
def destroy_session(token: str | None) -> None:
|
||||
if token:
|
||||
store.execute("DELETE FROM sessions WHERE token = ?", [token])
|
||||
|
||||
|
||||
def current_login(token: str | None = Cookie(default=None, alias=config.COOKIE_NAME)) -> str | None:
|
||||
login = resolve_session(token)
|
||||
if not login:
|
||||
raise HTTPException(401, "Требуется авторизация")
|
||||
return login
|
||||
|
||||
Reference in New Issue
Block a user