docs: AGENT.md — полное резюме проекта для агентов и разработчиков
This commit is contained in:
@@ -0,0 +1,427 @@
|
|||||||
|
# AGENT.md — Полное руководство по проекту IPWhiteList
|
||||||
|
|
||||||
|
> Этот файл предназначен для AI-агентов и разработчиков без контекста.
|
||||||
|
> Читай этот файл **первым** перед любой задачей.
|
||||||
|
> Обновляй после значимых изменений.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Суть проекта
|
||||||
|
|
||||||
|
**IP WhiteList** — веб-приложение для управления белыми IP-списками (CIDR) по компаниям.
|
||||||
|
|
||||||
|
- Каждая компания имеет свой список IPv4/CIDR записей.
|
||||||
|
- Есть лимит записей на компанию (по умолчанию 15, admin может менять).
|
||||||
|
- Есть одна роль admin (задаётся через env) и обычные пользователи.
|
||||||
|
- Аутентификация через JWT (RS256): в проде — внешний OIDC (Keycloak/Nubes auth-api), в dev — mock.
|
||||||
|
- Весь UI — server-side rendered (EJS). API — REST JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Репозиторий и деплой
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Репо (рабочий) | `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` ветка `master` |
|
||||||
|
| Локальный путь | `/home/naeel/ipwhitelist-app` |
|
||||||
|
| Деплой URL | `https://white.nodejsk8s.dev.nubes.ru` |
|
||||||
|
| Платформа | Nubes k8s — редеплой через Nubes UI из Gitea после `git push` |
|
||||||
|
| Версия | `package.json` → поле `version` (сейчас `0.4.9`) |
|
||||||
|
|
||||||
|
**Правило версий:** bump `version` в `package.json` при каждом значимом изменении.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Стек
|
||||||
|
|
||||||
|
```
|
||||||
|
Node.js 18+ + Express 4
|
||||||
|
EJS (server-side rendering)
|
||||||
|
PostgreSQL 17 (pg pool, SSL require)
|
||||||
|
jsonwebtoken RS256
|
||||||
|
express-session + connect-pg-simple
|
||||||
|
helmet (CSP)
|
||||||
|
express-rate-limit
|
||||||
|
supertest (тесты)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Переменные окружения
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV=development # production → отключает DEV_MODE, требует SESSION_SECRET
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
DEV_MODE=true # true → mock JWT, ЗАПРЕЩЕНО в production
|
||||||
|
DEV_SECRET= # опционально: пароль для /dev-login на staging
|
||||||
|
ADMIN_CLIENT_ID=WZ01112 # clientId пользователя с правами admin
|
||||||
|
JWT_ISSUER=mock-auth-api # issuer в JWT
|
||||||
|
|
||||||
|
# OIDC (если DEV_MODE=false)
|
||||||
|
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
|
||||||
|
KC_CLIENT_ID= # пусто = mock mode
|
||||||
|
KC_CLIENT_SECRET=
|
||||||
|
APP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# База данных
|
||||||
|
DB_HOST=write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=ipwhitelist
|
||||||
|
DB_USER=super
|
||||||
|
DB_PASS=<secret>
|
||||||
|
DB_SSLMODE=require
|
||||||
|
|
||||||
|
# Сессия
|
||||||
|
SESSION_SECRET=<secret> # обязателен в production (fail-fast при запуске)
|
||||||
|
|
||||||
|
# Прочее
|
||||||
|
DEFAULT_LIMIT=15 # лимит записей по умолчанию (если нет custom_limit)
|
||||||
|
API_BASE=http://localhost:3000 # UI-слой → куда идти за /api/v1/*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Структура файлов — карта ответственности
|
||||||
|
|
||||||
|
```
|
||||||
|
server.js ← точка входа: helmet, session, монтирование роутеров
|
||||||
|
src/
|
||||||
|
config.js ← MOCK_USERS, APP_VERSION, backUrl()
|
||||||
|
db.js ← pg Pool, checkConnection()
|
||||||
|
auth.js ← initAuth() → { middleware, issueMockToken, ... }
|
||||||
|
queries.js ← ВСЕ SQL-запросы — единственное место работы с БД
|
||||||
|
validators.js ← validate(cidr), overlaps(), aggregateCIDRs(), BLOCKED_RANGES
|
||||||
|
api/
|
||||||
|
index.js ← createApiRouter({auth, q}) — монтируется на /api/v1
|
||||||
|
middleware/
|
||||||
|
bearerAuth.js ← requireBearer: нет заголовка → 401 JSON
|
||||||
|
routes/
|
||||||
|
entries.js ← GET/POST/PATCH/DELETE /api/v1/entries + GET /api/v1/entries/export
|
||||||
|
admin.js ← GET /api/v1/companies, PATCH limit, GET /api/v1/audit
|
||||||
|
middleware/
|
||||||
|
rateLimit.js ← mutationLimiter, exportLimiter, authLimiter
|
||||||
|
session.js ← createSessionMiddleware(pool)
|
||||||
|
csrf.js ← initCsrf() — НЕ используется в новом UI
|
||||||
|
csp.js ← CSP_DIRECTIVES для helmet
|
||||||
|
routes/
|
||||||
|
export.js ← GET /export — требует авторизации (Bearer)
|
||||||
|
exp.js ← ⚠️ ВРЕМЕННЫЙ GET /exp — без авторизации (удалить после prod)
|
||||||
|
ui/
|
||||||
|
index.js ← createUiRouter({auth, MOCK_USERS, authLimiter})
|
||||||
|
api-client.js ← HTTP-клиент к /api/v1/* (берёт Bearer из session.token)
|
||||||
|
routes/
|
||||||
|
auth.js ← GET/POST /login, /login-token, /logout
|
||||||
|
entries.js ← /, /add, /edit/:id, /delete/:id
|
||||||
|
admin.js ← /admin, /admin/limit/:id, /audit
|
||||||
|
export.js ← /export (UI-обёртка, проксирует к /api/v1/entries/export)
|
||||||
|
views/
|
||||||
|
index.ejs ← главная: таблица записей + форма добавления
|
||||||
|
admin.ejs ← список компаний с лимитами (admin only)
|
||||||
|
audit.ejs ← журнал аудита (admin only)
|
||||||
|
ui-login.ejs ← страница входа (mock-пресеты + вставить Bearer вручную)
|
||||||
|
error.ejs ← 403/404/500
|
||||||
|
sql/
|
||||||
|
schema.sql ← DDL: CREATE TABLE IF NOT EXISTS (идемпотентно)
|
||||||
|
tests/
|
||||||
|
integration.js ← интеграционные тесты (supertest + реальный PG)
|
||||||
|
run-tests.js ← runner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Поток запроса — HTTP → БД
|
||||||
|
|
||||||
|
### 6.1 REST API запрос
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP Request
|
||||||
|
→ server.js: helmet, session, cookieParser
|
||||||
|
→ GET /exp → src/routes/exp.js (без auth, временный) ← ТОЛЬКО ЭТО без auth
|
||||||
|
→ /api/v1/* → src/api/index.js
|
||||||
|
→ requireBearer : нет заголовка Authorization: Bearer → 401 JSON
|
||||||
|
→ auth.middleware : валидация JWT → req.user = { clientId, isAdmin, email, ... }
|
||||||
|
→ /entries/* → src/api/routes/entries.js
|
||||||
|
→ resolveCompany() : admin+?company=id → getCompanyById() | иначе getOrCreateCompany()
|
||||||
|
→ q.*() : SQL через pg pool
|
||||||
|
→ JSON response
|
||||||
|
→ /companies, /audit → src/api/routes/admin.js
|
||||||
|
→ apiRequireAdmin() : !isAdmin → 403 JSON
|
||||||
|
→ q.*()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 UI запрос (SSR)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP Request
|
||||||
|
→ server.js → ui/index.js (createUiRouter)
|
||||||
|
→ createAuthRouter : /login, /login-token, /logout — публичные
|
||||||
|
→ requireToken : нет session.token → redirect /login
|
||||||
|
→ resolveUser : jwt.decode(token) → req.user (без верификации подписи)
|
||||||
|
→ createEntriesRouter, createAdminRouter, createExportRouter
|
||||||
|
→ ui/api-client.js : HTTP запрос к /api/v1/* с Bearer из session.token
|
||||||
|
→ разбирает JSON ответ
|
||||||
|
→ res.render('view.ejs', data)
|
||||||
|
→ Если API вернул 401 → destroy session → redirect /login
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. req.user — структура
|
||||||
|
|
||||||
|
После прохождения `auth.middleware` (API) или `resolveUser` (UI):
|
||||||
|
|
||||||
|
```js
|
||||||
|
req.user = {
|
||||||
|
clientId: 'WZ01112', // из JWT claim ClientID (заглавная!)
|
||||||
|
companyId: 'uuid-...', // из JWT claim company_id
|
||||||
|
companyName: 'Nubes Admin', // из JWT claim company_name
|
||||||
|
email: 'admin@nubes.ru', // из JWT claim email
|
||||||
|
isAdmin: true, // clientId === ADMIN_CLIENT_ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. База данных — схема
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Компании (1 строка на WZ-номер, создаётся при первом входе)
|
||||||
|
companies (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id VARCHAR(64) UNIQUE NOT NULL, -- WZ01112, WZ01325 и т.д.
|
||||||
|
name VARCHAR(255),
|
||||||
|
custom_limit INTEGER DEFAULT NULL, -- NULL = использует DEFAULT_LIMIT
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Записи белого списка (soft delete через deleted_at)
|
||||||
|
whitelist_entries (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
company_id INTEGER REFERENCES companies(id),
|
||||||
|
value_cidr VARCHAR(18), -- '192.168.1.0/24'
|
||||||
|
comment VARCHAR(255),
|
||||||
|
created_by VARCHAR(255), -- email
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_by VARCHAR(255),
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
deleted_by VARCHAR(255),
|
||||||
|
deleted_at TIMESTAMPTZ -- NULL = активная запись
|
||||||
|
)
|
||||||
|
-- INDEX: uq_entries_active_cidr(company_id, value_cidr) WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
-- Аудит
|
||||||
|
audit_log (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_email VARCHAR(255),
|
||||||
|
company_id INTEGER,
|
||||||
|
action VARCHAR(32), -- 'CREATE' | 'UPDATE' | 'DELETE'
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
entry_id INTEGER,
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. src/queries.js — все функции
|
||||||
|
|
||||||
|
| Функция | Сигнатура | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| `getOrCreateCompany` | `(clientId, companyName) → company` | UPSERT компании по clientId |
|
||||||
|
| `getCompanyById` | `(id) → company\|null` | по числовому PK |
|
||||||
|
| `getLimit` | `(company) → number` | custom_limit ?? DEFAULT_LIMIT |
|
||||||
|
| `listEntries` | `(companyId, includeDeleted=false) → []` | список записей |
|
||||||
|
| `createEntry` | `(companyId, rawCidr, comment, email) → {entry, wasNormalized}` | INSERT + audit (транзакция) |
|
||||||
|
| `updateEntry` | `(entryId, companyId, rawCidr, comment, email) → {entry, wasNormalized}` | UPDATE + audit (транзакция) |
|
||||||
|
| `deleteEntry` | `(entryId, companyId, email) → void` | soft delete + audit (транзакция) |
|
||||||
|
| `getAllCompanies` | `() → []` | все компании + active_count |
|
||||||
|
| `setLimit` | `(companyId, newLimit) → void` | custom_limit = newLimit |
|
||||||
|
| `getExportCIDRs` | `(companyId=null) → string[]` | CIDR строки; null = все компании |
|
||||||
|
| `logAudit` | `(email, companyId, action, old, new, entryId, db?) → void` | внутренняя, вызывается из транзакций |
|
||||||
|
| `getAudit` | `(companyId=null) → []` | журнал с JOIN companies |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. src/validators.js — функции
|
||||||
|
|
||||||
|
```js
|
||||||
|
validate(rawValue)
|
||||||
|
→ { cidr: '1.2.3.0/24', wasNormalized: boolean }
|
||||||
|
→ throws Error если невалидно
|
||||||
|
// Нормализует host bits, проверяет BLOCKED_RANGES
|
||||||
|
|
||||||
|
overlaps(cidr1, cidr2) → boolean
|
||||||
|
// true если диапазоны пересекаются
|
||||||
|
|
||||||
|
aggregateCIDRs(cidrArray) → string[]
|
||||||
|
// Объединяет пересекающиеся и смежные диапазоны → минимальный набор
|
||||||
|
|
||||||
|
BLOCKED_RANGES // приватные/зарезервированные диапазоны — запрещены для добавления
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. src/auth.js — initAuth()
|
||||||
|
|
||||||
|
```js
|
||||||
|
const auth = await initAuth();
|
||||||
|
// Возвращает:
|
||||||
|
auth.middleware // Express middleware: валидирует Bearer JWT → req.user
|
||||||
|
auth.issueMockToken(claims) → jwtString // только в mock-режиме, для тестов
|
||||||
|
auth.jwksHandler // GET /.well-known/jwks.json (только mock)
|
||||||
|
// OIDC-режим:
|
||||||
|
auth.getAuthUrl(state, nonce) → string // redirect URL на Keycloak
|
||||||
|
auth.exchangeCode(code) → {token, user} // обмен code → JWT
|
||||||
|
```
|
||||||
|
|
||||||
|
**Логика определения режима:**
|
||||||
|
- `KC_CLIENT_ID && KC_CLIENT_SECRET` → OIDC-режим
|
||||||
|
- иначе → mock-режим (генерирует RSA-пару при старте)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. ui/api-client.js — интерфейс
|
||||||
|
|
||||||
|
```js
|
||||||
|
api.token(req) // → Bearer string из req.session.token
|
||||||
|
api.get(path, token) // → { status, data }
|
||||||
|
api.post(path, token, body) // → { status, data }
|
||||||
|
api.patch(path, token, body) // → { status, data }
|
||||||
|
api.delete(path, token) // → { status, data }
|
||||||
|
// path примеры: '/api/v1/entries', '/api/v1/entries/42', '/api/v1/companies/5/limit'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Все HTTP эндпоинты
|
||||||
|
|
||||||
|
### Публичные (без auth)
|
||||||
|
| Метод | URL | Файл | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/healthz` | server.js | k8s liveness probe |
|
||||||
|
| GET | `/.well-known/jwks.json` | server.js | mock JWKS (только DEV_MODE) |
|
||||||
|
| GET | `/login` | ui/routes/auth.js | страница входа |
|
||||||
|
| POST | `/login` | ui/routes/auth.js | выбор mock-пользователя |
|
||||||
|
| POST | `/login-token` | ui/routes/auth.js | вставить Bearer вручную |
|
||||||
|
| GET | `/logout` | ui/routes/auth.js | destroy session |
|
||||||
|
| GET | `/callback` | ui/routes/auth.js | OIDC callback |
|
||||||
|
| GET | `/exp` | src/routes/exp.js | ⚠️ ВРЕМЕННЫЙ: экспорт без auth |
|
||||||
|
|
||||||
|
### UI (требуют session.token)
|
||||||
|
| Метод | URL | Файл | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/` | ui/routes/entries.js | главная страница |
|
||||||
|
| POST | `/add` | ui/routes/entries.js | добавить запись |
|
||||||
|
| POST | `/edit/:id` | ui/routes/entries.js | изменить запись |
|
||||||
|
| POST | `/delete/:id` | ui/routes/entries.js | удалить запись |
|
||||||
|
| GET | `/admin` | ui/routes/admin.js | список компаний (admin) |
|
||||||
|
| POST | `/admin/limit/:id` | ui/routes/admin.js | изменить лимит (admin) |
|
||||||
|
| GET | `/audit` | ui/routes/admin.js | журнал аудита (admin) |
|
||||||
|
| GET | `/export` | ui/routes/export.js | скачать whitelist.txt |
|
||||||
|
|
||||||
|
### REST API `/api/v1` (требуют Bearer JWT)
|
||||||
|
| Метод | URL | Файл | Кто | Описание |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| GET | `/api/v1/entries` | api/routes/entries.js | все | список + limit + used |
|
||||||
|
| GET | `/api/v1/entries?company=<id>` | api/routes/entries.js | admin | записи другой компании |
|
||||||
|
| POST | `/api/v1/entries` | api/routes/entries.js | все | создать |
|
||||||
|
| PATCH | `/api/v1/entries/:id` | api/routes/entries.js | все | обновить |
|
||||||
|
| DELETE | `/api/v1/entries/:id` | api/routes/entries.js | все | удалить |
|
||||||
|
| GET | `/api/v1/entries/export` | api/routes/entries.js | все | text/plain CIDR |
|
||||||
|
| GET | `/api/v1/companies` | api/routes/admin.js | admin | все компании |
|
||||||
|
| PATCH | `/api/v1/companies/:id/limit` | api/routes/admin.js | admin | установить лимит |
|
||||||
|
| GET | `/api/v1/audit` | api/routes/admin.js | admin | журнал аудита |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Rate limiting
|
||||||
|
|
||||||
|
| Лимитер | Применяется к | Лимит |
|
||||||
|
|---|---|---|
|
||||||
|
| `authLimiter` | POST /login, /login-token, GET /callback | 10 req / 5 мин |
|
||||||
|
| `exportLimiter` | GET /export, GET /exp, GET /api/v1/entries/export | 20 req / мин |
|
||||||
|
| `mutationLimiter` | POST /add, /edit/:id, /delete/:id | 30 req / мин |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Mock-пользователи (DEV_MODE=true)
|
||||||
|
|
||||||
|
| id | clientId | Роль | companyName | email |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `admin` | `WZ01112` | **admin** | Nubes Admin | admin@nubes.ru |
|
||||||
|
| `test` | `WZ01325` | user | Тест | tazet@narod.ru |
|
||||||
|
| `client2` | `WZ02001` | user | Вторая Компания | user2@example.com |
|
||||||
|
|
||||||
|
Файл: `src/config.js` → `MOCK_USERS`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Бизнес-логика — важные правила
|
||||||
|
|
||||||
|
1. **Изоляция компаний**: везде `company_id` в WHERE — данные одной компании недоступны другой.
|
||||||
|
2. **Транзакции с FOR UPDATE**: createEntry/updateEntry/deleteEntry блокируют строку компании — защита от race condition при параллельных вставках.
|
||||||
|
3. **Soft delete**: `deleted_at IS NULL` = активная запись. Удалённые остаются в БД.
|
||||||
|
4. **Дубли/пересечения**: при создании и обновлении проверяются через `overlaps()` против всех активных записей компании.
|
||||||
|
5. **Лимит**: снижение лимита не удаляет записи — только блокирует создание новых.
|
||||||
|
6. **CIDR нормализация**: `validate()` нормализует host bits (192.168.1.5/24 → 192.168.1.0/24), возвращает `wasNormalized: true`.
|
||||||
|
7. **Агрегация при экспорте**: `aggregateCIDRs()` объединяет смежные диапазоны — минимальный набор.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. HTTP коды ошибок API
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| 400 | Невалидный CIDR, некорректные параметры |
|
||||||
|
| 401 | Нет/невалидный Bearer токен |
|
||||||
|
| 403 | Не admin на admin-endpoint |
|
||||||
|
| 404 | Запись/компания не найдена |
|
||||||
|
| 409 | Дубликат CIDR, пересечение, лимит исчерпан |
|
||||||
|
| 500 | Ошибки БД и прочие |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Запуск и тесты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка зависимостей
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Запуск локально (нужен .env)
|
||||||
|
node server.js
|
||||||
|
|
||||||
|
# Тесты (supertest + реальная БД)
|
||||||
|
npm test
|
||||||
|
# или
|
||||||
|
node tests/run-tests.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Тесты требуют реального PG подключения (переменные из .env).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. ⚠️ ВРЕМЕННЫЕ ЭЛЕМЕНТЫ — удалить перед продом
|
||||||
|
|
||||||
|
### GET /exp — публичный экспорт без авторизации (добавлен v0.4.9)
|
||||||
|
|
||||||
|
**Причина:** По ТЗ п. 4.7 `/export` должен быть публичным, но сейчас требует авторизацию. Создан временный `/exp` как workaround.
|
||||||
|
|
||||||
|
**Что удалить:**
|
||||||
|
1. Файл `src/routes/exp.js` — полностью
|
||||||
|
2. В `server.js` строку: `app.use(require('./src/routes/exp'));`
|
||||||
|
|
||||||
|
**Когда:** когда `/export` сделают публичным по ТЗ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. Расхождения с ТЗ (открытые задачи)
|
||||||
|
|
||||||
|
| Пункт ТЗ | Суть | Статус |
|
||||||
|
|---|---|---|
|
||||||
|
| 3.1 | Несколько компаний на пользователя | Не реализовано — ждём devops |
|
||||||
|
| 4.1 | Отображение soft-deleted для admin | Не реализовано |
|
||||||
|
| 4.7 | `/export` без авторизации | Обход: `/exp` (временный) |
|
||||||
Reference in New Issue
Block a user