472 Commits
Author SHA1 Message Date
REDCODE 593d88d3e3 perf: add BuildKit cache mounts for NuGet packages in Dockerfiles
Build and Push Containers / build-and-push (milkstream) (push) Has been cancelled
Build and Push Containers / build-and-push (lactose) (push) Has been cancelled
2026-07-17 01:58:53 +02:00
REDCODE 6463df51d1 CI: added sixlabors license key to the docker file for lactose 2026-07-17 01:43:37 +02:00
REDCODE f57e2864c5 refactor: replace individual [FromQuery] params with AssetBrowseOptionsDto 2026-07-17 00:07:47 +02:00
REDCODE d89f6f0a21 fix: address PR #142 review issues
- Fix timer leak: implement IDisposable on AlbumAssetPicker
- Remove unnecessary full-table COUNT in GetFolderGroups
- Remove dead DirectoryEntryDto.AssetCount
- Remove unused ExistingAssetIds parameter
- Remove redundant .Distinct() after .Union()
- Extract hardcoded page size 150 to named constant
- Rename AssetSearchOptionsDto.Unassigned to Unlinked for consistency
- BrowseAssets returns 404 for missing folderId instead of 200 empty
2026-07-17 00:00:37 +02:00
REDCODE 8589a85625 refactor: rename UnlinkedAssetGroupDto to AssetGroupDto 2026-07-16 23:14:14 +02:00
REDCODE 983cd8baef refactor: add Embedded mode, UnlinkedOnly param, rename methods from Unlinked to generic names 2026-07-16 23:13:18 +02:00
REDCODE eae6152738 fix: filter directory names to only paths containing a slash after prefix, excluding filenames 2026-07-16 22:56:17 +02:00
REDCODE c4788e1572 fix: use parameterized SqlQueryRaw instead of unquoted string.Format to prevent SQL error 2026-07-16 22:51:41 +02:00
REDCODE 1dfa9eaf41 perf: replace in-memory path parsing with SELECT DISTINCT SQL, remove dir count badges 2026-07-16 22:44:20 +02:00
REDCODE 50b5a28841 perf: replace in-memory path parsing with SQL SPLIT_PART aggregation and double-LIKE filter 2026-07-16 22:15:11 +02:00
REDCODE 5d9aa21987 feat: add file-browser BrowseUnlinkedAssets endpoint with directory tree from OriginalPath 2026-07-16 21:43:44 +02:00
REDCODE 90c81c2d20 fix: use Albums.Count==0 instead of !Any() for unlinked filter, add trace logging 2026-07-16 21:06:18 +02:00
REDCODE 34220e93d4 feat: extend AssetController with unlinked-groups endpoint and pass new search/unassigned/folder/uploader params 2026-07-16 20:52:33 +02:00
REDCODE 22706a09cd feat: extend AssetRepository with unassigned, folderId, uploadedBy, search filters and GetUnlinkedGroups 2026-07-16 20:51:34 +02:00
REDCODE 664c1de5da feat: add trigram GIN index on Asset.OriginalFilename for efficient ILike search 2026-07-16 20:50:30 +02:00
REDCODE 7dd00c0a5e fix: gate registration behind UserRegistrationEnabled setting
AuthController.Register() never checked the UserRegistrationEnabled
server setting, allowing anyone to register even when disabled.

- Inject ISettingsRepository into AuthController
- Return 403 Forbidden at top of Register() if setting is not 'true'
- Add setup test #3.5 to enable registration for the main test flow

Closes #141
2026-07-16 20:37:50 +02:00
REDCODE 3a195dbc15 chore: Adjusted rate-limits of thumbs, preview and jobs to avoid triggering them during normal usage 2026-07-16 20:09:47 +02:00
REDCODE c5ea749517 test: added rate limiting stress test 2026-07-16 19:17:37 +02:00
REDCODE ebe83fe691 chore: use record for config classes 2026-07-16 19:06:35 +02:00
REDCODE ab58ac7dd8 Merge branch 'develop' into feature/rate-limit 2026-07-16 16:56:33 +00:00
REDCODE 1e2bc83b20 fix: guard against null PersonOwnerId in MergePeople albums query
Replace PersonOwnerId ?? Guid.Empty with explicit null check to
avoid matching albums with no owner if sourceIds contained Guid.Empty.
2026-07-16 18:30:23 +02:00
MrFastwind 58014a37bd feat: apply rate-limit buckets to controllers
- AuthController: auth bucket (10 req/60s) — anti-brute-force
- MediaController.GetImage: media_original bucket (50 req/60s)
- MediaController.GetThumb: media_thumb bucket (200 req/60s)
- MediaController.GetPreview: media_preview bucket (200 req/60s)
- JobsController: jobs bucket (30 req/10s) — quick refresh for long-polling
- All other endpoints: global bucket (100 req/60s) via default
2026-07-16 18:26:39 +02:00
MrFastwind c79ed47e41 feat: add rate limiting with configurable buckets
Add fixed-window rate limiting to Lactose with 6 configurable buckets:
- Global (100 req/60s) for all generic calls
- Auth (10 req/60s) strict anti-brute-force
- MediaOriginal (50 req/60s) for full-size media
- MediaThumb (200 req/60s) for thumbnails
- MediaPreview (200 req/60s) for previews
- Jobs (30 req/10s) quick refresh for long-polling

All buckets configurable via appsettings.json RateLimiting section.
Rejected requests return HTTP 429 with JSON body and Retry-After header.
2026-07-16 18:17:58 +02:00
REDCODE e48da562fa test: flatten test numbering to sequential 1–137
Renumber all test blocks flatly with no gaps or sub-letter suffixes.
Split the remaining multi-request block in the bulk visibility section.
2026-07-16 18:08:17 +02:00
REDCODE b943d2e595 fix: split multi-request test blocks into single-request blocks
REST Client only processes the first request per ### block.
Split 99f into 99f/99f1 and 99i2 into 99i2/99i2b so dynamic
variables set by response handlers are available in subsequent tests.
2026-07-16 18:04:40 +02:00
REDCODE 0950106609 test: add merge and bulk visibility tests to WepApiTest.http
Covers: album merge validation and e2e, person merge validation and e2e,
bulk visibility update (people + assets), authorization gating.
2026-07-16 17:58:14 +02:00
REDCODE 7ba1ab1551 feat: add merge endpoints to AlbumController and PersonController
POST /api/album/merge — Curator/Admin, validates DestinationId not in SourceIds
POST /api/person/merge — Admin/Curator, validates DestinationId not in SourceIds
2026-07-16 17:56:52 +02:00
REDCODE 656e6fa9bf feat: add MergePeople to person repository
Reassigns albums (PersonOwnerId), faces (PersonId), and maintainers
from source people to destination, then hard-deletes source people.
2026-07-16 17:56:24 +02:00
REDCODE 5e1306e466 feat: add MergeAlbums to album repository
Loads destination + source albums with assets, moves all assets
from sources to destination (deduplicated), then hard-deletes sources.
2026-07-16 17:55:46 +02:00
REDCODE e62a567e4e fix: apply Visibility and MaintainerUserIds in PersonController.BulkUpdate
The bulk update path was ignoring Visibility and MaintainerUserIds
from PersonUpdateDto, unlike the single-update path.
2026-07-16 17:55:00 +02:00
REDCODE c7e83ed97b feat: bulk cosplayer assignment for albums from multiselect mode
Adds a shared LinkToCosplayerModal component with person typeahead
search and wires it into the albums list page's multiselect topbar.

The backend already supports bulk person assignment via POST /api/album
with BulkDto<AlbumUpdateDto> { Data.Person = ... }, so no backend
changes are needed.

Also adds API tests for the bulk album person assignment flow.

Part of #128, closes #122
2026-07-16 02:59:15 +02:00
REDCODE 7db1738678 fix: soft-delete users in UserController.Delete instead of hard-delete
UserController.Delete was calling userRepository.Delete(user) which performed
a hard delete (context.Users.Remove), violating the project's soft-delete
convention. Changed to set user.DeletedAt = DateTime.UtcNow, matching the
pattern used in AssetController.Delete.

Also removed the now-unused Delete method from IUserRepository and
UserRepository for consistency with AssetRepository (which also has no
Delete method).

Extended REST tests 103-104 to verify deletedAt is set after deletion.

Closes #130
2026-07-16 02:31:52 +02:00
REDCODE ca50554108 feat: wire up SystemUploaderId setting — seed, UI, and crawl job backfill
- Seed SystemUploaderId in DefaultSettings.json (empty = no default owner)
- Read setting once per crawl and apply UploadedBy on new assets
- Backfill UploadedBy on existing assets that are missing it
- Add UserSelect display type for user-picker setting
- Create SettingUserSelect component with search dropdown
- Add setting to Scanning group in Settings UI
2026-07-16 01:18:46 +02:00
REDCODE 3bf98cbfed fix: prevent private assets with null UploadedBy leaking to anonymous users
Adds userId.HasValue guard to the private-visibility condition in both
AssetRepository and MediaRepository, preventing the null==null match
when an anonymous visitor (userId=null) encounters an asset with null
UploadedBy.
2026-07-15 23:33:08 +02:00
REDCODE a203885377 Merge branch 'issue-78-album-detail-bulk-actions' into issue-78-role-visible-borders
Adds bulk delete, visibility modal, and rename Remove→Unlink to
AlbumDetail select mode, plus bugfix for missing Save() in bulk endpoints.
2026-07-15 21:48:18 +02:00
REDCODE fff5ea65df feat: add bulk delete and visibility change to AlbumDetail select mode
- Fix AssetController.BulkUpdate/BulkDelete — add missing Save() call
- Add BulkUpdateAssetsAsync and BulkDeleteAssetsAsync to AssetService
- Rename 'Remove N' → 'Unlink N' (removes from album, original behavior)
- Add 'Delete N' button — soft-deletes selected assets via BulkDelete
- Add 'Visibility' button — opens ModalFrame to pick Public/Protected/Private
- Apply calls BulkUpdateAssetsAsync with chosen visibility level
2026-07-15 21:46:28 +02:00
REDCODE 86f95d7461 feat: add role-visible colored borders on albums and assets based on item state (#78)
Backend:
- Add DeletedAt to AssetPreviewDto, AlbumAssetPreviewDto, AlbumPreviewDto
- Remove redundant DeletedAt from AssetDto (now inherited from base)
- Add viewerId parameter to ToAssetPreviewDto, ToAlbumPreviewDto,
  ToAlbumFullDto, ToPersonDetailedDto mappers
- Conditionally send DeletedAt only when viewer is Admin or asset uploader
- Pass uid/viewerId from all controller/repository call sites

Frontend:
- AlbumCard: show orange (not public) / red (deleted) border in select
  mode for admins/curators
- AlbumDetail: same border logic in GetTileClass for asset tiles
- Borders only appear in select mode per REDCODE's feedback
2026-07-15 20:48:00 +02:00
REDCODE 758696f7c9 fix: add entity-level visibility gating for albums and persons
Adds R3/R4 visibility gates to AlbumRepository.FindVisible and
PersonRepository.FindVisible so that 404 is returned when the
requesting user lacks permission to see the entity itself.

Adds comprehensive HTTP tests verifying:
- R2: anonymous asset detail access
- R3: Public/Protected visible to User, Private returns 404 (Admin sees all)
- R4: Private person returns 404 for User, 200 for Admin/Curator
2026-07-15 20:30:06 +02:00
REDCODE 6efccae030 fix: address PR #127 review issues — fallback, eager load, indentation, Maintainer tests
- Change ?? EAccessLevel.Admin fallback to ?? EAccessLevel.User in UserController.GetAll
- Add .Include(u => u.MaintainedPersons) to UserRepository.GetAll() so MaintainedPersonIds/MaintainedPersonNames populate in list endpoint
- Fix UsersMapper.cs method body indentation (was flush with class)
- Add Maintainer-role visibility tests (68.5-68.8): promote user, test viewing admin, demote back
2026-07-15 17:31:33 +02:00
REDCODE cace8640e9 fix: replace remaining jsonPath calls in asset visibility tests
Test 75 admin assertion: allow null email for server-scanned assets
(Guid.Empty placeholder has no email). Removes all remaining jsonPath
calls that could NPE on null resolved values.
2026-07-15 15:03:50 +02:00
REDCODE a3ce98a984 fix: replace jsonPath with response.body for null-value checks in visibility tests
jsonPath throws NullPointerException in GraalVM when the resolved JSON
value is null. Use response.body[n].property directly for all null
assertions in search-list and visibility tests.
2026-07-15 14:55:27 +02:00
REDCODE 4f3e3a28cc fix: enforce data visibility per access level with test coverage
- UsersMapper.ToGetUsersDto: filter Email, BannedAt, DeletedAt, AccessLevel,
  MaintainedPersonIds by viewer access (admin/self only)
- AssetsMapper.ToFullAssetsDto: pass viewerId to restrict UploadedBy.Email;
  null-safe Uploader fallback
- AssetsMapper.ToAssetPreviewDto: gate FileName behind Curator+ (was public)
- AlbumMapper: gate FileName behind Curator+; fix AssetCount to count only
  non-deleted assets
- AssetRepository.FindVisible: include Uploader to prevent NRE (500 error)
- AssetController: restrict UploadedBy reassignment to Admin only
- UserController: pass viewer context to all ToGetUsersDto calls
- WepApiTest.http: add 18 data visibility tests (#64-#81) verifying field
  gating per role; fix 3 pre-existing stale assertions; avoid jsonPath NPE
  on null values by using response.body directly
2026-07-15 14:53:39 +02:00
REDCODE 700d5a63a7 chore: fix Dockerfiles, add debug compose to solution 2026-07-15 14:21:18 +02:00
REDCODE 358bf0918e feat: maintainer assignment UI for cosplayers, maintainer display on user page
Backend:
- Add MaintainerUserIds to PersonUpdateDto/PersonDetailedDto
- Add MaintainedPersonNames to UserInfoDto
- Add IPersonRepository.SetMaintainers + implementation
- PersonRepository.Find/FindVisible now include Maintainers
- PersonController.Update handles maintainer assignment (admin/curator)
- Fix PersonController.Update to save Visibility field
- Allow curators to list all users via UserController.GetAll

Frontend (PersonForm):
- Add maintainer search+select UI (admin/curator only in edit mode)
- Search users client-side, click to add, badge with X to remove
- Saves maintainer IDs on person update

Frontend (CosplayerDetail):
- Show 'Maintained by: user1, user2' in header with clickable links
- Pass maintainer data to PersonForm

Frontend (User page):
- Show 'Maintainer of: cosplayer1, cosplayer2' with links
- Non-admin view: strip Email, Created, Deleted fields
- Fix LoadUser to use route UserId instead of logged-in user
2026-07-15 13:11:05 +02:00
REDCODE 656784af00 feat: maintainer cosplayer assignment in admin user edit modal
Backend:
- Add MaintainedPersonIds to UserUpdateDto
- Add SetMaintainedPersons to user repository (removes old PersonMaintainer rows, inserts new)
- Wire up in UserController.Update (admin only)

Frontend:
- Add PersonService injection to AdminUsers page
- Searchable cosplayer dropdown in edit modal for Maintainer users
- Click result to add, badge with X to remove
- Zero debounce, saves only on Submit
- Pre-loads currently assigned cosplayer names on modal open
2026-07-15 12:55:03 +02:00
REDCODE f32d0678df feat: add MaintainedPersonIds to UserInfoDto with backend plumbing 2026-07-15 11:22:33 +02:00
REDCODE 88a0098314 perf: add AsSplitQuery to repository queries with multiple Include calls 2026-07-14 22:00:33 +02:00
REDCODE e2b0eaba37 refactor: rename AlbumSearchParametersDto.UploadedBy to AssetUploadedBy
Clarifies that this property filters albums by assets uploaded by a user,
not albums themselves being uploaded by a user.
2026-07-14 21:30:14 +02:00
REDCODE e1504dbf72 style: standardize access level comparison to switch expressions
- AlbumRepository.SearchQuery: if/else → switch expression
- PersonRepository.FindVisible: nested if → switch statement with guarded case
- AlbumController Update/BulkDelete/Delete: if/else → switch
2026-07-14 21:29:46 +02:00
REDCODE b67b2260fe docs(repo): fix stale pageSize default in IAlbumRepository XML doc
Actual default is PagedParametersDto.MaxPageSize (changed in 08c3890),
but documentation still referenced the old hardcoded 150.
2026-07-14 21:27:15 +02:00