diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f61b95..29595f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Не выпущено +- Frontend `0.3.3` / Backend `0.13.2`: управление пользователями отображается + согласно правам аккаунта, подписи ролей локализованы, повторяющиеся действия + кабинета убраны, а знак лаборатории приведён к основному бренду. - Frontend `0.3.2`: устранены пересечения названия бренда с поиском и мобильной навигации документа; навигационные ссылки получили общие quiet и primary состояния. diff --git a/README.md b/README.md index 854e61b..b5ec8c2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2`, backend версии -`0.13.1` и frontend `0.3.2` с поисковым экраном и внутренней лабораторией. +`0.13.2` и frontend `0.3.3` с поисковым экраном, внутренней лабораторией и +согласованным отображением доступных действий учетной записи. Авторизация хранится в SQLite; первый пользователь `admin` создаётся интерактивной CLI-командой. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает @@ -19,8 +20,8 @@ SQLite; первый пользователь `admin` создаётся инт | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.13.1` | авторизация, поиск с фильтрами, ограниченный staging и статические ресурсы | -| Frontend | `0.3.2` | поиск по правовой базе, просмотр редакций, настраиваемая ширина панелей и внутренняя лаборатория | +| Backend | `0.13.2` | авторизация, серверная capability управления пользователями, поиск и статические ресурсы | +| Frontend | `0.3.3` | поиск по правовой базе, просмотр редакций, внутренняя лаборатория и контекстная навигация кабинета | | Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи | | RAG и база знаний | — | ещё не созданы | @@ -63,4 +64,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.13.1 · Frontend v0.3.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.13.2 · Frontend v0.3.3 diff --git a/backend/search/api.py b/backend/search/api.py index bea9111..d69fe59 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -13,7 +13,7 @@ import urllib.parse from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path -from search.auth import AuthError, AuthStore +from search.auth import AuthError, AuthStore, can_manage_users from search.minjust_opensearch import APP_VERSION, request_json from search.catalog import CATALOGS, labels from search.query import build_search_query @@ -345,21 +345,21 @@ class Api: return 200, {"account": account, "session_token": token} except AuthError as error: raise ApiError(400, str(error)) from error - if user and user["username"].casefold() == "admin" and method == "GET" and parts == ["admin", "users"]: + if user and can_manage_users(user["username"]) and method == "GET" and parts == ["admin", "users"]: return 200, {"users": self.auth_store.list_users()} - if user and user["username"].casefold() == "admin" and method == "POST" and parts == ["admin", "users"]: + if user and can_manage_users(user["username"]) and method == "POST" and parts == ["admin", "users"]: try: self.auth_store.create_user(body.get("username"), body.get("password")) return 201, {"ok": True} except AuthError as error: raise ApiError(400, str(error)) from error - if user and user["username"].casefold() == "admin" and method == "DELETE" and len(parts) == 3 and parts[:2] == ["admin", "users"]: + if user and can_manage_users(user["username"]) and method == "DELETE" and len(parts) == 3 and parts[:2] == ["admin", "users"]: try: self.auth_store.delete_user(parts[2]) return 200, {"ok": True} except AuthError as error: raise ApiError(400, str(error)) from error - if user and user["username"].casefold() == "admin" and method == "POST" and len(parts) == 4 and parts[:2] == ["admin", "users"] and parts[3] == "reset-password": + if user and can_manage_users(user["username"]) and method == "POST" and len(parts) == 4 and parts[:2] == ["admin", "users"] and parts[3] == "reset-password": try: self.auth_store.reset_password(parts[2], body.get("password")) return 200, {"ok": True} @@ -424,7 +424,7 @@ def handler(api: Api): raise ApiError(401, "authentication required") if user["must_change_password"] and not (method == "POST" and route == "/auth/change-password") and not (method == "GET" and route == "/auth/me"): raise ApiError(403, "change password before continuing") - if route.startswith("/admin/users") and user["username"].casefold() != "admin": + if route.startswith("/admin/users") and not can_manage_users(user["username"]): raise ApiError(403, "admin account required") body = None if method in {"POST", "DELETE"}: @@ -503,7 +503,7 @@ def main() -> int: api = Api(arguments.url, arguments.index, arguments.data, arguments.reviews_db, secret, arguments.users_db, arguments.cookie_secure) if arguments.create_admin or arguments.reset_admin_password: users = api.auth_store.list_users() - admin_exists = any(user["username"].casefold() == "admin" for user in users) + admin_exists = any(can_manage_users(user["username"]) for user in users) if arguments.create_admin and users: parser.error("accounts already exist") if arguments.reset_admin_password and not admin_exists: diff --git a/backend/search/auth.py b/backend/search/auth.py index 3583a5e..bb50645 100644 --- a/backend/search/auth.py +++ b/backend/search/auth.py @@ -22,6 +22,10 @@ class AuthError(ValueError): pass +def can_manage_users(username: str) -> bool: + return isinstance(username, str) and username.casefold() == "admin" + + class AuthStore: def __init__(self, path: Path | str): if path != ":memory:": @@ -106,7 +110,8 @@ class AuthStore: @staticmethod def _public(user: sqlite3.Row) -> dict: - return {"username": user["username"], "role": "Администратор", "must_change_password": bool(user["must_change_password"]), "created_at": user["created_at"]} + can_manage = can_manage_users(user["username"]) + return {"username": user["username"], "role": "Администратор" if can_manage else "Пользователь", "can_manage_users": can_manage, "must_change_password": bool(user["must_change_password"]), "created_at": user["created_at"]} def user_for_token(self, token: str | None) -> dict | None: if not token: @@ -151,7 +156,7 @@ class AuthStore: raise AuthError("login already exists") from error def delete_user(self, username: str) -> None: - if username.casefold() == "admin": + if can_manage_users(username): raise AuthError("admin account cannot be deleted") with self._lock: self.connection.execute("DELETE FROM users WHERE username=?", (username,)) diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index 314a06a..078fa57 100644 --- a/backend/search/minjust_opensearch.py +++ b/backend/search/minjust_opensearch.py @@ -17,7 +17,7 @@ from typing import Iterator from search.catalog import authority_codes, legal_force_code, source_code -APP_VERSION = "0.13.1" +APP_VERSION = "0.13.2" LANGUAGES = {"ru", "ky"} DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json") @@ -465,7 +465,7 @@ def main() -> int: else: documents, fragments = export_bulk(arguments.input, arguments.output, arguments.index, arguments.limit) destination = arguments.output - print(f"documents={documents} fragments={fragments} destination={destination}\nAkyldash · Backend v{APP_VERSION} · Frontend v0.3.2") + print(f"documents={documents} fragments={fragments} destination={destination}\nAkyldash · Backend v{APP_VERSION} · Frontend v0.3.3") return 0 diff --git a/backend/search/review.html b/backend/search/review.html index df2d315..4caffca 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -25,7 +25,7 @@ .review__header p { max-width: 65ch; margin: 0; color: var(--muted); } .review__header-top { display: flex; justify-content: space-between; gap: 1rem; align-items: start; } .review__eyebrow { display: inline-flex; align-items: center; gap: .5rem; color: var(--ink); font: 600 1.1rem/1.25 var(--serif); } - .review__eyebrow::before { content: ''; width: 1.5rem; height: 1.5rem; border: 2px solid var(--brand); border-radius: 50%; background: linear-gradient(90deg, transparent 45%, var(--brand) 45% 55%, transparent 55%), linear-gradient(0deg, transparent 45%, var(--brand) 45% 55%, transparent 55%); } + .review__eyebrow-mark { display: block; flex: 0 0 1.5rem; width: 1.5rem; height: 1.5rem; } .review__main { padding-block-end: 2rem; } .review__search { display: grid; grid-template-columns: minmax(18rem, 1fr) 9rem 9rem auto; align-items: end; gap: .75rem; padding: 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; } .review__field { display: grid; gap: .35rem; min-width: 0; } @@ -621,7 +621,7 @@
Перейти к результатамСопоставляйте запрос с текстом документов и сохраняйте оценку выдачи.
Черновик появится после поиска.