LogEnrichmentMiddleware: после Authentication+Authorization вытягивает из
ClaimsPrincipal OrgId (claim org_id) и UserId (sub/NameIdentifier), плюс
CorrelationId из заголовка X-Correlation-ID (или генерирует Guid). Все три
кладутся в Serilog LogContext через PushProperty — каждая ILogger.Log*
внутри пайплайна автоматически получает эти поля как структурные
properties (не текст), пригодные для фильтрации в Loki/ELK без regex.
Эхо CorrelationId в response-header — клиент видит id для support.
Business-логи (структурные плейсхолдеры, не string interpolation):
- Supply.Post → "Supply posted: {SupplyNumber} supplier={SupplierId}
store={StoreId} lines={LinesCount} total={Total}".
- RetailSale.Post → "RetailSale posted: {SaleNumber} store={StoreId}
payment={Payment} lines={LinesCount} total={Total}".
docs/logging.md — паттерн, anti-pattern'ы (string interpolation, PII в
логах, токены/пароли), correlation-id workflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
API:
• SwaggerGen с OpenAPI info (title/version/description),
Bearer security-scheme (через OpenIddict JWT),
стабильные operationId = Controller_VerbAction (HTTP-verb включён
чтобы избежать коллизии когда ASP.NET стрипает Async-суффикс —
WipeAll и WipeAllAsync ранее давали одинаковый operationId);
• CustomSchemaIds с префиксом из namespace (одноимённые nested
record'ы в разных контроллерах больше не схлопываются — StockRow
есть в Inventory_StockController и Reports_StockReportController).
UI:
• /swagger (UI) и /swagger/v1/swagger.json (документ) — только в Development.
На prod не раскрываем (endpoint enumeration).
Web:
• Добавлен devDependency openapi-typescript@^7.5.2 + npm-script gen:api,
читающий http://localhost:5081/swagger/v1/swagger.json.
• src/lib/api.generated.ts — сгенерированные типы (~7700 строк, все
схемы и operations).
• src/lib/apiClient.ts — тонкая обёртка над axios api, использующая
типы из generated. Подключена для пары контроллеров (Reports/Sales,
Reports/ABC, Reports/Profit) как образец постепенной миграции.
docs/openapi.md — workflow генерации (live API или Swashbuckle CLI),
versioning, наставления для нового кода.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
deploy/.env.example — все required/опц. переменные (POSTGRES_PASSWORD, REGISTRY,
*_TAG, OPENIDDICT_ISSUER/CERT_PASSWORD, FM_* бэкапа, Cors/RateLimiting/MoySklad).
docs/secrets.md — таблица переменных, где живут секреты (SMTP в БД, сертификаты в
volume), ротация, гигиена. compose: api получает OpenIddict__Issuer (за прокси
обязателен) и OpenIddict__CertPassword из .env. compose config валиден.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
food-market-backup.sh: pg_dump -Fc контейнера + tar uploads, ротация 30 дней,
атомарная запись через .tmp+mv. food-market-backup.{service,timer} — ежедневно
03:00 с догоном пропущенных. docs/backup-restore.md — установка таймера, ручной
бэкап, восстановление БД (drop+create / --clean) и uploads, проверка дампа.
Скрипт проверен против food-market-postgres: дамп PGDMP custom-format,
248 TOC, pg_restore --list читает. Установку на prod-vm не делаем — только артефакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OpenIddictKeyConfigurator: dev — прежний RSA-ключ в App_Data (поведение не
менялось, шифрование access-token выключено); prod/stage — отдельные X509
сертификаты подписи и шифрования из конфига (OpenIddict:SigningCertPath /
EncryptionCertPath / CertPassword, можно env). Нет файла → генерируется
persistent self-signed (RSA 2048, 5 лет) и сохраняется в App_Data (volume),
а не dev-ephemeral — токены переживают рестарт.
Проверено: prod выдаёт 5-сегментный JWE, /api/me 200; рестарт → те же
сертификаты (fingerprint совпал), pre-restart токен валиден. dev — 3-сегментный
JWT, /api/me 200. docs/openiddict-keys.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Завершающий пункт пакета фиксов по ролям/валидации/удалению. Обход:
1. /connect/token — IsActive + BelongsToLiveOrg + SuperAdmin bypass.
2. JWT cookie vs Bearer — все три AuthN-схемы переопределены в
OpenIddictValidationAspNetCoreDefaults; cookie не активна для API.
3. X-Org-Override — фильтрует по IsInRole(SuperAdmin), подделать нельзя.
4. Tenant query filters — ITenantEntity и IOptionalTenantEntity
подключаются через reflection, фильтр консистентен с tenant.context.
5. Smoke per-role — sidebar+RoleGuard за один проход покрывает все
tenant-роуты; tenant-admin на /super-admin URL → описан risk + future fix.
6. Reset password / deactivate account — токены revoke в БД одним SQL.
7. Catch-22 для SuperAdmin платформы — он не Employee и не имеет
OrganizationId, через текущие endpoint-ы deactivate невозможен.
Findings разбиты на critical (закрыто этим пакетом), high/medium (не
закрыто — будущая серия) и low (косметика).
Аудит 2026-04-27. Полный отчёт — docs/audit-2026-04-27.md.
Что закрыто:
— /connect/token (AuthorizationController) теперь отказывает в login если
AppUser привязан к удалённой/архивной Organization. SuperAdmin обходит
проверку (ему org не нужна). Жалоба: nurnetps@gmail.com мог логиниться
после удаления своей org из SuperAdmin консоли.
— SuperAdminOrganizationsController.Delete (DELETE org) каскадно
деактивирует всех AppUser привязанных к этой org (IsActive=false,
OrganizationId=null) и помечает Status='revoked' для всех их
OpenIddictTokens. Раньше Org удалялась, а юзеры оставались валидными
с активными refresh-tokens на 30 дней.
— EmployeesController.Delete теперь soft-delete (IsActive=false,
FiredAt). Запрещены: 403 если попытка удалить себя; 403 если
попытка удалить Owner (Organization.AccountOwnerUserId ==
employee.UserId). Сообщения с инструкцией («передайте права»,
«покинуть через настройки»).
— /api/me возвращает hasLiveOrg и hasActiveEmployee — frontend
использует это для редиректа на /no-organization вместо белого экрана.
— Новая страница /no-organization (NoOrganizationPage) — fallback для
orphan AppUser. CTA: создать новую org через публичный /signup
или попросить инвайт. Кнопка «выйти». TenantRouteGuard редиректит
orphan юзеров туда.
— SuperAdminAsOrgBanner: добавлена проверка через useMe — баннер
рендерится только если у текущего юзера есть Identity-роль
SuperAdmin. Lingering localStorage override от прошлой сессии
(другой юзер логинился до этого) автоматически чистится.
— auth.ts: clearTokens() теперь сбрасывает superAdminAsOrg и
superAdminEditMode. login() вызывает clearTokens() ПЕРЕД запросом
чтобы новый юзер не унаследовал override-состояние от предыдущего.
— deploy/recovery-restore-orphan-owners.sql — идемпотентный скрипт
деактивирующий уже накопленных orphan AppUser (как nurnetps) и
revoke их токены. Применён на стейдже: 1 user деактивирован,
9 токенов revoked.
— deploy/Dockerfile.api: убран `--no-restore` из publish — два
раздельных шага роняли build с NETSDK1064 на свежих analyzer-
зависимостях, теперь restore идёт внутри publish.
Smoke (стейдж):
- nurnetps@gmail.com /connect/token → invalid_grant.
- admin@food-market.local /connect/token → access_token выдан.
- food-market.zat.kz/, /signup/, app.../login, /health → 200.
Cross-checked every entity (Product, Counterparty, Supply, RetailSale,
Stock, Store, RetailPoint, Organization, ProductGroup, Barcode, Price,
PriceType, Country, Currency, VatRate, UoM) against real responses from
OtherSystem's API — a flat list of:
- fields we have and MS doesn't (to justify or drop)
- fields MS has and we don't (to add)
- semantic mismatches (e.g. MS holds prices in kopecks, our decimal)
Report only, no code changes — to be discussed with the user before
touching models/migrations. Priorities are split into P1 (import
parity: ExternalCode, Code, TrackingType enum, PaymentItemType, KZ
entrepreneur type), P2 (semantic fixes: RetailSale payment sums,
Overhead on supply, legal fields on Organization), P3 (nice-to-have),
and a list of deliberate divergences (why our VatRate/StockMovement
exist even though MS doesn't model them that way).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pushing straight to GitHub from KZ is a lottery — TCP to github.com
times out often enough that git push becomes a flake. Fix: Forgejo runs
on the stage server (sqlite, single container), all pushes go there
first (local network, always reliable), a systemd timer mirrors the
whole repo into GitHub every 10 minutes so GitHub stays up-to-date as
a backup + CI source.
What's committed here is the infra-as-code side:
- deploy/forgejo/docker-compose.yml — Forgejo 7 on :3000 (HTTP) and :2222 (SSH)
- deploy/forgejo/food-market-forgejo.service — systemd unit that drives compose
- deploy/forgejo/mirror-to-github.sh + mirror timer/service — push to GH every 10 min
- deploy/forgejo/nginx.conf — vhost for git.zat.kz (certbot to be run once DNS is set)
- docs/forgejo.md — how to clone/push, operations, what's left for the user (DNS + certbot)
GitHub Actions CI is untouched: commits land on GitHub via the mirror
and the self-hosted runner picks them up as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Telegram bridge lets me drive the local Claude Code tmux session from my
phone — inbound messages are typed into the 'claude' session, pane diffs
are streamed back as plain Telegram messages (TUI noise, tool-call
blocks, echoed user input and already-sent lines are filtered so only
the assistant's actual reply reaches the chat). Deployed as
food-market-telegram-bridge.service, reads creds from
/etc/food-market/telegram.env (not committed).
Also committing the local docker-registry unit for reproducibility —
registry:2 on 127.0.0.1:5001, data persisted in
/opt/food-market-data/docker-registry.
Setup docs in docs/telegram-bridge.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>