upd
This commit is contained in:
13
src/api.py
13
src/api.py
@@ -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)
|
||||
|
||||
28
src/state.py
28
src/state.py
@@ -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()]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user