diff --git a/cms-admin-assets-root-folder-seed.md b/cms-admin-assets-root-folder-seed.md new file mode 100644 index 0000000..5c603a7 --- /dev/null +++ b/cms-admin-assets-root-folder-seed.md @@ -0,0 +1,92 @@ +--- +title: Admin assets crash — missing root AssetsFolder seed +type: concept +tags: [cms, admin, data-seed, mssql, gotcha, null-check] +sources: [../sources/iis-host-migration-2026-05-19.md] +updated: 2026-05-19 +--- + +# CMS admin assets crash — root AssetsFolder seed + +`AssetsJsonViewModelBuilder.cs:22` (admin /admin/assets/.../getList) падает NullReferenceException на любом site, где **root row `Folders` (Discriminator='AssetsFolder', LoweredPath='/', OwnerId=SiteId)** отсутствует. Root создаётся lazy (при first asset upload через admin), и sites которые никогда не использовали admin assets UI — без root. + +Связано: [[cms-server-port-leak-fix]] (нашли при verification после port-leak fix), [[recovery-architecture-snapshot]] (был open issue #8). + +## Симптом + +``` +GET /admin/assets//getList?path= +→ 500 NullReferenceException + +[NullReferenceException] + MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build(...) + AssetsJsonViewModelBuilder.cs:22 + MoreThenCms.Web.Admin.Controllers.AssetsController.GetList(Guid ownerId, String path) + AssetsController.cs:46 +``` + +В `AssetsController.GetList` (line 46): +```csharp +var model = _assetsViewModelBuilder.Build( + _assetsFoldersService.GetFolderByPath(ExecutionContext, ownerId, path), ...); +``` + +`GetFolderByPath` (`MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54`) делает `repo.Find(p => p.LoweredPath == path && p.OwnerId == ownerId)` — возвращает **null** если row нет. `Build` дальше делает `new JValue(model.ParentPath)` без null-check → crash. + +Frontend код (`AssetsAppFunc.cs:66-86`) делает proper null-check → HTTP 404. Только admin view-model builder упустил. + +## Кого затрагивает (snapshot 2026-05-19) + +После recovery: **15 sites без root** (из ~50). Sites где admin assets когда-либо открывался → root есть. Остальные: +- emspb.ru, pilorama98.ru, labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru +- 4 sites с NULL `PrimaryDomain` (legacy/test data) + +Проверочный запрос: +```sql +SELECT s.SiteId, s.PrimaryDomain, + CASE WHEN EXISTS (SELECT 1 FROM Folders f + WHERE f.OwnerId = s.SiteId + AND f.LoweredPath = '/' + AND f.Discriminator = 'AssetsFolder') + THEN 'OK' ELSE 'NO ROOT' END AS RootStatus +FROM Sites s +ORDER BY RootStatus, s.PrimaryDomain; +``` + +## Fix (применён 2026-05-19) + +DB seed — один INSERT для всех missing roots, идемпотентный (`WHERE NOT EXISTS`): + +```sql +DECLARE @CreatedById uniqueidentifier = 'E4CC416B-5D13-4757-8EFE-03CBEA15B18C'; -- existing admin user +DECLARE @Inserted TABLE (FolderId uniqueidentifier, OwnerId uniqueidentifier); + +INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath, + DateCreated, UtcDateCreated, CreatedById, Discriminator) +OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted +SELECT NEWID(), s.SiteId, '/', '/', + SYSDATETIME(), SYSUTCDATETIME(), @CreatedById, 'AssetsFolder' +FROM Sites s +WHERE NOT EXISTS (SELECT 1 FROM Folders f + WHERE f.OwnerId = s.SiteId + AND f.LoweredPath = '/' + AND f.Discriminator = 'AssetsFolder'); + +SELECT FolderId, OwnerId FROM @Inserted; -- save for atomic revert +``` + +15 rows вставлены. Inserted FolderId/OwnerId сохранены в `.tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt` для atomic revert (`DELETE FROM Folders WHERE FolderId IN (...)`). + +После: `/admin/assets//getList?path=` возвращает HTTP 302 → /login для unauth (normal auth path), вместо 500. Authorized users видят empty assets list. + +## Долгосрочный TODO (не сделано — нет рабочего build env) + +`AssetsJsonViewModelBuilder.Build` должна null-guard'иться — возвращать empty JObject ({parentPath:"", path:"/", folders:[], files:[]}) когда model null. Это **defensive code**, плюс auto-create root в `GetFolderByPath` для root path (как обычно делают админ-репозитории). + +Требует recompile `MoreThenCms.Admin.dll` — отложено до восстановления полного build pipeline (gitea/build/registry — см. [[vds-kzntsv-bootstrap]] task). + +## Возможно затрагивает ещё (open для следующей сессии) + +`Folders` table — polymorphic: `AssetsFolder`, `ImagesFolder` (OwnerId=ThemeId), `ScriptsFolder` (ThemeId), `StylesheetsFolder` (ThemeId). Аналогичная null-falling логика может быть в admin views для Themes (Stylesheets/Scripts/Images). Не проверено, не reported user'ом. + +Если будет — same pattern: `SELECT s.SiteId vs Folders root по соответствующему Discriminator` + seed. diff --git a/cms-server-port-leak-fix.md b/cms-server-port-leak-fix.md index 28d29e2..d14b60d 100644 --- a/cms-server-port-leak-fix.md +++ b/cms-server-port-leak-fix.md @@ -169,6 +169,6 @@ Site `Web.config` MOJET содержать `` - URL Rewrite 2.1 MSI installed. - Затрагивает **все 11 cms hosts** (общий site `snolla`): emspb, labtools, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials, rimiz, plus snolla.com sub-domains. -## Известное ограничение (НЕ закрыто этим fix'ом) +## Sibling fix (отдельная task, тот же день) -`/admin/assets//getList?path=` на `emspb.snolla.com` admin → 500 NullReferenceException в `AssetsJsonViewModelBuilder.cs:22` (`model.ParentPath` на null). User подтвердил «только этот site». `_assetsFoldersService.GetFolderByPath(ctx, ownerId, '')` возвращает null. Скорее всего связано с пустыми settings `` (basePath/siteUrl/sitePath = ""). **Отдельный bug**, не в scope этого fix'а — отдельная task если будет приоритет. +После port-leak fix'а surfaced `/admin/assets//getList → 500 NullReferenceException` на admin assets. Оказался **не site-specific** — общий для 15 sites без root AssetsFolder в DB. Решён DB seed'ом, см. [[cms-admin-assets-root-folder-seed]]. diff --git a/recovery-architecture-snapshot.md b/recovery-architecture-snapshot.md index 8406b4f..174f57a 100644 --- a/recovery-architecture-snapshot.md +++ b/recovery-architecture-snapshot.md @@ -150,7 +150,7 @@ ACL: `IIS AppPool\:(OI)(CI)M` рекурсивно на каждом site 1. ~~**X-Forwarded headers** не передаются от traefik в IIS → CMS делает redirect с `:4443` в URL.~~ → **RESOLVED 2026-05-19 вечер** через URL Rewrite 2.1 + `` rule на host IIS. Также закрыл утечку `:8089` в admin SPA после attempt 2 миграции. См. [[cms-server-port-leak-fix]]. 2. **rimiz.ru → 404**. CMS-side, не инфра. 3. **ics-artmaterials.com → 404**. Аналогично — CMS-side (`www.ics-artmaterials.com → 301 → ics-artmaterials.com → 404`). -8. **emspb.snolla.com `/admin/assets//getList` → 500 NullReferenceException** в `AssetsJsonViewModelBuilder.cs:22` (`model` null). User подтвердил «только этот site». Не в scope port-leak fix; CMS-side bug в storage/Site mapping. Открыт. +8. ~~**emspb.snolla.com `/admin/assets//getList` → 500 NullReferenceException**.~~ → **RESOLVED 2026-05-19 вечер** через DB seed root AssetsFolder rows для 15 sites без них (включая emspb, pilorama98, и др.). Симптом был НЕ site-specific — общий для всех sites которые никогда не использовали admin assets UI. См. [[cms-admin-assets-root-folder-seed]]. Долгосрочный TODO — null-guard в `AssetsJsonViewModelBuilder.Build` (требует recompile DLL, отложено до восстановления build env). 4. **acme.json HTTP-01 renewal** будет фейлить для доменов с DNS не на нашем IP → переезд на DNS-01 через REGRU до истечения сертификатов (~3 месяца). 5. **C:\inetpub\logs\** растёт — нужна ротация. 6. **docker.sock provider в traefik** не работает — но не блокирует (всё через file-provider). См. [[traefik-on-windows-docker-desktop]] Pitfall 3.