From d765a80ea30bb4c80f5a9425406d505fe774a296 Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 3 Apr 2026 22:32:20 +0200 Subject: [PATCH 1/5] fix(immich): proxy shared photos using owner's Immich credentials Trip members viewing another member's shared photo were getting a 404 because the proxy endpoints always used the requesting user's Immich credentials instead of the photo owner's. The ?userId= query param the client already sent was silently ignored. - Add canAccessUserPhoto() to verify the asset is shared and the requesting user is a trip member before allowing cross-user proxying - Pass optional ownerUserId through proxyThumbnail, proxyOriginal, and getAssetInfo so credentials are fetched for the correct user - Enforce shared=1 check so unshared photos remain inaccessible --- server/src/routes/immich.ts | 22 ++++++++++++++++--- server/src/services/immichService.ts | 32 ++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/server/src/routes/immich.ts b/server/src/routes/immich.ts index 198b6e8..8cc93b7 100644 --- a/server/src/routes/immich.ts +++ b/server/src/routes/immich.ts @@ -20,6 +20,7 @@ import { proxyThumbnail, proxyOriginal, isValidAssetId, + canAccessUserPhoto, listAlbums, listAlbumLinks, createAlbumLink, @@ -143,7 +144,12 @@ router.get('/assets/:assetId/info', authenticate, async (req: Request, res: Resp const authReq = req as AuthRequest; const { assetId } = req.params; if (!isValidAssetId(assetId)) return res.status(400).json({ error: 'Invalid asset ID' }); - const result = await getAssetInfo(authReq.user.id, assetId); + const queryUserId = req.query.userId ? Number(req.query.userId) : undefined; + const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined; + if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) { + return res.status(403).json({ error: 'Forbidden' }); + } + const result = await getAssetInfo(authReq.user.id, assetId, ownerUserId); if (result.error) return res.status(result.status!).json({ error: result.error }); res.json(result.data); }); @@ -154,7 +160,12 @@ router.get('/assets/:assetId/thumbnail', authFromQuery, async (req: Request, res const authReq = req as AuthRequest; const { assetId } = req.params; if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID'); - const result = await proxyThumbnail(authReq.user.id, assetId); + const queryUserId = req.query.userId ? Number(req.query.userId) : undefined; + const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined; + if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) { + return res.status(403).send('Forbidden'); + } + const result = await proxyThumbnail(authReq.user.id, assetId, ownerUserId); if (result.error) return res.status(result.status!).send(result.error); res.set('Content-Type', result.contentType!); res.set('Cache-Control', 'public, max-age=86400'); @@ -165,7 +176,12 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res: const authReq = req as AuthRequest; const { assetId } = req.params; if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID'); - const result = await proxyOriginal(authReq.user.id, assetId); + const queryUserId = req.query.userId ? Number(req.query.userId) : undefined; + const ownerUserId = queryUserId && queryUserId !== authReq.user.id ? queryUserId : undefined; + if (ownerUserId && !canAccessUserPhoto(authReq.user.id, ownerUserId, assetId)) { + return res.status(403).send('Forbidden'); + } + const result = await proxyOriginal(authReq.user.id, assetId, ownerUserId); if (result.error) return res.status(result.status!).send(result.error); res.set('Content-Type', result.contentType!); res.set('Cache-Control', 'public, max-age=86400'); diff --git a/server/src/services/immichService.ts b/server/src/services/immichService.ts index 6468338..aceb8c0 100644 --- a/server/src/services/immichService.ts +++ b/server/src/services/immichService.ts @@ -230,11 +230,27 @@ export function togglePhotoSharing(tripId: string, userId: number, assetId: stri // ── Asset Info / Proxy ───────────────────────────────────────────────────── +/** + * Verify that requestingUserId can access a shared photo belonging to ownerUserId. + * The asset must be shared (shared=1) and the requesting user must be a member of + * the same trip that contains the photo. + */ +export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, assetId: string): boolean { + const row = db.prepare(` + SELECT tp.trip_id FROM trip_photos tp + WHERE tp.immich_asset_id = ? AND tp.user_id = ? AND tp.shared = 1 + `).get(assetId, ownerUserId) as { trip_id: number } | undefined; + if (!row) return false; + return !!canAccessTrip(String(row.trip_id), requestingUserId); +} + export async function getAssetInfo( userId: number, - assetId: string + assetId: string, + ownerUserId?: number ): Promise<{ data?: any; error?: string; status?: number }> { - const creds = getImmichCredentials(userId); + const effectiveUserId = ownerUserId ?? userId; + const creds = getImmichCredentials(effectiveUserId); if (!creds) return { error: 'Not found', status: 404 }; try { @@ -272,9 +288,11 @@ export async function getAssetInfo( export async function proxyThumbnail( userId: number, - assetId: string + assetId: string, + ownerUserId?: number ): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> { - const creds = getImmichCredentials(userId); + const effectiveUserId = ownerUserId ?? userId; + const creds = getImmichCredentials(effectiveUserId); if (!creds) return { error: 'Not found', status: 404 }; try { @@ -293,9 +311,11 @@ export async function proxyThumbnail( export async function proxyOriginal( userId: number, - assetId: string + assetId: string, + ownerUserId?: number ): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: number }> { - const creds = getImmichCredentials(userId); + const effectiveUserId = ownerUserId ?? userId; + const creds = getImmichCredentials(effectiveUserId); if (!creds) return { error: 'Not found', status: 404 }; try { From 20ce7460c1b372036348149dcae0d5a6d469544a Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 3 Apr 2026 22:59:08 +0200 Subject: [PATCH 2/5] docs: add contributing guidelines --- CONTRIBUTING.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ece91c3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to TREK + +Thanks for your interest in contributing! Please read these guidelines before opening a pull request. + +## Ground Rules + +1. **Ask in Discord first** — Before writing any code, pitch your idea in the `#dev-discussion` channel on our [Discord server](https://discord.gg/P7TUxHJs). We'll let you know if the PR is wanted and give direction. PRs that show up without prior discussion will be closed +2. **One change per PR** — Keep it focused. Don't bundle unrelated fixes or refactors +3. **No breaking changes** — Backwards compatibility is non-negotiable +4. **Target the `dev` branch** — All PRs must be opened against `dev`, not `main` +5. **Match the existing style** — No reformatting, no linter config changes, no "while I'm here" cleanups + +## Pull Requests + +### Your PR should include: + +- **Summary** — What does this change and why? (1-3 bullet points) +- **Test plan** — How did you verify it works? +- **Linked issue** — Reference the issue (e.g. `Fixes #123`) + +### Your PR will be closed if it: + +- Wasn't discussed and approved in `#dev-discussion` on Discord first +- Introduces breaking changes +- Adds unnecessary complexity or features beyond scope +- Reformats or refactors unrelated code +- Adds dependencies without clear justification + +### Commit messages + +Use [conventional commits](https://www.conventionalcommits.org/): + +``` +fix(maps): correct zoom level on Safari +feat(budget): add CSV export for expenses +``` + +## Development Setup + +```bash +git clone https://github.com/mauriceboe/TREK.git +cd TREK + +# Server +cd server && npm install && npm run dev + +# Client (separate terminal) +cd client && npm install && npm run dev +``` + +Server: `http://localhost:3001` | Client: `http://localhost:5173` + +On first run, check the server logs for the auto-generated admin credentials. + +## More Details + +See the [Contributing wiki page](https://github.com/mauriceboe/TREK/wiki/Contributing) for the full tech stack, architecture overview, and detailed guidelines. From eae799c7d63e678b9ec0aba8adbb8e715161be95 Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 3 Apr 2026 23:07:00 +0200 Subject: [PATCH 3/5] fix(deployment): remove unessessary files from docker image --- .dockerignore | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index d9ee946..c0defbd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,8 +6,8 @@ data uploads .git .github -.env -.env.* +**/.env +**/.env.* *.log *.md !client/**/*.md @@ -21,8 +21,12 @@ unraid-template.xml *.db *.db-shm *.db-wal -coverage +**/coverage .DS_Store Thumbs.db .vscode .idea +sonar-project.properties +server/tests/ +server/vitest.config.ts +server/reset-admin.js From c05640100032e00067032a5c57936719cc06a92f Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 3 Apr 2026 23:44:11 +0200 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20auto=20version=20bump=20on=20main=20?= =?UTF-8?q?=E2=80=94=20minor=20for=20dev=20merges,=20patch=20for=20hotfixe?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker.yml | 73 ++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 05ad1f8..27bab6f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,9 +5,67 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: write + jobs: + version-bump: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.VERSION }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine bump type and update version + id: bump + run: | + # Check if this push is a merge commit from dev branch + COMMIT_MSG=$(git log -1 --pretty=%s) + PARENT_COUNT=$(git log -1 --pretty=%p | wc -w) + + if echo "$COMMIT_MSG" | grep -qiE "^Merge (pull request|branch).*dev"; then + BUMP="minor" + elif [ "$PARENT_COUNT" -gt 1 ] && git log -1 --pretty=%P | xargs -n1 git branch -r --contains 2>/dev/null | grep -q "origin/dev"; then + BUMP="minor" + else + BUMP="patch" + fi + + echo "Bump type: $BUMP" + + # Read current version + CURRENT=$(node -p "require('./server/package.json').version") + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + + if [ "$BUMP" = "minor" ]; then + MINOR=$((MINOR + 1)) + PATCH=0 + else + PATCH=$((PATCH + 1)) + fi + + NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" + echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "$CURRENT → $NEW_VERSION ($BUMP)" + + # Update both package.json files + cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd .. + cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd .. + + # Commit and tag + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add server/package.json server/package-lock.json client/package.json + git commit -m "chore: bump version to $NEW_VERSION [skip ci]" + git tag "v$NEW_VERSION" + git push origin main --follow-tags + build: runs-on: ${{ matrix.runner }} + needs: version-bump strategy: fail-fast: false matrix: @@ -21,6 +79,8 @@ jobs: run: echo "PLATFORM_PAIR=$(echo ${{ matrix.platform }} | sed 's|/|-|g')" >> $GITHUB_ENV - uses: actions/checkout@v4 + with: + ref: main - uses: docker/setup-buildx-action@v3 @@ -54,13 +114,11 @@ jobs: merge: runs-on: ubuntu-latest - needs: build + needs: [version-bump, build] steps: - uses: actions/checkout@v4 - - - name: Get version from package.json - id: version - run: echo "VERSION=$(node -p "require('./server/package.json').version")" >> $GITHUB_OUTPUT + with: + ref: main - name: Download build digests uses: actions/download-artifact@v4 @@ -79,12 +137,13 @@ jobs: - name: Create and push multi-arch manifest working-directory: /tmp/digests run: | + VERSION=${{ needs.version-bump.outputs.version }} mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *) docker buildx imagetools create \ -t mauriceboe/trek:latest \ - -t mauriceboe/trek:${{ steps.version.outputs.VERSION }} \ + -t mauriceboe/trek:$VERSION \ -t mauriceboe/nomad:latest \ - -t mauriceboe/nomad:${{ steps.version.outputs.VERSION }} \ + -t mauriceboe/nomad:$VERSION \ "${digests[@]}" - name: Inspect manifest From 88a40c3294a4caef4841a601586297739b4a314e Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 3 Apr 2026 23:53:12 +0200 Subject: [PATCH 5/5] docs: update Discord channel to #github-pr --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ece91c3..684c775 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in contributing! Please read these guidelines before op ## Ground Rules -1. **Ask in Discord first** — Before writing any code, pitch your idea in the `#dev-discussion` channel on our [Discord server](https://discord.gg/P7TUxHJs). We'll let you know if the PR is wanted and give direction. PRs that show up without prior discussion will be closed +1. **Ask in Discord first** — Before writing any code, pitch your idea in the `#github-pr` channel on our [Discord server](https://discord.gg/P7TUxHJs). We'll let you know if the PR is wanted and give direction. PRs that show up without prior discussion will be closed 2. **One change per PR** — Keep it focused. Don't bundle unrelated fixes or refactors 3. **No breaking changes** — Backwards compatibility is non-negotiable 4. **Target the `dev` branch** — All PRs must be opened against `dev`, not `main` @@ -20,7 +20,7 @@ Thanks for your interest in contributing! Please read these guidelines before op ### Your PR will be closed if it: -- Wasn't discussed and approved in `#dev-discussion` on Discord first +- Wasn't discussed and approved in `#github-pr` on Discord first - Introduces breaking changes - Adds unnecessary complexity or features beyond scope - Reformats or refactors unrelated code