This commit is contained in:
2026-05-01 03:27:33 +03:00
parent 469fd1ba94
commit 43597be020
6 changed files with 260 additions and 66 deletions

View File

@@ -141,7 +141,7 @@ async def startup_event():
if not _db.get_all_users():
admin_login = os.getenv("AUTH_LOGIN", "admin")
admin_password = os.getenv("AUTH_PASSWORD", "admin")
_db.create_user(admin_login, hash_password(admin_password), "admin")
_db.create_user(admin_login, hash_password(admin_password), "admin", is_env_admin=True)
logger.info("Создан начальный администратор: '{}'", admin_login)
if admin_login == "admin" and admin_password == "admin":
logger.warning(
@@ -353,7 +353,8 @@ async def auth_check(request: Request):
return {"authenticated": False, "auth_enabled": True}
return {
"authenticated": True, "auth_enabled": True,
"user": {"id": user["id"], "username": user["username"], "role": user["role"]},
"user": {"id": user["id"], "username": user["username"], "role": user["role"],
"is_env_admin": bool(user.get("is_env_admin"))},
}
finally:
db.close()
@@ -369,7 +370,8 @@ async def login(body: LoginRequest, response: Response):
db.create_session(token, user["id"], expires_at)
response.set_cookie(key=COOKIE_NAME, value=token, max_age=COOKIE_MAX_AGE,
httponly=True, samesite="lax", secure=False)
return {"ok": True, "user": {"id": user["id"], "username": user["username"], "role": user["role"]}}
return {"ok": True, "user": {"id": user["id"], "username": user["username"], "role": user["role"],
"is_env_admin": bool(user.get("is_env_admin"))}}
finally:
db.close()
@app.post("/api/logout")
@@ -428,6 +430,9 @@ async def update_user_endpoint(user_id: int, body: UpdateUserRequest,
user = db.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
if user.get("is_env_admin") and body.password is not None:
raise HTTPException(status_code=403,
detail="Пароль системного администратора нельзя изменить через интерфейс — используйте переменную окружения AUTH_PASSWORD")
if body.role and body.role not in ("admin", "user"):
raise HTTPException(status_code=400, detail="Роль должна быть 'admin' или 'user'")
if body.role == "user" and user["role"] == "admin":
@@ -459,6 +464,8 @@ async def delete_user_endpoint(user_id: int, current_user: dict = Depends(requir
user = db.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
if user.get("is_env_admin"):
raise HTTPException(status_code=403, detail="Системного администратора нельзя удалить")
if user["role"] == "admin" and db.count_admins() <= 1:
raise HTTPException(status_code=400, detail="Нельзя удалить последнего администратора")
db.delete_user(user_id)

View File

@@ -97,12 +97,13 @@ class StateDB:
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT,
updated_at TEXT
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
is_env_admin INTEGER NOT NULL DEFAULT 0,
created_at TEXT,
updated_at TEXT
)
""")
self.conn.execute("""
@@ -127,6 +128,7 @@ class StateDB:
("mangas", "folder_name", "TEXT"),
("mangas", "source_id", "INTEGER REFERENCES sources(id)"),
("mangas", "added_by", "INTEGER REFERENCES users(id)"),
("users", "is_env_admin", "INTEGER NOT NULL DEFAULT 0"),
]
for table, col, typedef in migrations:
try:
@@ -536,15 +538,17 @@ class StateDB:
# ── Users ─────────────────────────────────────
def create_user(self, username: str, hashed_password: str, role: str = "user") -> dict:
def create_user(self, username: str, hashed_password: str, role: str = "user",
is_env_admin: bool = False) -> dict:
"""Создаёт пользователя. Возвращает dict без поля password."""
self.conn.execute("""
INSERT INTO users (username, password, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (username, hashed_password, role, _now(), _now()))
INSERT INTO users (username, password, role, is_env_admin, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (username, hashed_password, role, 1 if is_env_admin else 0, _now(), _now()))
self.conn.commit()
row = self.conn.execute(
"SELECT id, username, role, created_at FROM users WHERE username=?", (username,)
"SELECT id, username, role, is_env_admin, created_at FROM users WHERE username=?",
(username,)
).fetchone()
return dict(row)
@@ -561,7 +565,7 @@ class StateDB:
def get_all_users(self) -> list[dict]:
"""Возвращает всех пользователей без поля password."""
cur = self.conn.execute(
"SELECT id, username, role, created_at, updated_at FROM users ORDER BY id"
"SELECT id, username, role, is_env_admin, created_at, updated_at FROM users ORDER BY id"
)
return [dict(r) for r in cur.fetchall()]