Merge pull request #4 from tiquis0290/dev
pulling changes from dev branch
This commit is contained in:
@@ -6,8 +6,8 @@ data
|
|||||||
uploads
|
uploads
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
.env
|
**/.env
|
||||||
.env.*
|
**/.env.*
|
||||||
*.log
|
*.log
|
||||||
*.md
|
*.md
|
||||||
!client/**/*.md
|
!client/**/*.md
|
||||||
@@ -21,8 +21,12 @@ unraid-template.xml
|
|||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
coverage
|
**/coverage
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
sonar-project.properties
|
||||||
|
server/tests/
|
||||||
|
server/vitest.config.ts
|
||||||
|
server/reset-admin.js
|
||||||
|
|||||||
73
.github/workflows/docker.yml
vendored
73
.github/workflows/docker.yml
vendored
@@ -5,9 +5,67 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
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:
|
build:
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
needs: version-bump
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -21,6 +79,8 @@ jobs:
|
|||||||
run: echo "PLATFORM_PAIR=$(echo ${{ matrix.platform }} | sed 's|/|-|g')" >> $GITHUB_ENV
|
run: echo "PLATFORM_PAIR=$(echo ${{ matrix.platform }} | sed 's|/|-|g')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@@ -54,13 +114,11 @@ jobs:
|
|||||||
|
|
||||||
merge:
|
merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: [version-bump, build]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
- name: Get version from package.json
|
ref: main
|
||||||
id: version
|
|
||||||
run: echo "VERSION=$(node -p "require('./server/package.json').version")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download build digests
|
- name: Download build digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -79,12 +137,13 @@ jobs:
|
|||||||
- name: Create and push multi-arch manifest
|
- name: Create and push multi-arch manifest
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
|
VERSION=${{ needs.version-bump.outputs.version }}
|
||||||
mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *)
|
mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *)
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t mauriceboe/trek:latest \
|
-t mauriceboe/trek:latest \
|
||||||
-t mauriceboe/trek:${{ steps.version.outputs.VERSION }} \
|
-t mauriceboe/trek:$VERSION \
|
||||||
-t mauriceboe/nomad:latest \
|
-t mauriceboe/nomad:latest \
|
||||||
-t mauriceboe/nomad:${{ steps.version.outputs.VERSION }} \
|
-t mauriceboe/nomad:$VERSION \
|
||||||
"${digests[@]}"
|
"${digests[@]}"
|
||||||
|
|
||||||
- name: Inspect manifest
|
- name: Inspect manifest
|
||||||
|
|||||||
57
CONTRIBUTING.md
Normal file
57
CONTRIBUTING.md
Normal file
@@ -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 `#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`
|
||||||
|
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 `#github-pr` 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.
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
proxyThumbnail,
|
proxyThumbnail,
|
||||||
proxyOriginal,
|
proxyOriginal,
|
||||||
isValidAssetId,
|
isValidAssetId,
|
||||||
|
canAccessUserPhoto,
|
||||||
listAlbums,
|
listAlbums,
|
||||||
syncAlbumAssets,
|
syncAlbumAssets,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
@@ -86,7 +87,12 @@ router.get('/assets/:assetId/info', authenticate, async (req: Request, res: Resp
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).json({ error: 'Invalid asset ID' });
|
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 });
|
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||||
res.json(result.data);
|
res.json(result.data);
|
||||||
});
|
});
|
||||||
@@ -97,7 +103,12 @@ router.get('/assets/:assetId/thumbnail', authFromQuery, async (req: Request, res
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
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);
|
if (result.error) return res.status(result.status!).send(result.error);
|
||||||
res.set('Content-Type', result.contentType!);
|
res.set('Content-Type', result.contentType!);
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
@@ -108,7 +119,12 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res:
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { assetId } = req.params;
|
const { assetId } = req.params;
|
||||||
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
|
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);
|
if (result.error) return res.status(result.status!).send(result.error);
|
||||||
res.set('Content-Type', result.contentType!);
|
res.set('Content-Type', result.contentType!);
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
|
|||||||
@@ -190,11 +190,27 @@ export async function searchPhotos(
|
|||||||
|
|
||||||
// ── Asset Info / Proxy ─────────────────────────────────────────────────────
|
// ── 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(
|
export async function getAssetInfo(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ data?: any; error?: string; status?: 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 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -232,9 +248,11 @@ export async function getAssetInfo(
|
|||||||
|
|
||||||
export async function proxyThumbnail(
|
export async function proxyThumbnail(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: 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 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -253,9 +271,11 @@ export async function proxyThumbnail(
|
|||||||
|
|
||||||
export async function proxyOriginal(
|
export async function proxyOriginal(
|
||||||
userId: number,
|
userId: number,
|
||||||
assetId: string
|
assetId: string,
|
||||||
|
ownerUserId?: number
|
||||||
): Promise<{ buffer?: Buffer; contentType?: string; error?: string; status?: 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 };
|
if (!creds) return { error: 'Not found', status: 404 };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user