From a1bb3eab984d96419b10732267e1e2d4e8e79862 Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 24 Oct 2026 22:32:30 +0300 Subject: [PATCH 1/4] fix: align admin navigation with backend capability --- README.md | 9 ++++---- backend/search/api.py | 14 ++++++------ backend/search/auth.py | 9 ++++++-- backend/search/minjust_opensearch.py | 4 ++-- backend/search/review.html | 14 ++++++------ backend/test_search_api.py | 28 +++++++++++++++++++++++ tools/frontend-visual-review/check.cjs | 28 ++++++++++++++++------- tools/frontend-visual-review/fixtures.cjs | 5 ++-- 8 files changed, 79 insertions(+), 32 deletions(-) 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..618437c 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -657,7 +657,7 @@

Документ

Выберите документ в выдаче, чтобы открыть текст.

Содержание
- +

Черновик появится после поиска.

Вход в Акылдаш

Введите учетные данные, выданные администратором.

Создайте новый пароль

Для продолжения замените временный пароль. Используйте не менее 12 символов.

@@ -675,7 +675,7 @@

Документ

Источники документов

Поиск выполняется по документам, подготовленным из правовых источников Кыргызской Республики. Для каждого результата доступная редакция и ссылка на официальный источник показываются в панели документа.

Официальный источник: Централизованный банк данных правовой информации Министерства юстиции Кыргызской Республики.

Короткое знакомство

- + `; const previewHtml = html.replace('', `${warning}`); @@ -60,7 +60,7 @@ const server = http.createServer(async (request, response) => { if (method === 'GET' && url.pathname === '/admin/users') return json(response, 200, {users}); if (method === 'POST' && url.pathname === '/admin/users') { const body = await readBody(request); const username = String(body.username || 'fixture-created'); - users.push({username,role:'user',must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}); + users.push({username,role:'Пользователь',can_manage_users:false,must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}); return json(response, 201, {username}); } if (method === 'POST' && /^\/admin\/users\/[^/]+\/reset-password$/.test(url.pathname)) return json(response, 200, {ok:true}); -- 2.54.0 From 3620bb734cde02ef653a3a514e449fc8741bd1eb Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 24 Oct 2026 23:26:06 +0300 Subject: [PATCH 3/4] test: wait for localized user role --- tools/frontend-visual-review/check.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/frontend-visual-review/check.cjs b/tools/frontend-visual-review/check.cjs index 29e77ab..46685d4 100644 --- a/tools/frontend-visual-review/check.cjs +++ b/tools/frontend-visual-review/check.cjs @@ -353,7 +353,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe ["!reviewFlow.reviewScroll.footerAfterSave", !reviewFlow.reviewScroll.footerAfterSave], ["!reviewFlow.reviewSaved.includes('Оценка сохранена')", !reviewFlow.reviewSaved.includes('Оценка сохранена')] ].filter(([, failed]) => failed).map(([name]) => name); console.log('labLayout',JSON.stringify(labLayout)); console.log('reviewFlow',JSON.stringify(reviewFlow)); console.log('failedCoreChecks', JSON.stringify(failedCoreChecks)); if (failedCoreChecks.length) process.exit(1); - const usersPage=page; await usersPage.setViewportSize({width:320,height:760}); await usersPage.goto('https://fixture.invalid/users'); await usersPage.locator('#user-list .review__user-row').waitFor(); const usersRoute=await usersPage.evaluate(()=>({visibleFooters:[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length,visible:!document.querySelector('#users-page').hidden,admin:document.body.classList.contains('is-admin'),accountOpen:document.querySelector('#account-dialog').open,headerVisible:getComputedStyle(document.querySelector('body > header.review__header')).display!=='none',footerAfterMain:Boolean(document.querySelector('#users-page').compareDocumentPosition(document.querySelector('body > footer.review__header')) & Node.DOCUMENT_POSITION_FOLLOWING),reviewActionsHidden:getComputedStyle(document.querySelector('body > .review__actions')).display==='none',footerAfterUsers:document.querySelector('body > footer.review__header').getBoundingClientRect().top>=document.querySelector('#user-list').getBoundingClientRect().bottom,overflow:document.documentElement.scrollWidth>innerWidth})); await usersPage.locator('#users-ui-ky').click(); const usersKy=await usersPage.evaluate(()=>({lang:document.documentElement.lang,heading:document.querySelector('#users-heading').textContent})); const localizedRoleCells=await usersPage.locator('#user-list .review__user-row').evaluateAll(rows=>rows.map(row=>row.children[1].textContent)); const usersNavContract=await usersPage.evaluate(()=>({headerUsers:!document.querySelector('#review-users-open').hidden,dialogUsers:!document.querySelector('#search-users-open').hidden,dialogLab:!document.querySelector('#review-nav').hidden,headerLab:!document.querySelector('#review-review-nav').hidden})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,usersNavContract,errors})); if(usersRoute.visibleFooters!==1||!usersRoute.visible||!usersRoute.admin||usersRoute.accountOpen||!usersRoute.headerVisible||!usersRoute.footerAfterMain||!usersRoute.reviewActionsHidden||!usersRoute.footerAfterUsers||usersRoute.overflow||errors.length||usersKy.lang!=='ky'||usersKy.heading!=='Колдонуучулар'||!localizedRoleCells.includes('Колдонуучу')||!usersNavContract.headerUsers||usersNavContract.dialogUsers||usersNavContract.dialogLab||!usersNavContract.headerLab) process.exit(1); + const usersPage=page; await usersPage.setViewportSize({width:320,height:760}); await usersPage.goto('https://fixture.invalid/users'); await usersPage.locator('#user-list .review__user-row').waitFor(); const usersRoute=await usersPage.evaluate(()=>({visibleFooters:[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length,visible:!document.querySelector('#users-page').hidden,admin:document.body.classList.contains('is-admin'),accountOpen:document.querySelector('#account-dialog').open,headerVisible:getComputedStyle(document.querySelector('body > header.review__header')).display!=='none',footerAfterMain:Boolean(document.querySelector('#users-page').compareDocumentPosition(document.querySelector('body > footer.review__header')) & Node.DOCUMENT_POSITION_FOLLOWING),reviewActionsHidden:getComputedStyle(document.querySelector('body > .review__actions')).display==='none',footerAfterUsers:document.querySelector('body > footer.review__header').getBoundingClientRect().top>=document.querySelector('#user-list').getBoundingClientRect().bottom,overflow:document.documentElement.scrollWidth>innerWidth})); await usersPage.locator('#users-ui-ky').click(); await usersPage.waitForFunction(()=>[...document.querySelectorAll('#user-list .review__user-row')].some(row=>row.children[1]?.textContent.trim()==='Колдонуучу')); const usersKy=await usersPage.evaluate(()=>({lang:document.documentElement.lang,heading:document.querySelector('#users-heading').textContent})); const localizedRoleCells=await usersPage.locator('#user-list .review__user-row').evaluateAll(rows=>rows.map(row=>row.children[1].textContent)); const usersNavContract=await usersPage.evaluate(()=>({headerUsers:!document.querySelector('#review-users-open').hidden,dialogUsers:!document.querySelector('#search-users-open').hidden,dialogLab:!document.querySelector('#review-nav').hidden,headerLab:!document.querySelector('#review-review-nav').hidden})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,usersNavContract,errors})); if(usersRoute.visibleFooters!==1||!usersRoute.visible||!usersRoute.admin||usersRoute.accountOpen||!usersRoute.headerVisible||!usersRoute.footerAfterMain||!usersRoute.reviewActionsHidden||!usersRoute.footerAfterUsers||usersRoute.overflow||errors.length||usersKy.lang!=='ky'||usersKy.heading!=='Колдонуучулар'||!localizedRoleCells.includes('Колдонуучу')||!usersNavContract.headerUsers||usersNavContract.dialogUsers||usersNavContract.dialogLab||!usersNavContract.headerLab) process.exit(1); const reflowWidth=640; await usersPage.setViewportSize({width:reflowWidth,height:450}); await usersPage.goto('https://fixture.invalid/?q=%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3'); await usersPage.locator('.search__result-title').first().waitFor(); const search640=await usersPage.evaluate(()=>{const button=document.querySelector('#search-account-open'),r=button.getBoundingClientRect();return {documentWidth:document.documentElement.scrollWidth,accountVisible:r.width>0&&getComputedStyle(button).visibility!=='hidden',accountWithinViewport:r.left>=0&&r.right<=innerWidth};}); -- 2.54.0 From a153dc87651e38f5be36c1b6dabdef8863f21255 Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 24 Oct 2026 23:29:57 +0300 Subject: [PATCH 4/4] docs: record admin navigation release changes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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 состояния. -- 2.54.0