Commit Graph

1018 Commits

Author SHA1 Message Date
jubnl 405263d015 test(server): resolve async backupFileExists mocks for the tests typecheck 2026-08-17 08:05:11 +02:00
jubnl cd40369590 Merge branch 'origin/dev' into feat/storage-driver
Both sides appended a migration to the same schema_version slot: dev's
reservation_day_positions cross-trip cleanup keeps its published index,
the storage slice-2 trip_files prefix strip moves after it. The trailing-
migration guard now rewinds two versions and asserts both. Dev's new
places image_url/website tests gain the StorageService constructor arg
and async update() handling from the storage slices.
2026-08-17 08:00:41 +02:00
jubnl 5263ed3ccf test(coverage): raise slice-5 backup pin 2026-08-17 07:48:09 +02:00
jubnl 070d08da5e test(server): close slice-5 gaps surfaced by the coverage tier 2026-08-17 07:48:02 +02:00
jubnl ceb9af2480 refactor(server): boot dir-creation to driver init 2026-08-17 07:34:15 +02:00
jubnl 633103a246 refactor(backup): auto-backup retention through storage 2026-08-17 07:29:22 +02:00
jubnl 3838277048 refactor(backup): restore and upload-restore through storage 2026-08-17 07:22:17 +02:00
jubnl 4b8c6013a6 refactor(backup): create backup through per-category storage enumeration 2026-08-17 07:13:15 +02:00
jubnl e4109db9f6 refactor(backup): download through storage sendToResponse 2026-08-17 07:03:13 +02:00
jubnl 8039362e04 refactor(backup): list, existence and delete through storage 2026-08-17 07:00:00 +02:00
jubnl 6f22eb8296 test(coverage): raise slice-4 domain pins 2026-08-17 06:32:33 +02:00
jubnl af85d39799 test(server): close slice-4 gaps surfaced by the full tier 2026-08-17 06:32:32 +02:00
jubnl 5870dd4fda feat(journey): journey-thumbs orphan sweep and delete-site derived-artifact reclaim 2026-08-17 06:23:13 +02:00
jubnl 616897b60b fix(memories): reclaim orphaned trek photo cache objects in the ttl sweep 2026-08-17 06:19:31 +02:00
jubnl 0962df2f22 refactor(journey): photo deletion through storage 2026-08-17 06:17:24 +02:00
jubnl 939c2ec3e8 refactor(memories): immich upload reads through storage; drop uploads-root 2026-08-17 06:15:01 +02:00
jubnl 364f376b07 refactor(memories): thumbnail service category-addressed 2026-08-17 06:08:04 +02:00
jubnl 1a2e4d4ef2 refactor(memories): trek photo cache internals through storage 2026-08-17 06:05:52 +02:00
jubnl f8faef77c4 refactor(files): trash deletion through storage 2026-08-17 06:01:28 +02:00
jubnl f382a7635e refactor(collab): note-file deletion through storage 2026-08-17 05:51:44 +02:00
jubnl dfb8f8a8a4 refactor(auth): avatar reclaim through storage 2026-08-17 05:47:31 +02:00
jubnl 8a3327c5f6 refactor(trips): trip covers through storage 2026-08-17 05:37:52 +02:00
jubnl 965566369e refactor(places): reclaim place images and collection covers through storage 2026-08-17 05:31:53 +02:00
jubnl 4eca78b53e refactor(place-photos): route place-photo cache through storage 2026-08-17 05:20:09 +02:00
Maurice c9124c9b63 fix(places): only attach tags owned by someone on the trip
A tag belongs to a user, not to a trip, and the place create/update body is an
open record the Zod pipe does not police — so any tag id could be attached, and
the tag projection ships tags.user_id, which answers who owns a tag the caller
has no way to see. collection_place_tags had it verbatim.

Scoped to the roster rather than to the caller on purpose: a member tags a shared
place, another member re-saves it and, because the update is delete-then-insert,
sends that id back. Caller-scoping would drop a co-traveller's tag on every
foreign edit; roster-scoping keeps it. Collections resolve the same question
through their own member list.

No client sends a tags array today, so this only closes the hand-crafted path.
2026-08-16 23:17:50 +02:00
Maurice 244eb56e4c fix(budget): confine payers, split members and settlement parties to the trip
knownUserIds asked whether a user exists at all, not whether they are on this
trip, so any id on the instance could be made a payer or a split member. Both
read-backs join users, and the settlement projection carries the avatar through
avatarUrl, which passes an absolute URL straight out — for an OIDC account that
is the picture claim.

Payers and split members drop off-roster ids, as before, only against the right
set. Settlements cannot: they name exactly two parties and both columns are NOT
NULL, so dropping one would either break the constraint or move money to the
wrong person. Both wrappers refuse the write and return null, which lands on the
404 the update path already had. That answer is deliberately the same one a
missing settlement gets, so the endpoint says nothing about whether an id exists
— and it retires the foreign key error that used to surface as a 500 and drew
exactly that distinction.

createSettlement grows the 404 its sibling had; the MCP tool reuses the error
string already at hand. Money rules are untouched: a settlement is a transfer, so
refusing one cannot unbalance the sum, and the cent arithmetic is unchanged.
2026-08-16 23:17:50 +02:00
Maurice 0337ebbbd2 fix(assignments): confine day participants to the trip roster
setParticipants took the ids on trust. The caller is proven to be on the trip and
the assignment is proven to sit on it, but nothing said the same about the ids in
the body, and the read-back joins users — so a stranger's name and avatar came
back to the writer and went out over the assignment:participants broadcast to
everyone else in the room.

Takes the tripId both callers already hold and filters against the roster. Same
drop-rather-than-reject rule as bag members and reservation travellers: the
participants box posts the whole list on every edit, so a trip whose membership
changed underneath it would otherwise be stuck.

ASG-SVC-028 covered a foreign key error rolling the replace back. The filter
takes an unknown id out before the insert can reach it, so that path is gone and
the case now pins what happens instead: a list of nothing but strangers clears,
like the empty list ASG-SVC-018 already pins.
2026-08-16 23:17:50 +02:00
Maurice 1eb29f8b77 fix(trips): confine assignee writes to the people on the trip
Holding packing_edit or todo_edit says you may edit this trip's lists, not that
any user id you name belongs to it. Both updateCategoryAssignees loops wrote the
body's ids straight through, and both end by re-selecting through a JOIN on
users — so an id from anywhere on the instance came back with a display name and
an avatar attached, in the response and in the WebSocket fan-out to the room. The
packing item recipient writes had the same gap.

packing already had tripRosterIds for exactly this and used it only for bag
members; its doc comment said "assigned to a bag", which is probably why the
other three writes grew up without it. The query moves to DatabaseService next to
canAccessTrip, which asks the same question as a predicate, and the helper now
delegates. Every service that needs it already injects DatabaseService.

Off-roster ids drop rather than 400: a copied trip carries assignee ids over
before its members exist, and rejecting the whole request would stall the picker
there. Guests are unaffected — a guest is a trip_members row, so the roster
includes them, which is the point of #1362.
2026-08-16 23:17:50 +02:00
Maurice c8607ba234 fix(places): pin the image and website URL fields on the way in
image_url and website were bare strings in the contract, and the create/update
bodies are open records, so the Zod pipe never looked at either. Both leave the
database for something that treats them as a URL: the thumbnail is interpolated
into hand-built marker HTML, and the homepage is handed to window.open, where a
javascript: value would run in this origin rather than opening a page.

Both are allow-listed on the way in now, next to hexColorSchema and checked in
the handler the way validateRouteColor already is, on all three write surfaces
so REST, the plugin RPC and MCP agree on what they accept. The entity schemas
stay open: rows written before this still have to round-trip.

safeHttpUrl covers the read side for exactly those rows, the same split
safeHexColor uses, and the window.open calls it guards pick up
noopener,noreferrer while they are being touched.
2026-08-16 21:57:21 +02:00
Maurice 0f0bf50cd3 fix(assignments): guard the per-assignment routes like the day routes
AssignmentOpsController declared @RequirePermission('day_edit') on its
mutations but carried only JwtAuthGuard. TripAccessGuard is what reads that
decorator and what resolves :tripId, so on this controller the permission
markers were metadata nobody looked at and the trip in the URL was never
matched against the caller. Its sibling, DayAssignmentsController, has had the
pair since the migration.

The two participants handlers also went straight to the service with the id
from the path, unlike move/time/transport, which ask getAssignmentForTrip
first. They ask now too.
2026-08-16 21:57:21 +02:00
Maurice ff27624e02 fix(reservations): scope the per-day position write to the trip
updatePositions has two branches. The legacy one updates
`WHERE id = ? AND trip_id = ?`; the per-day one, three lines below it, bound
the caller's ids straight into reservation_day_positions and never touched the
tripId it was handed. That table has no trip_id of its own and its two foreign
keys only require the ids to exist, so nothing further down caught a pair that
did not belong together.

The trip now comes from the join, so the row materialises only when the
reservation and the day agree on it. Doing that in the statement rather than as
a pre-check also turns a stale id into a quiet no-op instead of a foreign-key
error surfacing as a 500, and lets an omitted day_plan_position bind the way
the wire contract has always said it may.

reorder_reservations reports a day that is not on the trip, the way its sibling
tools in that file already do, and a migration clears rows whose reservation
and day disagree.
2026-08-16 21:57:21 +02:00
jubnl d27e54952a test(storage): raise src/nest/storage branches pin 2026-08-16 18:36:43 +02:00
jubnl 31478e232a test(server): close serving-swap gaps surfaced by the full tier 2026-08-16 18:36:38 +02:00
jubnl 94490b7d31 fix(share): answer place-photo cache miss with 204 like the maps proxy 2026-08-16 11:16:17 +02:00
jubnl afd6d297c0 refactor(journey): serve public journey assets through storage 2026-08-16 11:12:28 +02:00
jubnl 1bf6517b75 refactor(share): stream shared place-photo bytes through storage 2026-08-16 11:07:04 +02:00
jubnl 786b1f5c07 refactor(maps): stream place-photo bytes through storage 2026-08-16 11:04:04 +02:00
jubnl 980f8be6e8 refactor(memories): serve photo bytes through storage 2026-08-16 10:54:06 +02:00
jubnl a0a5bb8203 refactor(files): serve trip-file downloads through storage 2026-08-16 10:50:28 +02:00
jubnl 4527e8688d refactor(platform): serve /uploads routes through storage 2026-08-16 10:46:01 +02:00
jubnl 36f3f89e14 test(storage): raise src/nest/storage coverage pin 2026-08-16 10:08:51 +02:00
jubnl 9ef886bbdd test(files): key FILE-P02 residue checks on upload size 2026-08-16 10:08:33 +02:00
jubnl db3c5da04e fix(storage): age-gate the boot spool sweep 2026-08-16 09:53:26 +02:00
jubnl 92c87d1f3a test(db): re-point the trailing-migration guard at the trip_files prefix strip 2026-08-16 09:53:26 +02:00
jubnl f9d84f5907 fix(storage): default defParamCharset to utf8 in the upload factory 2026-08-16 09:53:25 +02:00
jubnl b72645272b fix(server): consolidate allowed_file_types onto AllowedFileTypesService 2026-08-16 09:49:46 +02:00
jubnl c75d431e4c refactor(auth): spool avatar upload through storage 2026-08-16 09:46:27 +02:00
jubnl 8b0c768ba0 refactor(places): spool place-image and collection uploads through storage 2026-08-16 09:44:03 +02:00
jubnl 7e91f1abaf refactor(trips): spool trip cover upload through storage 2026-08-16 09:38:26 +02:00
jubnl b1abfd2110 refactor(journey): spool journey media uploads through storage 2026-08-16 09:34:48 +02:00