Compare commits

..

2 Commits

Author SHA1 Message Date
Marty Fuhry
6a156b813e background 2024-05-21 16:00:46 -04:00
Lukas
4907916345 fix(web): emit updated date when pressing enter (#9640)
wip uploading

format

wip first working version
2024-05-21 16:00:46 -04:00
616 changed files with 11384 additions and 18122 deletions

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version: '3.22.0'
cache: true cache: true
- name: Create the Keystore - name: Create the Keystore

View File

@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0 uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.7.0 uses: stumpylog/image-cleaner-action/untagged@v0.6.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"

View File

@@ -1,43 +0,0 @@
name: Docs build
on:
push:
branches: [main]
paths:
- "docs/**"
pull_request:
branches: [main]
paths:
- "docs/**"
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Check formatting
run: npm run format
- name: Run build
run: npm run build
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: docs-build-output
path: docs/build/
retention-days: 1

View File

@@ -1,189 +0,0 @@
name: Docs deploy
on:
workflow_run:
workflows: ["Docs build"]
types:
- completed
jobs:
checks:
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
steps:
- if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow failed' && exit 1
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const eventType = context.payload.workflow_run.event;
const isFork = context.payload.workflow_run.repository.fork;
let parameters;
console.log({eventType, isFork});
if (eventType == "push") {
const branch = context.payload.workflow_run.head_branch;
console.log({branch});
const shouldDeploy = !isFork && branch == "main";
parameters = {
event: "branch",
name: "main",
shouldDeploy
};
} else if (eventType == "pull_request") {
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
if(!pull_number) {
const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,})
const items = response.data.items
if (items.length < 1) {
throw new Error("No pull request found for the commit")
}
const pullRequestNumber = items[0].number
console.info("Pull request number is", pullRequestNumber)
pull_number = pullRequestNumber
}
const {data: pr} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number
});
console.log({pull_number});
parameters = {
event: "pr",
name: `pr-${pull_number}`,
pr_number: pull_number,
shouldDeploy: true
};
} else if (eventType == "release") {
parameters = {
event: "release",
name: context.payload.workflow_run.head_branch,
shouldDeploy: !isFork
};
}
console.log(parameters);
return parameters;
deploy:
runs-on: ubuntu-latest
needs: checks
if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Load parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const json = `${{ needs.checks.outputs.parameters }}`;
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data));
- name: Unzip artifact
run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"
- name: Deploy Docs Subdomain
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "apply"
- name: Deploy Docs Subdomain Output
id: docs-output
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "output -json"
- name: Output Cleaning
id: clean
run: |
TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
echo "output=$TG_OUT" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
workingDirectory: "docs"
directory: "build"
branch: ${{ steps.parameters.outputs.name }}
wranglerVersion: '3'
- name: Deploy Docs Release Domain
if: ${{ steps.parameters.outputs.event == 'release' }}
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/docs-release'
tg_command: 'apply'
- name: Comment
uses: actions-cool/maintain-one-comment@v3
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: |
📖 Documentation deployed to [${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }}](https://${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }})
emojis: 'rocket'
body-include: '<!-- Docs PR URL -->'

View File

@@ -1,32 +0,0 @@
name: Docs destroy
on:
pull_request_target:
types: [closed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Destroy Docs Subdomain
env:
TF_VAR_prefix_name: "pr-${{ github.event.number }}"
TF_VAR_prefix_event_type: "pr"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "destroy"
- name: Comment
uses: actions-cool/maintain-one-comment@v3
with:
number: ${{ github.event.number }}
delete: true
body-include: '<!-- Docs PR URL -->'

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: PR Conventional Commit Validation - name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.2.0 uses: ytanikin/PRConventionalCommits@1.1.0
with: with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
add_label: 'false' add_label: 'false'

View File

@@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version: '3.22.0'
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get

View File

@@ -10,6 +10,28 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
doc-tests:
name: Docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run build
run: npm run build
if: ${{ !cancelled() }}
server-unit-tests: server-unit-tests:
name: Server name: Server
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -186,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version: '3.22.0'
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
@@ -238,18 +260,9 @@ jobs:
name: OpenAPI Clients name: OpenAPI Clients
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix=server ci
- name: Build the app
run: npm --prefix=server run build
- name: Run API generation - name: Run API generation
run: make open-api run: make open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@v20 uses: tj-actions/verify-changed-files@v20
id: verify-changed-files id: verify-changed-files
@@ -257,8 +270,6 @@ jobs:
files: | files: |
mobile/openapi mobile/openapi
open-api/typescript-sdk open-api/typescript-sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
run: | run: |
@@ -321,7 +332,7 @@ jobs:
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: npm run sync:sql run: npm run sql:generate
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich

View File

@@ -37,7 +37,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
npm --prefix server run sync:sql npm --prefix server run sql:generate
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh

View File

@@ -1 +1 @@
20.14 20.13

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

106
cli/package-lock.json generated
View File

@@ -54,7 +54,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.13", "@types/node": "^20.12.12",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -1138,9 +1138,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.13", "version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1154,17 +1154,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.11.0", "@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.11.0", "@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1188,16 +1188,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1217,14 +1217,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0" "@typescript-eslint/visitor-keys": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1235,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.11.0", "@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1263,9 +1263,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1277,14 +1277,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1306,16 +1306,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.11.0" "@typescript-eslint/typescript-estree": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1329,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -1801,11 +1801,10 @@
"dev": true "dev": true
}, },
"node_modules/commander": { "node_modules/commander": {
"version": "12.1.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -4281,11 +4280,10 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.12", "version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"postcss": "^8.4.38", "postcss": "^8.4.38",

View File

@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"volta": { "volta": {
"node": "20.14.0" "node": "20.13.1"
} }
} }

View File

@@ -1,8 +1,7 @@
import { import {
Action, Action,
AssetBulkUploadCheckResult, AssetBulkUploadCheckResult,
AssetMediaResponseDto, AssetFileUploadResponseDto,
AssetMediaStatus,
addAssetsToAlbum, addAssetsToAlbum,
checkBulkUpload, checkBulkUpload,
createAlbum, createAlbum,
@@ -168,7 +167,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
newAssets.push({ id: response.id, filepath }); newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) { if (response.duplicate) {
duplicateCount++; duplicateCount++;
duplicateSize += stats.size ?? 0; duplicateSize += stats.size ?? 0;
} else { } else {
@@ -193,7 +192,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
return newAssets; return newAssets;
}; };
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => { const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
const { baseUrl, headers } = defaults; const { baseUrl, headers } = defaults;
const assetPath = path.parse(input); const assetPath = path.parse(input);
@@ -226,7 +225,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
formData.append('sidecarData', sidecarData); formData.append('sidecarData', sidecarData);
} }
const response = await fetch(`${baseUrl}/assets`, { const response = await fetch(`${baseUrl}/asset/upload`, {
method: 'post', method: 'post',
redirect: 'error', redirect: 'error',
headers: headers as Record<string, string>, headers: headers as Record<string, string>,

View File

@@ -1,4 +1,4 @@
import { getMyUser } from '@immich/sdk'; import { getMyUserInfo } from '@immich/sdk';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { mkdir, unlink } from 'node:fs/promises'; import { mkdir, unlink } from 'node:fs/promises';
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
@@ -10,13 +10,13 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
await connect(url, key); await connect(url, key);
const [error, user] = await withError(getMyUser()); const [error, userInfo] = await withError(getMyUserInfo());
if (error) { if (error) {
logError(error, 'Failed to load user info'); logError(error, 'Failed to load user info');
process.exit(1); process.exit(1);
} }
console.log(`Logged in as ${user.email}`); console.log(`Logged in as ${userInfo.email}`);
if (!existsSync(configDir)) { if (!existsSync(configDir)) {
// Create config folder if it doesn't exist // Create config folder if it doesn't exist

View File

@@ -1,4 +1,4 @@
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { BaseOptions, authenticate } from 'src/utils'; import { BaseOptions, authenticate } from 'src/utils';
export const serverInfo = async (options: BaseOptions) => { export const serverInfo = async (options: BaseOptions) => {
@@ -8,7 +8,7 @@ export const serverInfo = async (options: BaseOptions) => {
getServerVersion(), getServerVersion(),
getSupportedMediaTypes(), getSupportedMediaTypes(),
getAssetStatistics({}), getAssetStatistics({}),
getMyUser(), getMyUserInfo(),
]); ]);
console.log(`Server Info (via ${userInfo.email})`); console.log(`Server Info (via ${userInfo.email})`);

View File

@@ -1,4 +1,4 @@
import { getMyUser, init, isHttpError } from '@immich/sdk'; import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob'; import { glob } from 'fast-glob';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs'; import { createReadStream } from 'node:fs';
@@ -48,7 +48,7 @@ export const connect = async (url: string, key: string) => {
init({ baseUrl: url, apiKey: key }); init({ baseUrl: url, apiKey: key });
const [error] = await withError(getMyUser()); const [error] = await withError(getMyUserInfo());
if (isHttpError(error)) { if (isHttpError(error)) {
logError(error, 'Failed to connect to server'); logError(error, 'Failed to connect to server');
process.exit(1); process.exit(1);

37
cli/src/version.ts Normal file
View File

@@ -0,0 +1,37 @@
import { version } from '../package.json';
export interface ICliVersion {
major: number;
minor: number;
patch: number;
}
export class CliVersion implements ICliVersion {
constructor(
public readonly major: number,
public readonly minor: number,
public readonly patch: number,
) {}
toString() {
return `${this.major}.${this.minor}.${this.patch}`;
}
toJSON() {
const { major, minor, patch } = this;
return { major, minor, patch };
}
static fromString(version: string): CliVersion {
const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex);
if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number);
return new CliVersion(major, minor, patch);
} else {
throw new Error(`Invalid version format: ${version}`);
}
}
}
export const cliVersion = CliVersion.fromString(version);

38
deployment/.gitignore vendored
View File

@@ -1,38 +0,0 @@
# OpenTofu
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Terragrunt
# terragrunt cache directories
**/.terragrunt-cache/*
# Terragrunt debug output file (when using `--terragrunt-debug` option)
# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug
terragrunt-debug.tfvars.json

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,14 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_release_domain" {
account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
domain = "immich.app"
}
resource "cloudflare_record" "immich_app_release_domain" {
name = "immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,27 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}
data "terraform_remote_state" "cloudflare_immich_app_docs" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_immich_app_docs_${var.prefix_name}"
}
}

View File

@@ -1,20 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_release"
}
}

View File

@@ -1,4 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,26 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id
project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name
domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
}
resource "cloudflare_record" "immich_app_branch_subdomain" {
name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
output "immich_app_branch_subdomain" {
value = cloudflare_record.immich_app_branch_subdomain.hostname
}
output "immich_app_branch_pages_hostname" {
value = cloudflare_record.immich_app_branch_subdomain.value
}
output "pages_project_name" {
value = cloudflare_pages_domain.immich_app_branch_domain.project_name
}

View File

@@ -1,7 +0,0 @@
locals {
domain_name = "immich.app"
preview_prefix = contains(["branch", "pr"], var.prefix_event_type) ? "preview" : ""
archive_prefix = contains(["release"], var.prefix_event_type) ? "archive" : ""
deploy_domain_prefix = coalesce(local.preview_prefix, local.archive_prefix)
is_release = contains(["release"], var.prefix_event_type)
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,17 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}

View File

@@ -1,24 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
locals {
prefix_name = get_env("TF_VAR_prefix_name")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_${local.prefix_name}"
}
}

View File

@@ -1,5 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}
variable "prefix_event_type" {}

View File

@@ -1,20 +0,0 @@
locals {
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
}
remote_state {
backend = "pg"
config = {
conn_str = local.tf_state_postgres_conn_str
}
}
inputs = {
cloudflare_account_id = local.cloudflare_account_id
cloudflare_api_token = local.cloudflare_api_token
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
}

View File

@@ -9,9 +9,6 @@ services:
container_name: immich_server container_name: immich_server
command: ['/usr/src/app/bin/immich-dev'] command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
@@ -84,9 +81,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
healthcheck:
test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
@@ -102,11 +97,6 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
# set IMMICH_METRICS=true in .env to enable metrics # set IMMICH_METRICS=true in .env to enable metrics

View File

@@ -4,9 +4,6 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: immich-server:latest image: immich-server:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
@@ -15,12 +12,12 @@ services:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
restart: always
ports: ports:
- 2283:3001 - 2283:3001
depends_on: depends_on:
- redis - redis
- database - database
restart: always
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
@@ -41,9 +38,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
healthcheck:
test: redis-cli ping || exit 1
restart: always restart: always
database: database:
@@ -60,13 +55,7 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
# set IMMICH_METRICS=true in .env to enable metrics # set IMMICH_METRICS=true in .env to enable metrics
immich-prometheus: immich-prometheus:

View File

@@ -12,9 +12,6 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@@ -43,9 +40,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: docker.io/redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
healthcheck:
test: redis-cli ping || exit 1
restart: always restart: always
database: database:
@@ -58,13 +53,8 @@ services:
POSTGRES_INITDB_ARGS: '--data-checksums' POSTGRES_INITDB_ARGS: '--data-checksums'
volumes: volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always restart: always
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
volumes: volumes:
model-cache: model-cache:

View File

@@ -5,9 +5,6 @@ UPLOAD_LOCATION=./library
# The location where your database files are stored # The location where your database files are stored
DB_DATA_LOCATION=./postgres DB_DATA_LOCATION=./postgres
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v1.71.0" # The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release IMMICH_VERSION=release

View File

@@ -1 +1 @@
20.14 20.13

View File

@@ -67,7 +67,7 @@ Yes, with an [External Library](/docs/features/libraries.md).
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)? ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page. Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
### Why are only photos and not videos being uploaded to Immich? ### Why are only photos and not videos being uploaded to Immich?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,55 +0,0 @@
# Jobs and Workers
## Workers
### Architecture
The `immich-server` container contains multiple workers:
- `api`: responds to API requests for data and files for the web and mobile app.
- `microservices`: handles most other work, such as thumbnail generation and video encoding, in the form of _jobs_. Simply put, a job is a request to process data in the background.
## Split workers
If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/docs/install/environment-variables) to specify which container should pick up which tasks.
For example, for a simple setup with one container for the Web/API and one for all other microservices, you can do the following:
Copy the entire `immich-server` block as a new service and make the following changes to the **copy**:
```diff
- immich-server:
- container_name: immich_server
...
- ports:
- - 2283:3001
+ immich-microservices:
+ container_name: immich_microservices
```
Once you have two copies of the immich-server service, make the following chnages to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
```diff
services:
immich-server:
...
+ environment:
+ IMMICH_WORKERS_INCLUDE: 'api'
immich-microservices:
...
+ environment:
+ IMMICH_WORKERS_EXCLUDE: 'api'
```
## Jobs
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -0,0 +1,13 @@
# Jobs
The `immich-server` responds to API requests for data and files for the web and mobile app. To do this quickly and reliably, it offloads most other work to `immich-microservices` in the form of _jobs_. Simply put, a job is a request to process data in the background. Jobs are picked up automatically by microservices containers.
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -110,66 +110,8 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
## Example Configuration ## Example Configuration
<details>
<summary>Authentik Example</summary>
### Authentik Example
Here's an example of OAuth configured for Authentik: Here's an example of OAuth configured for Authentik:
Configuration of Authorised redirect URIs (Authentik OAuth2/OpenID Provider) ![OAuth Settings](./img/oauth-settings.png)
<img src={require('./img/authentik-redirect-uris-example.webp').default} width='70%' title="Authentik authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| Issuer URL | `https://example.immich.app/application/o/immich/.well-known/openid-configuration` |
| Client ID | AFCj2rM1f4rps**\*\*\*\***\***\*\*\*\***lCLEum6hH9... |
| Client Secret | 0v89FXkQOWO\***\*\*\*\*\***\*\*\***\*\*\*\*\***mprbvXD549HH6s1iw... |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Authentik (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled (optional) |
| Mobile Redirect URI Override | Disable |
| Mobile Redirect URI | |
</details>
<details>
<summary>Google Example</summary>
### Google Example
Here's an example of OAuth configured for Google:
Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-redirect-uris-example.webp').default} width='50%' title="Google authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
</details>
[oidc]: https://openid.net/connect/ [oidc]: https://openid.net/connect/

View File

@@ -77,6 +77,7 @@ immich-admin list-users
deletedAt: null, deletedAt: null,
updatedAt: 2023-09-21T15:42:28.129Z, updatedAt: 2023-09-21T15:42:28.129Z,
oauthId: '', oauthId: '',
memoriesEnabled: true
} }
] ]
``` ```

View File

@@ -80,7 +80,7 @@ The Immich Microservices image uses the same `Dockerfile` as the Immich Server,
- Background jobs (file deletion, user deletion) - Background jobs (file deletion, user deletion)
:::info :::info
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities. This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs.md) page, which provides some remote queue management capabilities.
::: :::
### Machine Learning ### Machine Learning

View File

@@ -44,16 +44,16 @@ If the import paths are edited in a way that an external file is no longer in an
### Troubleshooting ### Troubleshooting
Sometimes, an external library will not scan correctly. This can happen if Immich can't access the files. Here are some things to check: Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
- In the docker-compose file, are the volumes mounted correctly? - In the docker-compose file, are the volumes mounted correctly?
- Are the volumes also mounted to any worker containers? - Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file? - Are the import paths set correctly, and do they match the path set in docker-compose file?
- Make sure you don't use symlinks in your import libraries, and that you aren't linking across docker mounts. - Make sure you don't use symlinks in your import libraries, and that you aren't linking across docker mounts.
- Are the permissions set correctly? - Are the permissions set correctly?
- Make sure you are using forward slashes (`/`) and not backward slashes. - Make sure you are using forward slashes (`/`) and not backward slashes.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the same in any microservices containers. To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
### Exclusion Patterns ### Exclusion Patterns
@@ -98,7 +98,7 @@ First, we need to plan how we want to organize the libraries. The christmas trip
### Mount Docker Volumes ### Mount Docker Volumes
The `immich-server` container will need access to the gallery. Modify your docker compose file as follows `immich-server` and `immich-microservices` containers will need access to the gallery. Modify your docker compose file as follows
```diff title="docker-compose.yml" ```diff title="docker-compose.yml"
immich-server: immich-server:
@@ -107,6 +107,15 @@ The `immich-server` container will need access to the gallery. Modify your docke
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro + - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro + - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro + - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system. + - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
``` ```

View File

@@ -9,7 +9,7 @@ It is important to remember to update the backup settings after following the gu
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
```diff title=".env" ```diff title=".env"
# You can find documentation for all the supported env variables [here](/docs/install/environment-variables) # You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# Custom location where your uploaded, thumbnails, and transcoded video files are stored # Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library - UPLOAD_LOCATION=./library

View File

@@ -11,8 +11,9 @@ docker ps -a # see a list of running and stopped containers
```bash ```bash
docker exec -it <id or name> <command> # attach to a container with a command docker exec -it <id or name> <command> # attach to a container with a command
docker exec -it immich_server bash docker exec -it immich_server sh
docker exec -it immich_machine_learning bash docker exec -it immich_microservices sh
docker exec -it immich_machine_learning sh
``` ```
## Logs ## Logs
@@ -21,6 +22,7 @@ docker exec -it immich_machine_learning bash
docker logs <id or name> # see the logs for a specific container (by id or name) docker logs <id or name> # see the logs for a specific container (by id or name)
docker logs immich_server docker logs immich_server
docker logs immich_microservices
docker logs immich_machine_learning docker logs immich_machine_learning
``` ```

View File

@@ -6,14 +6,20 @@ in a directory on the same machine.
# Mount the directory into the containers. # Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points in the section `immich-server:` under `volumes:` Edit `docker-compose.yml` to add two new mount points in the sections `immich-server:` and `immich-microservices:` under `volumes:`
```diff ```diff
immich-server: immich-server:
volumes: volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external + - ${EXTERNAL_PATH}:/usr/src/app/external
immich-microservices:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
``` ```
Be sure to add exactly the same path to both services.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer: Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
``` ```

View File

@@ -32,7 +32,7 @@ def upload(file):
} }
response = requests.post( response = requests.post(
f'{BASE_URL}/assets', headers=headers, data=data, files=files) f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
print(response.json()) print(response.json())
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False} # {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}

View File

@@ -7,7 +7,7 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Start the container by running `docker compose up -d`. - Start the container by running `docker compose up -d`.
:::info :::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning container, but facial recognition is done in the `microservices` worker. Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
::: :::
:::note :::note
@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
::: :::
```yaml ```yaml
name: immich_remote_ml version: '3.8'
services: services:
immich-machine-learning: immich-machine-learning:

View File

@@ -157,6 +157,9 @@ The default configuration looks like this:
"server": { "server": {
"externalDomain": "", "externalDomain": "",
"loginPageMessage": "" "loginPageMessage": ""
},
"user": {
"deleteDelay": 7
} }
} }
``` ```

View File

@@ -17,11 +17,11 @@ If this should not work, try running `docker compose up -d --force-recreate`.
## Docker Compose ## Docker Compose
| Variable | Description | Default | Containers | | Variable | Description | Default | Services |
| :----------------- | :------------------------------ | :-------: | :----------------------- | | :----------------- | :------------------------------ | :-------: | :-------------------------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning | | `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning |
| `UPLOAD_LOCATION` | Host Path for uploads | | server | | `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database | | `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
:::tip :::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
@@ -38,16 +38,15 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General ## General
| Variable | Description | Default | Containers | Workers | | Variable | Description | Default | Services |
| :------------------------------ | :---------------------------------------------- | :----------------------: | :----------------------- | :----------------- | | :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
| `TZ` | Timezone | | server | microservices | | `TZ` | Timezone | | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api | | `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices | | `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. \*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing. It only need to be set if the Immich deployment method is changing.
@@ -55,39 +54,28 @@ It only need to be set if the Immich deployment method is changing.
:::tip :::tip
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. `TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
:::
## Workers
| Variable | Description | Default | Containers |
| :----------------------- | :--------------------------------------------------------------------------------------------------- | :-----: | :--------- |
| `IMMICH_WORKERS_INCLUDE` | Only run these workers. | | server |
| `IMMICH_WORKERS_EXCLUDE` | Do not run these workers. Matches against default workers, or `IMMICH_WORKERS_INCLUDE` if specified. | | server |
:::info
Information on the current workers can be found [here](/docs/administration/jobs-workers).
::: :::
## Ports ## Ports
| Variable | Description | Default | | Variable | Description | Default |
| :------------ | :------------- | :----------------------------------------: | | :------------ | :------------- | :------------------------------------: |
| `IMMICH_HOST` | Listening host | `0.0.0.0` | | `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `IMMICH_PORT` | Listening port | `3001` (server), `3003` (machine learning) | | `IMMICH_PORT` | Listening port | 3001 (server), 3003 (machine learning) |
## Database ## Database
| Variable | Description | Default | Containers | | Variable | Description | Default | Services |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- | | :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :-------------------------------------------- |
| `DB_URL` | Database URL | | server | | `DB_URL` | Database URL | | server, microservices |
| `DB_HOSTNAME` | Database Host | `database` | server | | `DB_HOSTNAME` | Database Host | `database` | server, microservices |
| `DB_PORT` | Database Port | `5432` | server | | `DB_PORT` | Database Port | `5432` | server, microservices |
| `DB_USERNAME` | Database User | `postgres` | server, database<sup>\*1</sup> | | `DB_USERNAME` | Database User | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database Password | `postgres` | server, database<sup>\*1</sup> | | `DB_PASSWORD` | Database Password | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, database<sup>\*1</sup> | | `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server | | `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | | `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server, microservices |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`. \*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
@@ -95,34 +83,30 @@ Information on the current workers can be found [here](/docs/administration/jobs
:::info :::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
`DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`.
You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`.
When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored.
::: :::
## Redis ## Redis
| Variable | Description | Default | Containers | | Variable | Description | Default | Services |
| :--------------- | :------------- | :-----: | :--------- | | :--------------- | :------------- | :-----: | :-------------------- |
| `REDIS_URL` | Redis URL | | server | | `REDIS_URL` | Redis URL | | server, microservices |
| `REDIS_SOCKET` | Redis Socket | | server | | `REDIS_HOSTNAME` | Redis Host | `redis` | server, microservices |
| `REDIS_HOSTNAME` | Redis Host | `redis` | server | | `REDIS_PORT` | Redis Port | `6379` | server, microservices |
| `REDIS_PORT` | Redis Port | `6379` | server | | `REDIS_DBINDEX` | Redis DB Index | `0` | server, microservices |
| `REDIS_USERNAME` | Redis Username | | server | | `REDIS_USERNAME` | Redis Username | | server, microservices |
| `REDIS_PASSWORD` | Redis Password | | server | | `REDIS_PASSWORD` | Redis Password | | server, microservices |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server | | `REDIS_SOCKET` | Redis Socket | | server, microservices |
:::info :::info
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration. `REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis][redis-api] documentation. More info can be found in the upstream [ioredis][redis-api] documentation.
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored. - When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
::: :::
Redis (Sentinel) URL example JSON before encoding: Redis (Sentinel) URL example JSON before encoding:
@@ -154,7 +138,7 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning ## Machine Learning
| Variable | Description | Default | Containers | | Variable | Description | Default | Services |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- | | :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | | `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | | `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
@@ -179,13 +163,13 @@ Other machine learning parameters can be tuned from the admin UI.
## Prometheus ## Prometheus
| Variable | Description | Default | Containers | Workers | | Variable | Description | Default | Services |
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- | | :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- |
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server | api, microservices | | `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server | api, microservices | | `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server | api, microservices | | `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server | api, microservices | | `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server | api, microservices | | `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices |
\*1: Overridden for a metric group when its corresponding environmental variable is set. \*1: Overridden for a metric group when its corresponding environmental variable is set.

View File

@@ -100,9 +100,9 @@ const config = {
label: 'Docs', label: 'Docs',
}, },
{ {
to: '/roadmap', to: '/milestones',
position: 'right', position: 'right',
label: 'Roadmap', label: 'Milestones',
}, },
{ {
to: '/docs/api', to: '/docs/api',

View File

@@ -56,6 +56,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "20.14.0" "node": "20.13.1"
} }
} }

View File

@@ -1,23 +1,28 @@
import useIsBrowser from '@docusaurus/useIsBrowser';
import { mdiCheckboxBlankCircle, mdiCheckboxMarkedCircle } from '@mdi/js';
import Icon from '@mdi/react';
import React from 'react'; import React from 'react';
import Icon from '@mdi/react';
import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
import useIsBrowser from '@docusaurus/useIsBrowser';
export type Item = { export interface Item {
icon: string; icon: string;
iconColor: string;
title: string; title: string;
description?: string; description?: string;
link?: { url: string; text: string }; release?: string;
done?: false; tag?: string;
getDateLabel: (language: string) => string; date: Date;
}; dateType: DateType;
}
export enum DateType {
RELEASE = 'Release Date',
DATE = 'Date',
}
interface Props { interface Props {
items: Item[]; items: Item[];
} }
export function Timeline({ items }: Props): JSX.Element { export default function Timeline({ items }: Props): JSX.Element {
const isBrowser = useIsBrowser(); const isBrowser = useIsBrowser();
return ( return (
@@ -25,15 +30,21 @@ export function Timeline({ items }: Props): JSX.Element {
{items.map((item, index) => { {items.map((item, index) => {
const isFirst = index === 0; const isFirst = index === 0;
const isLast = index === items.length - 1; const isLast = index === items.length - 1;
const done = item.done ?? true;
const dateLabel = item.getDateLabel(isBrowser ? navigator.language : 'en-US'); const classNames: string[] = [];
const timelineIcon = done ? mdiCheckboxMarkedCircle : mdiCheckboxBlankCircle;
const cardIcon = item.icon; if (isFirst) {
classNames.push('');
}
if (isLast) {
classNames.push('rounded rounded-b-full');
}
return ( return (
<li key={index} className={`flex min-h-24 w-[700px] max-w-[90vw] ${done ? '' : 'italic'}`}> <li key={index} className="flex min-h-24 w-[700px] max-w-[90vw]">
<div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden"> <div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
{dateLabel} {isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
</div> </div>
<div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}> <div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
<div <div
@@ -43,32 +54,33 @@ export function Timeline({ items }: Props): JSX.Element {
></div> ></div>
</div> </div>
<div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg "> <div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
{<Icon path={timelineIcon} size={1.25} />} <Icon path={mdiCheckboxMarkedCircleOutline} size={1.25} />
</div> </div>
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-row w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all"> <section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-col w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="flex flex-col flex-grow justify-between gap-2"> <div className="m-0 text-lg flex w-full items-center justify-between gap-2">
<div className="flex gap-2 items-center"> <p className="m-0 items-start flex gap-2">
{cardIcon === 'immich' ? ( <Icon path={item.icon} size={1} />
<img src="img/immich-logo.svg" height="30" /> <span>{item.title}</span>
) : ( </p>
<Icon path={cardIcon} size={1} color={item.iconColor} />
)}
<p className="m-0 mt-1 text-lg items-start flex gap-2 place-items-center content-center">
<span>{item.title}</span>
</p>
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</div>
<div className="flex flex-col justify-between place-items-end">
<span className="dark:text-immich-dark-primary text-immich-primary"> <span className="dark:text-immich-dark-primary text-immich-primary">
{item.link && ( {item.tag ? (
<a href={item.link.url} target="_blank" rel="noopener"> <a
[{item.link.text}] href={`https://github.com/immich-app/immich/releases/tag/${item.tag}`}
target="_blank"
rel="noopener"
>
[{item.release ?? item.tag}]{' '}
</a> </a>
) : (
item.release && <span>[{item.release}]</span>
)} )}
</span> </span>
<div className="md:hidden text-sm text-right">{dateLabel}</div>
</div> </div>
<div className="md:hidden text-xs">
{`${item.dateType} - ${isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}`}
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</section> </section>
</li> </li>
); );

View File

@@ -15,10 +15,6 @@ button {
font-family: 'Overpass', sans-serif; font-family: 'Overpass', sans-serif;
} }
img {
border-radius: 15px;
}
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
--ifm-color-primary: #4250af; --ifm-color-primary: #4250af;
@@ -46,7 +42,7 @@ img {
} }
div[class^='announcementBar_'] { div[class^='announcementBar_'] {
min-height: 2rem; height: 2rem;
} }
.navbar__brand .navbar__title { .navbar__brand .navbar__title {

File diff suppressed because it is too large Load Diff

View File

@@ -28,4 +28,3 @@
/docs/features/search /docs/features/smart-search 301 /docs/features/search /docs/features/smart-search 301
/docs/guides/api-album-sync /docs/community-projects 301 /docs/guides/api-album-sync /docs/community-projects 301
/docs/guides/remove-offline-files /docs/community-projects 301 /docs/guides/remove-offline-files /docs/community-projects 301
/milestones /roadmap 301

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
20.14 20.13

View File

@@ -27,7 +27,7 @@ services:
- 2283:3001 - 2283:3001
redis: redis:
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

163
e2e/package-lock.json generated
View File

@@ -11,7 +11,7 @@
"devDependencies": { "devDependencies": {
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
@@ -88,7 +88,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.13", "@types/node": "^20.12.12",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -971,12 +971,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.44.1", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.44.1" "playwright": "1.44.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -1230,9 +1230,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.13", "version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1344,17 +1344,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.11.0", "@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.11.0", "@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1378,16 +1378,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1407,14 +1407,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0" "@typescript-eslint/visitor-keys": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1425,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.11.0", "@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1453,9 +1453,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1467,14 +1467,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1522,16 +1522,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.11.0" "@typescript-eslint/typescript-estree": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1545,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -1831,13 +1831,13 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.0.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2700,11 +2700,10 @@
} }
}, },
"node_modules/exiftool-vendored": { "node_modules/exiftool-vendored": {
"version": "26.1.0", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz",
"integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==", "integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@photostructure/tz-lookup": "^10.0.0", "@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
@@ -2713,27 +2712,25 @@
"luxon": "^3.4.4" "luxon": "^3.4.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"exiftool-vendored.exe": "12.85.0", "exiftool-vendored.exe": "12.84.0",
"exiftool-vendored.pl": "12.85.0" "exiftool-vendored.pl": "12.84.0"
} }
}, },
"node_modules/exiftool-vendored.exe": { "node_modules/exiftool-vendored.exe": {
"version": "12.85.0", "version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz",
"integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==", "integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==",
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/exiftool-vendored.pl": { "node_modules/exiftool-vendored.pl": {
"version": "12.85.0", "version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz",
"integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==", "integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==",
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"!win32" "!win32"
@@ -2821,9 +2818,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3633,13 +3630,13 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.7", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
@@ -4255,12 +4252,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.44.1", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright-core": "1.44.1" "playwright-core": "1.44.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -4273,9 +4270,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.44.1", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"

View File

@@ -21,7 +21,7 @@
"devDependencies": { "devDependencies": {
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0" "vitest": "^1.3.0"
}, },
"volta": { "volta": {
"node": "20.14.0" "node": "20.13.1"
} }
} }

View File

@@ -2,7 +2,7 @@ import {
ActivityCreateDto, ActivityCreateDto,
AlbumResponseDto, AlbumResponseDto,
AlbumUserRole, AlbumUserRole,
AssetMediaResponseDto, AssetFileUploadResponseDto,
LoginResponseDto, LoginResponseDto,
ReactionType, ReactionType,
createActivity as create, createActivity as create,
@@ -14,10 +14,10 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activities', () => { describe('/activity', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let nonOwner: LoginResponseDto; let nonOwner: LoginResponseDto;
let asset: AssetMediaResponseDto; let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) => const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
@@ -45,24 +45,22 @@ describe('/activities', () => {
await utils.resetDatabase(['activity']); await utils.resetDatabase(['activity']);
}); });
describe('GET /activities', () => { describe('GET /activity', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/activities'); const { status, body } = await request(app).get('/activity');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require an albumId', async () => { it('should require an albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
}); });
it('should reject an invalid albumId', async () => { it('should reject an invalid albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: uuidDto.invalid }) .query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -71,7 +69,7 @@ describe('/activities', () => {
it('should reject an invalid assetId', async () => { it('should reject an invalid assetId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -80,7 +78,7 @@ describe('/activities', () => {
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id }) .query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]); expect(body).toEqual([]);
@@ -104,7 +102,7 @@ describe('/activities', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id }) .query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -123,7 +121,7 @@ describe('/activities', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id, type: 'comment' }) .query({ albumId: album.id, type: 'comment' })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -142,7 +140,7 @@ describe('/activities', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id, type: 'like' }) .query({ albumId: album.id, type: 'like' })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -154,7 +152,7 @@ describe('/activities', () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like }); const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const response1 = await request(app) const response1 = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id, userId: uuidDto.notFound }) .query({ albumId: album.id, userId: uuidDto.notFound })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -162,7 +160,7 @@ describe('/activities', () => {
expect(response1.body.length).toBe(0); expect(response1.body.length).toBe(0);
const response2 = await request(app) const response2 = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id, userId: admin.userId }) .query({ albumId: album.id, userId: admin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -182,7 +180,7 @@ describe('/activities', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activity')
.query({ albumId: album.id, assetId: asset.id }) .query({ albumId: album.id, assetId: asset.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -191,16 +189,16 @@ describe('/activities', () => {
}); });
}); });
describe('POST /activities', () => { describe('POST /activity', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/activities'); const { status, body } = await request(app).post('/activity');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require an albumId', async () => { it('should require an albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid }); .send({ albumId: uuidDto.invalid });
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -209,7 +207,7 @@ describe('/activities', () => {
it('should require a comment when type is comment', async () => { it('should require a comment when type is comment', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); .send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -218,7 +216,7 @@ describe('/activities', () => {
it('should add a comment to an album', async () => { it('should add a comment to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
albumId: album.id, albumId: album.id,
@@ -238,7 +236,7 @@ describe('/activities', () => {
it('should add a like to an album', async () => { it('should add a like to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -255,7 +253,7 @@ describe('/activities', () => {
it('should return a 200 for a duplicate like on the album', async () => { it('should return a 200 for a duplicate like on the album', async () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like }); const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -269,7 +267,7 @@ describe('/activities', () => {
type: ReactionType.Like, type: ReactionType.Like,
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -278,7 +276,7 @@ describe('/activities', () => {
it('should add a comment to an asset', async () => { it('should add a comment to an asset', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
albumId: album.id, albumId: album.id,
@@ -299,7 +297,7 @@ describe('/activities', () => {
it('should add a like to an asset', async () => { it('should add a like to an asset', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' }); .send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -321,7 +319,7 @@ describe('/activities', () => {
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' }); .send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -329,16 +327,16 @@ describe('/activities', () => {
}); });
}); });
describe('DELETE /activities/:id', () => { describe('DELETE /activity/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`); const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/activities/${uuidDto.invalid}`) .delete(`/activity/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -351,7 +349,7 @@ describe('/activities', () => {
comment: 'This is a test comment', comment: 'This is a test comment',
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activities/${reaction.id}`) .delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
}); });
@@ -362,7 +360,7 @@ describe('/activities', () => {
type: ReactionType.Like, type: ReactionType.Like,
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activities/${reaction.id}`) .delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
}); });
@@ -375,7 +373,7 @@ describe('/activities', () => {
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activities/${reaction.id}`) .delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
@@ -389,7 +387,7 @@ describe('/activities', () => {
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/activities/${reaction.id}`) .delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`); .set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -407,7 +405,7 @@ describe('/activities', () => {
); );
const { status } = await request(app) const { status } = await request(app)
.delete(`/activities/${reaction.id}`) .delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`); .set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);

View File

@@ -2,9 +2,9 @@ import {
addAssetsToAlbum, addAssetsToAlbum,
AlbumResponseDto, AlbumResponseDto,
AlbumUserRole, AlbumUserRole,
AssetMediaResponseDto, AssetFileUploadResponseDto,
AssetOrder, AssetOrder,
deleteUserAdmin, deleteUser,
getAlbumInfo, getAlbumInfo,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
@@ -23,11 +23,11 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink'; const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared'; const user2NotShared = 'user2NotShared';
describe('/albums', () => { describe('/album', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user1Asset1: AssetMediaResponseDto; let user1Asset1: AssetFileUploadResponseDto;
let user1Asset2: AssetMediaResponseDto; let user1Asset2: AssetFileUploadResponseDto;
let user1Albums: AlbumResponseDto[]; let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto; let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[]; let user2Albums: AlbumResponseDto[];
@@ -107,19 +107,19 @@ describe('/albums', () => {
}), }),
]); ]);
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
}); });
describe('GET /albums', () => { describe('GET /album', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums'); const { status, body } = await request(app).get('/album');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should reject an invalid shared param', async () => { it('should reject an invalid shared param', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=invalid') .get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value'])); expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
@@ -127,7 +127,7 @@ describe('/albums', () => {
it('should reject an invalid assetId param', async () => { it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?assetId=invalid') .get('/album?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID'])); expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
@@ -135,7 +135,7 @@ describe('/albums', () => {
it("should not show other users' favorites", async () => { it("should not show other users' favorites", async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toEqual({ expect(body).toEqual({
@@ -146,7 +146,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => { it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=true') .get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -178,7 +178,7 @@ describe('/albums', () => {
}); });
it('should return the album collection including owned and shared', async () => { it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`); const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
@@ -209,7 +209,7 @@ describe('/albums', () => {
it('should return the album collection filtered by shared', async () => { it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=true') .get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
@@ -241,7 +241,7 @@ describe('/albums', () => {
it('should return the album collection filtered by NOT shared', async () => { it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums?shared=false') .get('/album?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
@@ -258,7 +258,7 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId', async () => { it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`) .get(`/album?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
@@ -266,7 +266,7 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId and ignores shared=true', async () => { it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?shared=true&assetId=${user1Asset1.id}`) .get(`/album?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
@@ -274,23 +274,23 @@ describe('/albums', () => {
it('should return the album collection filtered by assetId and ignores shared=false', async () => { it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums?shared=false&assetId=${user1Asset1.id}`) .get(`/album?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
}); });
}); });
describe('GET /albums/:id', () => { describe('GET /album/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`); const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return album info for own album', async () => { it('should return album info for own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -302,7 +302,7 @@ describe('/albums', () => {
it('should return album info for shared album (editor)', async () => { it('should return album info for shared album (editor)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user2Albums[0].id}?withoutAssets=false`) .get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -311,7 +311,7 @@ describe('/albums', () => {
it('should return album info for shared album (viewer)', async () => { it('should return album info for shared album (viewer)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[3].id}?withoutAssets=false`) .get(`/album/${user1Albums[3].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -320,7 +320,7 @@ describe('/albums', () => {
it('should return album info with assets when withoutAssets is undefined', async () => { it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}`) .get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -332,7 +332,7 @@ describe('/albums', () => {
it('should return album info without assets when withoutAssets is true', async () => { it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=true`) .get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -344,16 +344,16 @@ describe('/albums', () => {
}); });
}); });
describe('GET /albums/count', () => { describe('GET /album/count', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/count'); const { status, body } = await request(app).get('/album/count');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return total count of albums the user has access to', async () => { it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums/count') .get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -361,16 +361,16 @@ describe('/albums', () => {
}); });
}); });
describe('POST /albums', () => { describe('POST /album', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' }); const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should create an album', async () => { it('should create an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/albums') .post('/album')
.send({ albumName: 'New album' }) .send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201); expect(status).toBe(201);
@@ -383,6 +383,7 @@ describe('/albums', () => {
description: '', description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
shared: false, shared: false,
sharedUsers: [],
albumUsers: [], albumUsers: [],
hasSharedLink: false, hasSharedLink: false,
assets: [], assets: [],
@@ -394,9 +395,9 @@ describe('/albums', () => {
}); });
}); });
describe('PUT /albums/:id/assets', () => { describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`); const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -404,7 +405,7 @@ describe('/albums', () => {
it('should be able to add own asset to own album', async () => { it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`) .put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
@@ -415,7 +416,7 @@ describe('/albums', () => {
it('should be able to add own asset to shared album', async () => { it('should be able to add own asset to shared album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${user2Albums[0].id}/assets`) .put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
@@ -426,7 +427,7 @@ describe('/albums', () => {
it('should not be able to add assets to album as a viewer', async () => { it('should not be able to add assets to album as a viewer', async () => {
const asset = await utils.createAsset(user2.accessToken); const asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${user1Albums[3].id}/assets`) .put(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
@@ -437,7 +438,7 @@ describe('/albums', () => {
it('should add duplicate assets only once', async () => { it('should add duplicate assets only once', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`) .put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id, asset.id] }); .send({ ids: [asset.id, asset.id] });
@@ -449,10 +450,10 @@ describe('/albums', () => {
}); });
}); });
describe('PATCH /albums/:id', () => { describe('PATCH /album/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/albums/${uuidDto.notFound}`) .patch(`/album/${uuidDto.notFound}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -463,7 +464,7 @@ describe('/albums', () => {
albumName: 'New album', albumName: 'New album',
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/albums/${album.id}`) .patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ .send({
albumName: 'New album name', albumName: 'New album name',
@@ -480,7 +481,7 @@ describe('/albums', () => {
it('should not be able to update as a viewer', async () => { it('should not be able to update as a viewer', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/albums/${user1Albums[3].id}`) .patch(`/album/${user1Albums[3].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
@@ -490,7 +491,7 @@ describe('/albums', () => {
it('should not be able to update as an editor', async () => { it('should not be able to update as an editor', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/albums/${user1Albums[0].id}`) .patch(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
@@ -499,10 +500,10 @@ describe('/albums', () => {
}); });
}); });
describe('DELETE /albums/:id/assets', () => { describe('DELETE /album/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`) .delete(`/album/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(401); expect(status).toBe(401);
@@ -511,7 +512,7 @@ describe('/albums', () => {
it('should not be able to remove foreign asset from own album', async () => { it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`) .delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -527,7 +528,7 @@ describe('/albums', () => {
it('should not be able to remove foreign asset from foreign album', async () => { it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`) .delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -543,7 +544,7 @@ describe('/albums', () => {
it('should be able to remove own asset from own album', async () => { it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`) .delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -553,7 +554,7 @@ describe('/albums', () => {
it('should be able to remove own asset from shared album', async () => { it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`) .delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -563,7 +564,7 @@ describe('/albums', () => {
it('should not be able to remove assets from album as a viewer', async () => { it('should not be able to remove assets from album as a viewer', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[3].id}/assets`) .delete(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -573,7 +574,7 @@ describe('/albums', () => {
it('should remove duplicate assets only once', async () => { it('should remove duplicate assets only once', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`) .delete(`/album/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id, user1Asset1.id] }); .send({ ids: [user1Asset1.id, user1Asset1.id] });
@@ -595,7 +596,7 @@ describe('/albums', () => {
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] }); const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -603,25 +604,21 @@ describe('/albums', () => {
it('should be able to add user to own album', async () => { it('should be able to add user to own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
albumUsers: [ sharedUsers: [expect.objectContaining({ id: user2.userId })],
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
}), }),
); );
}); });
it('should not be able to share album with owner', async () => { it('should not be able to share album with owner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
@@ -631,12 +628,12 @@ describe('/albums', () => {
it('should not be able to add existing user to shared album', async () => { it('should not be able to add existing user to shared album', async () => {
await request(app) await request(app)
.put(`/albums/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
@@ -655,16 +652,14 @@ describe('/albums', () => {
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app) const { status } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`) .put(`/album/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ role: AlbumUserRole.Editor }); .send({ role: AlbumUserRole.Editor });
expect(status).toBe(200); expect(status).toBe(200);
// Get album to verify the role change // Get album to verify the role change
const { body } = await request(app) const { body } = await request(app).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`);
.get(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })], albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
@@ -681,7 +676,7 @@ describe('/albums', () => {
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`) .put(`/album/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ role: AlbumUserRole.Editor }); .send({ role: AlbumUserRole.Editor });

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from
import { asBearerAuth, utils } from 'src/utils'; import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/audits', () => { describe('/audit', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, tempDir, utils } from 'src/utils'; import { app, tempDir, utils } from 'src/utils';
@@ -7,8 +7,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/download', () => { describe('/download', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetMediaResponseDto; let asset1: AssetFileUploadResponseDto;
let asset2: AssetMediaResponseDto; let asset2: AssetFileUploadResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
@@ -73,4 +73,22 @@ describe('/download', () => {
} }
}); });
}); });
describe('POST /download/asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download file', async () => {
const response = await request(app)
.post(`/download/asset/${asset1.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/png');
});
});
}); });

View File

@@ -11,7 +11,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) => const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) }); scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/libraries', () => { describe('/library', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user: LoginResponseDto; let user: LoginResponseDto;
let library: LibraryResponseDto; let library: LibraryResponseDto;
@@ -37,24 +37,24 @@ describe('/libraries', () => {
utils.resetEvents(); utils.resetEvents();
}); });
describe('GET /libraries', () => { describe('GET /library', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/libraries'); const { status, body } = await request(app).get('/library');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
}); });
describe('POST /libraries', () => { describe('POST /library', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/libraries').send({}); const { status, body } = await request(app).post('/library').send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require admin authentication', async () => { it('should require admin authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/libraries') .post('/library')
.set('Authorization', `Bearer ${user.accessToken}`) .set('Authorization', `Bearer ${user.accessToken}`)
.send({ ownerId: admin.userId }); .send({ ownerId: admin.userId });
@@ -64,7 +64,7 @@ describe('/libraries', () => {
it('should create an external library with defaults', async () => { it('should create an external library with defaults', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/libraries') .post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId }); .send({ ownerId: admin.userId });
@@ -83,7 +83,7 @@ describe('/libraries', () => {
it('should create an external library with options', async () => { it('should create an external library with options', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/libraries') .post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
@@ -103,7 +103,7 @@ describe('/libraries', () => {
it('should not create an external library with duplicate import paths', async () => { it('should not create an external library with duplicate import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/libraries') .post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
@@ -118,7 +118,7 @@ describe('/libraries', () => {
it('should not create an external library with duplicate exclusion patterns', async () => { it('should not create an external library with duplicate exclusion patterns', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/libraries') .post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
@@ -132,16 +132,16 @@ describe('/libraries', () => {
}); });
}); });
describe('PUT /libraries/:id', () => { describe('PUT /library/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/libraries/${uuidDto.notFound}`).send({}); const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should change the library name', async () => { it('should change the library name', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'New Library Name' }); .send({ name: 'New Library Name' });
@@ -155,7 +155,7 @@ describe('/libraries', () => {
it('should not set an empty name', async () => { it('should not set an empty name', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: '' }); .send({ name: '' });
@@ -165,7 +165,7 @@ describe('/libraries', () => {
it('should change the import paths', async () => { it('should change the import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [testAssetDirInternal] }); .send({ importPaths: [testAssetDirInternal] });
@@ -179,7 +179,7 @@ describe('/libraries', () => {
it('should reject an empty import path', async () => { it('should reject an empty import path', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [''] }); .send({ importPaths: [''] });
@@ -189,7 +189,7 @@ describe('/libraries', () => {
it('should reject duplicate import paths', async () => { it('should reject duplicate import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: ['/path', '/path'] }); .send({ importPaths: ['/path', '/path'] });
@@ -199,7 +199,7 @@ describe('/libraries', () => {
it('should change the exclusion pattern', async () => { it('should change the exclusion pattern', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/Raw/**'] }); .send({ exclusionPatterns: ['**/Raw/**'] });
@@ -213,7 +213,7 @@ describe('/libraries', () => {
it('should reject duplicate exclusion patterns', async () => { it('should reject duplicate exclusion patterns', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
@@ -223,7 +223,7 @@ describe('/libraries', () => {
it('should reject an empty exclusion pattern', async () => { it('should reject an empty exclusion pattern', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/libraries/${library.id}`) .put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: [''] }); .send({ exclusionPatterns: [''] });
@@ -232,9 +232,9 @@ describe('/libraries', () => {
}); });
}); });
describe('GET /libraries/:id', () => { describe('GET /library/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}`); const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -242,7 +242,7 @@ describe('/libraries', () => {
it('should require admin access', async () => { it('should require admin access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/libraries/${uuidDto.notFound}`) .get(`/library/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user.accessToken}`); .set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(403); expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
@@ -252,7 +252,7 @@ describe('/libraries', () => {
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId }); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/libraries/${library.id}`) .get(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -269,18 +269,18 @@ describe('/libraries', () => {
}); });
}); });
describe('GET /libraries/:id/statistics', () => { describe('GET /library/:id/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}/statistics`); const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
}); });
describe('POST /libraries/:id/scan', () => { describe('POST /library/:id/scan', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/scan`).send({}); const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -496,9 +496,9 @@ describe('/libraries', () => {
}); });
}); });
describe('POST /libraries/:id/removeOffline', () => { describe('POST /library/:id/removeOffline', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({}); const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -532,7 +532,7 @@ describe('/libraries', () => {
expect(offlineAssets.count).toBe(1); expect(offlineAssets.count).toBe(1);
const { status } = await request(app) const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`) .post(`/library/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(); .send();
expect(status).toBe(204); expect(status).toBe(204);
@@ -557,7 +557,7 @@ describe('/libraries', () => {
expect(assetsBefore.count).toBeGreaterThan(1); expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app) const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`) .post(`/library/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(); .send();
expect(status).toBe(204); expect(status).toBe(204);
@@ -569,9 +569,9 @@ describe('/libraries', () => {
}); });
}); });
describe('POST /libraries/:id/validate', () => { describe('POST /library/:id/validate', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/validate`).send({}); const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -617,9 +617,9 @@ describe('/libraries', () => {
}); });
}); });
describe('DELETE /libraries/:id', () => { describe('DELETE /library/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/libraries/${uuidDto.notFound}`); const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -629,7 +629,7 @@ describe('/libraries', () => {
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId }); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/libraries/${library.id}`) .delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
@@ -655,7 +655,7 @@ describe('/libraries', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/libraries/${library.id}`) .delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);

View File

@@ -1,162 +0,0 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/map', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let asset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
websocket = await utils.connectWebsocket(admin.accessToken);
asset = await utils.createAsset(admin.accessToken);
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
utils.resetEvents();
const uploadFile = async (input: string) => {
const filepath = join(testAssetDir, input);
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
};
await Promise.all(files.map((f) => uploadFile(f)));
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /map/markers', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/markers');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/map/markers')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/map/markers')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('GET /map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should allow shared link access', async () => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should throw an error if a theme is not light or dark', async () => {
for (const theme of ['dark1', true, 123, '', null, undefined]) {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});
it('should return the light style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'light' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
});
it('should return the dark style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should not require admin authentication', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
});
});

View File

@@ -1,5 +1,5 @@
import { import {
AssetMediaResponseDto, AssetFileUploadResponseDto,
LoginResponseDto, LoginResponseDto,
MemoryResponseDto, MemoryResponseDto,
MemoryType, MemoryType,
@@ -15,9 +15,9 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/memories', () => { describe('/memories', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user: LoginResponseDto; let user: LoginResponseDto;
let adminAsset: AssetMediaResponseDto; let adminAsset: AssetFileUploadResponseDto;
let userAsset1: AssetMediaResponseDto; let userAsset1: AssetFileUploadResponseDto;
let userAsset2: AssetMediaResponseDto; let userAsset2: AssetFileUploadResponseDto;
let userMemory: MemoryResponseDto; let userMemory: MemoryResponseDto;
beforeAll(async () => { beforeAll(async () => {

View File

@@ -5,7 +5,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/partners', () => { describe('/partner', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user2: LoginResponseDto; let user2: LoginResponseDto;
@@ -28,9 +28,9 @@ describe('/partners', () => {
]); ]);
}); });
describe('GET /partners', () => { describe('GET /partner', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/partners'); const { status, body } = await request(app).get('/partner');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -38,7 +38,7 @@ describe('/partners', () => {
it('should get all partners shared by user', async () => { it('should get all partners shared by user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/partners') .get('/partner')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-by' }); .query({ direction: 'shared-by' });
@@ -48,7 +48,7 @@ describe('/partners', () => {
it('should get all partners that share with user', async () => { it('should get all partners that share with user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/partners') .get('/partner')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-with' }); .query({ direction: 'shared-with' });
@@ -57,9 +57,9 @@ describe('/partners', () => {
}); });
}); });
describe('POST /partners/:id', () => { describe('POST /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/partners/${user3.userId}`); const { status, body } = await request(app).post(`/partner/${user3.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -67,7 +67,7 @@ describe('/partners', () => {
it('should share with new partner', async () => { it('should share with new partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/partners/${user3.userId}`) .post(`/partner/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201); expect(status).toBe(201);
@@ -76,7 +76,7 @@ describe('/partners', () => {
it('should not share with new partner if already sharing with this partner', async () => { it('should not share with new partner if already sharing with this partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/partners/${user2.userId}`) .post(`/partner/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -84,9 +84,9 @@ describe('/partners', () => {
}); });
}); });
describe('PUT /partners/:id', () => { describe('PUT /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/partners/${user2.userId}`); const { status, body } = await request(app).put(`/partner/${user2.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -94,7 +94,7 @@ describe('/partners', () => {
it('should update partner', async () => { it('should update partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/partners/${user2.userId}`) .put(`/partner/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ inTimeline: false }); .send({ inTimeline: false });
@@ -103,9 +103,9 @@ describe('/partners', () => {
}); });
}); });
describe('DELETE /partners/:id', () => { describe('DELETE /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/partners/${user3.userId}`); const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -113,7 +113,7 @@ describe('/partners', () => {
it('should delete partner', async () => { it('should delete partner', async () => {
const { status } = await request(app) const { status } = await request(app)
.delete(`/partners/${user3.userId}`) .delete(`/partner/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -121,7 +121,7 @@ describe('/partners', () => {
it('should throw a bad request if partner not found', async () => { it('should throw a bad request if partner not found', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/partners/${user3.userId}`) .delete(`/partner/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);

View File

@@ -12,7 +12,7 @@ const invalidBirthday = [
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] }, { birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
]; ];
describe('/people', () => { describe('/person', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto; let visiblePerson: PersonResponseDto;
let hiddenPerson: PersonResponseDto; let hiddenPerson: PersonResponseDto;
@@ -47,11 +47,11 @@ describe('/people', () => {
]); ]);
}); });
describe('GET /people', () => { describe('GET /person', () => {
beforeEach(async () => {}); beforeEach(async () => {});
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/people'); const { status, body } = await request(app).get('/person');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -59,7 +59,7 @@ describe('/people', () => {
it('should return all people (including hidden)', async () => { it('should return all people (including hidden)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/people') .get('/person')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true }); .query({ withHidden: true });
@@ -76,7 +76,7 @@ describe('/people', () => {
}); });
it('should return only visible people', async () => { it('should return only visible people', async () => {
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
@@ -90,9 +90,9 @@ describe('/people', () => {
}); });
}); });
describe('GET /people/:id', () => { describe('GET /person/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`); const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -100,7 +100,7 @@ describe('/people', () => {
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${uuidDto.notFound}`) .get(`/person/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -109,7 +109,7 @@ describe('/people', () => {
it('should return person information', async () => { it('should return person information', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${visiblePerson.id}`) .get(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -117,9 +117,9 @@ describe('/people', () => {
}); });
}); });
describe('GET /people/:id/statistics', () => { describe('GET /person/:id/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`); const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -127,7 +127,7 @@ describe('/people', () => {
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${uuidDto.notFound}/statistics`) .get(`/person/${uuidDto.notFound}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -136,7 +136,7 @@ describe('/people', () => {
it('should return the correct number of assets', async () => { it('should return the correct number of assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${multipleAssetsPerson.id}/statistics`) .get(`/person/${multipleAssetsPerson.id}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -144,9 +144,9 @@ describe('/people', () => {
}); });
}); });
describe('POST /people', () => { describe('POST /person', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/people`); const { status, body } = await request(app).post(`/person`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -154,7 +154,7 @@ describe('/people', () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => { it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/people`) .post(`/person`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
@@ -164,7 +164,7 @@ describe('/people', () => {
it('should create a person', async () => { it('should create a person', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/people`) .post(`/person`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
name: 'New Person', name: 'New Person',
@@ -179,9 +179,9 @@ describe('/people', () => {
}); });
}); });
describe('PUT /people/:id', () => { describe('PUT /person/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`); const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -193,7 +193,7 @@ describe('/people', () => {
]) { ]) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`) .put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null }); .send({ [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
@@ -204,7 +204,7 @@ describe('/people', () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => { it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`) .put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
@@ -214,7 +214,7 @@ describe('/people', () => {
it('should update a date of birth', async () => { it('should update a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`) .put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' }); .send({ birthDate: '1990-01-01T05:00:00.000Z' });
expect(status).toBe(200); expect(status).toBe(200);
@@ -223,7 +223,7 @@ describe('/people', () => {
it('should clear a date of birth', async () => { it('should clear a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`) .put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: null }); .send({ birthDate: null });
expect(status).toBe(200); expect(status).toBe(200);

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk'; import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -13,25 +13,25 @@ describe('/search', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let websocket: Socket; let websocket: Socket;
let assetFalcon: AssetMediaResponseDto; let assetFalcon: AssetFileUploadResponseDto;
let assetDenali: AssetMediaResponseDto; let assetDenali: AssetFileUploadResponseDto;
let assetCyclamen: AssetMediaResponseDto; let assetCyclamen: AssetFileUploadResponseDto;
let assetNotocactus: AssetMediaResponseDto; let assetNotocactus: AssetFileUploadResponseDto;
let assetSilver: AssetMediaResponseDto; let assetSilver: AssetFileUploadResponseDto;
let assetDensity: AssetMediaResponseDto; let assetDensity: AssetFileUploadResponseDto;
// let assetPhiladelphia: AssetMediaResponseDto; // let assetPhiladelphia: AssetFileUploadResponseDto;
// let assetOrychophragmus: AssetMediaResponseDto; // let assetOrychophragmus: AssetFileUploadResponseDto;
// let assetRidge: AssetMediaResponseDto; // let assetRidge: AssetFileUploadResponseDto;
// let assetPolemonium: AssetMediaResponseDto; // let assetPolemonium: AssetFileUploadResponseDto;
// let assetWood: AssetMediaResponseDto; // let assetWood: AssetFileUploadResponseDto;
// let assetGlarus: AssetMediaResponseDto; // let assetGlarus: AssetFileUploadResponseDto;
let assetHeic: AssetMediaResponseDto; let assetHeic: AssetFileUploadResponseDto;
let assetRocks: AssetMediaResponseDto; let assetRocks: AssetFileUploadResponseDto;
let assetOneJpg6: AssetMediaResponseDto; let assetOneJpg6: AssetFileUploadResponseDto;
let assetOneHeic6: AssetMediaResponseDto; let assetOneHeic6: AssetFileUploadResponseDto;
let assetOneJpg5: AssetMediaResponseDto; let assetOneJpg5: AssetFileUploadResponseDto;
let assetSprings: AssetMediaResponseDto; let assetSprings: AssetFileUploadResponseDto;
let assetLast: AssetMediaResponseDto; let assetLast: AssetFileUploadResponseDto;
let cities: string[]; let cities: string[];
let states: string[]; let states: string[];
let countries: string[]; let countries: string[];
@@ -66,7 +66,7 @@ describe('/search', () => {
// last asset // last asset
{ filename: '/albums/nature/wood_anemones.jpg' }, { filename: '/albums/nature/wood_anemones.jpg' },
]; ];
const assets: AssetMediaResponseDto[] = []; const assets: AssetFileUploadResponseDto[] = [];
for (const { filename, dto } of files) { for (const { filename, dto } of files) {
const bytes = await readFile(join(testAssetDir, filename)); const bytes = await readFile(join(testAssetDir, filename));
assets.push( assets.push(
@@ -134,7 +134,7 @@ describe('/search', () => {
// assetWood, // assetWood,
] = assets; ] = assets;
assetLast = assets.at(-1) as AssetMediaResponseDto; assetLast = assets.at(-1) as AssetFileUploadResponseDto;
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) }); await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });

View File

@@ -15,16 +15,16 @@ describe('/server-info', () => {
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
}); });
describe('GET /server-info/storage', () => { describe('GET /server-info', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/server-info/storage'); const { status, body } = await request(app).get('/server-info');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return the disk information', async () => { it('should return the disk information', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/server-info/storage') .get('/server-info')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({

View File

@@ -1,11 +1,11 @@
import { import {
AlbumResponseDto, AlbumResponseDto,
AssetMediaResponseDto, AssetFileUploadResponseDto,
LoginResponseDto, LoginResponseDto,
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
createAlbum, createAlbum,
deleteUserAdmin, deleteUser,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -13,10 +13,10 @@ import { app, asBearerAuth, shareUrl, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-links', () => { describe('/shared-link', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetMediaResponseDto; let asset1: AssetFileUploadResponseDto;
let asset2: AssetMediaResponseDto; let asset2: AssetFileUploadResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user2: LoginResponseDto; let user2: LoginResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
@@ -86,7 +86,7 @@ describe('/shared-links', () => {
}), }),
]); ]);
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
}); });
describe('GET /share/${key}', () => { describe('GET /share/${key}', () => {
@@ -114,9 +114,9 @@ describe('/shared-links', () => {
}); });
}); });
describe('GET /shared-links', () => { describe('GET /shared-link', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/shared-links'); const { status, body } = await request(app).get('/shared-link');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -124,7 +124,7 @@ describe('/shared-links', () => {
it('should get all shared links created by user', async () => { it('should get all shared links created by user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-links') .get('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -142,7 +142,7 @@ describe('/shared-links', () => {
it('should not get shared links created by other users', async () => { it('should not get shared links created by other users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-links') .get('/shared-link')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -150,15 +150,15 @@ describe('/shared-links', () => {
}); });
}); });
describe('GET /shared-links/me', () => { describe('GET /shared-link/me', () => {
it('should not require admin authentication', async () => { it('should not require admin authentication', async () => {
const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403); expect(status).toBe(403);
}); });
it('should get data for correct shared link', async () => { it('should get data for correct shared link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key }); const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
@@ -172,7 +172,7 @@ describe('/shared-links', () => {
it('should return unauthorized for incorrect shared link', async () => { it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-links/me') .get('/shared-link/me')
.query({ key: linkWithAlbum.key + 'foo' }); .query({ key: linkWithAlbum.key + 'foo' });
expect(status).toBe(401); expect(status).toBe(401);
@@ -180,14 +180,14 @@ describe('/shared-links', () => {
}); });
it('should return unauthorized if target has been soft deleted', async () => { it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithDeletedAlbum.key }); const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey); expect(body).toEqual(errorDto.invalidShareKey);
}); });
it('should return unauthorized for password protected link', async () => { it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key }); const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidSharePassword); expect(body).toEqual(errorDto.invalidSharePassword);
@@ -195,7 +195,7 @@ describe('/shared-links', () => {
it('should get data for correct password protected link', async () => { it('should get data for correct password protected link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-links/me') .get('/shared-link/me')
.query({ key: linkWithPassword.key, password: 'foo' }); .query({ key: linkWithPassword.key, password: 'foo' });
expect(status).toBe(200); expect(status).toBe(200);
@@ -209,7 +209,7 @@ describe('/shared-links', () => {
}); });
it('should return metadata for album shared link', async () => { it('should return metadata for album shared link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key }); const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@@ -225,7 +225,7 @@ describe('/shared-links', () => {
}); });
it('should not return metadata for album shared link without metadata', async () => { it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithoutMetadata.key }); const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@@ -239,9 +239,9 @@ describe('/shared-links', () => {
}); });
}); });
describe('GET /shared-links/:id', () => { describe('GET /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`); const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -249,7 +249,7 @@ describe('/shared-links', () => {
it('should get shared link by id', async () => { it('should get shared link by id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/shared-links/${linkWithAlbum.id}`) .get(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -264,7 +264,7 @@ describe('/shared-links', () => {
it('should not get shared link by id if user has not created the link or it does not exist', async () => { it('should not get shared link by id if user has not created the link or it does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/shared-links/${linkWithAlbum.id}`) .get(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -272,10 +272,10 @@ describe('/shared-links', () => {
}); });
}); });
describe('POST /shared-links', () => { describe('POST /shared-link', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-links') .post('/shared-link')
.send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound });
expect(status).toBe(401); expect(status).toBe(401);
@@ -284,7 +284,7 @@ describe('/shared-links', () => {
it('should require a type and the correspondent asset/album id', async () => { it('should require a type and the correspondent asset/album id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-links') .post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -293,7 +293,7 @@ describe('/shared-links', () => {
it('should require an asset/album id', async () => { it('should require an asset/album id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-links') .post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album }); .send({ type: SharedLinkType.Album });
@@ -303,7 +303,7 @@ describe('/shared-links', () => {
it('should require a valid asset id', async () => { it('should require a valid asset id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-links') .post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound }); .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
@@ -313,7 +313,7 @@ describe('/shared-links', () => {
it('should create a shared link', async () => { it('should create a shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-links') .post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album, albumId: album.id }); .send({ type: SharedLinkType.Album, albumId: album.id });
@@ -327,10 +327,10 @@ describe('/shared-links', () => {
}); });
}); });
describe('PATCH /shared-links/:id', () => { describe('PATCH /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-links/${linkWithAlbum.id}`) .patch(`/shared-link/${linkWithAlbum.id}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
expect(status).toBe(401); expect(status).toBe(401);
@@ -339,7 +339,7 @@ describe('/shared-links', () => {
it('should fail if invalid link', async () => { it('should fail if invalid link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-links/${uuidDto.notFound}`) .patch(`/shared-link/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
@@ -349,7 +349,7 @@ describe('/shared-links', () => {
it('should update shared link', async () => { it('should update shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-links/${linkWithAlbum.id}`) .patch(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
@@ -364,10 +364,10 @@ describe('/shared-links', () => {
}); });
}); });
describe('PUT /shared-links/:id/assets', () => { describe('PUT /shared-link/:id/assets', () => {
it('should not add assets to shared link (album)', async () => { it('should not add assets to shared link (album)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/shared-links/${linkWithAlbum.id}/assets`) .put(`/shared-link/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -377,7 +377,7 @@ describe('/shared-links', () => {
it('should add an assets to a shared link (individual)', async () => { it('should add an assets to a shared link (individual)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/shared-links/${linkWithAssets.id}/assets`) .put(`/shared-link/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -386,10 +386,10 @@ describe('/shared-links', () => {
}); });
}); });
describe('DELETE /shared-links/:id/assets', () => { describe('DELETE /shared-link/:id/assets', () => {
it('should not remove assets from a shared link (album)', async () => { it('should not remove assets from a shared link (album)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-links/${linkWithAlbum.id}/assets`) .delete(`/shared-link/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -399,7 +399,7 @@ describe('/shared-links', () => {
it('should remove assets from a shared link (individual)', async () => { it('should remove assets from a shared link (individual)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-links/${linkWithAssets.id}/assets`) .delete(`/shared-link/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -408,9 +408,9 @@ describe('/shared-links', () => {
}); });
}); });
describe('DELETE /shared-links/:id', () => { describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`); const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -418,7 +418,7 @@ describe('/shared-links', () => {
it('should fail if invalid link', async () => { it('should fail if invalid link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-links/${uuidDto.notFound}`) .delete(`/shared-link/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -427,7 +427,7 @@ describe('/shared-links', () => {
it('should delete a shared link', async () => { it('should delete a shared link', async () => {
const { status } = await request(app) const { status } = await request(app)
.delete(`/shared-links/${linkWithAlbum.id}`) .delete(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);

View File

@@ -1,4 +1,5 @@
import { LoginResponseDto, getConfig } from '@immich/sdk'; import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -8,10 +9,74 @@ const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAu
describe('/system-config', () => { describe('/system-config', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup(); admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
asset = await utils.createAsset(admin.accessToken);
});
describe('GET /system-config/map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/system-config/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should allow shared link access', async () => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
const { status, body } = await request(app)
.get(`/system-config/map/style.json?key=${sharedLink.key}`)
.query({ theme: 'dark' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should throw an error if a theme is not light or dark', async () => {
for (const theme of ['dark1', true, 123, '', null, undefined]) {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});
it('should return the light style.json', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'light' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
});
it('should return the dark style.json', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should not require admin authentication', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
}); });
describe('PUT /system-config', () => { describe('PUT /system-config', () => {

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -19,7 +19,7 @@ describe('/timeline', () => {
let user: LoginResponseDto; let user: LoginResponseDto;
let timeBucketUser: LoginResponseDto; let timeBucketUser: LoginResponseDto;
let userAssets: AssetMediaResponseDto[]; let userAssets: AssetFileUploadResponseDto[];
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk'; import { LoginResponseDto, getAllAssets } from '@immich/sdk';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
@@ -31,16 +31,16 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken); const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]); await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0); expect(after.length).toBe(0);
}); });
}); });
@@ -56,14 +56,14 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken); const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]); await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]);
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false })); expect(after).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: false })]);
}); });
}); });

View File

@@ -1,328 +0,0 @@
import {
LoginResponseDto,
deleteUserAdmin,
getMyUser,
getUserAdmin,
getUserPreferencesAdmin,
login,
} from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/admin/users', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, nonAdmin, deletedUser, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
await deleteUserAdmin(
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/admin/users`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should hide deleted users by default', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
]),
);
});
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/admin/users?withDeleted=true`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
expect.objectContaining({ email: deletedUser.userEmail }),
]),
);
});
});
describe('POST /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send(createUserDto.user1);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
});
describe('PUT /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ isAdmin: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ isAdmin: false });
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should update first and last name', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ name: 'Name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...before,
updatedAt: expect.any(String),
name: 'Name',
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update password', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ password: 'super-secret' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ email: nonAdmin.userEmail });
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail });
});
});
describe('PUT /admin/users/:id/preferences', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update memories enabled', async () => {
const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ memories: { enabled: false } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ memories: { enabled: false } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
});
});
describe('DELETE /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete a user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('POST /admin/users/:id/restore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users/${userToDelete.userId}/restore`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
});
});

View File

@@ -1,104 +1,288 @@
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk'; import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { createUserDto } from 'src/fixtures'; import { Socket } from 'socket.io-client';
import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/user', () => {
let websocket: Socket;
describe('/users', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let deletedUser: LoginResponseDto; let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto; let nonAdmin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false }); admin = await utils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin] = await Promise.all([ [websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1), utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2), utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]); ]);
await deleteUserAdmin( await deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
}); });
describe('GET /users', () => { afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /user', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/users'); const { status, body } = await request(app).get('/user');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should get users', async () => { it('should get users', async () => {
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(2); expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
it('should hide deleted users', async () => {
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]), ]),
); );
}); });
}); });
describe('GET /users/me', () => { describe('GET /user/info/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/users/me`); const { status } = await request(app).get(`/user/info/${admin.userId}`);
expect(status).toBe(401); expect(status).toEqual(401);
expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not work for shared links', async () => { it('should get the user info', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' }); const { status, body } = await request(app)
const sharedLink = await utils.createSharedLink(admin.accessToken, { .get(`/user/info/${admin.userId}`)
type: SharedLinkType.Album, .set('Authorization', `Bearer ${admin.accessToken}`);
albumId: album.id,
});
const { status, body } = await request(app).get(`/users/me?key=${sharedLink.key}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get my user', async () => {
const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
id: admin.userId, id: admin.userId,
email: 'admin@immich.cloud', email: 'admin@immich.cloud',
quotaUsageInBytes: 0,
}); });
}); });
}); });
describe('PUT /users/me', () => { describe('GET /user/me', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/users/me`); const { status, body } = await request(app).get(`/user/me`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
for (const key of ['email', 'name']) { it('should get my info', async () => {
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
});
});
describe('POST /user', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(createUserDto.user1)) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .post(`/user`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(dto); .send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(errorDto.badRequest());
}); });
} }
it('should update first and last name', async () => { it('should ignore `isAdmin`', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); const { status, body } = await request(app)
.post(`/user`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/user`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('DELETE /user/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('PUT /user', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/user`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(userDto.admin)) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/user`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...userDto.admin, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/user`)
.send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/user`)
.send({ id: admin.userId, profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .put(`/user`)
.send({ name: 'Name' }) .send({
id: admin.userId,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
deletedAt: '2023-01-01T00:00:00.000Z',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toStrictEqual(before);
});
it('should update first and last name', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
name: 'Name',
})
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -107,94 +291,26 @@ describe('/users', () => {
updatedAt: expect.any(String), updatedAt: expect.any(String),
name: 'Name', name: 'Name',
}); });
expect(before.updatedAt).not.toEqual(body.updatedAt);
}); });
/** @deprecated */ it('should update memories enabled', async () => {
it('should allow a user to change their password (deprecated)', async () => { const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
expect(user.shouldChangePassword).toBe(true);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .put(`/user`)
.send({ password: 'super-secret' }) .send({
.set('Authorization', `Bearer ${nonAdmin.accessToken}`); id: admin.userId,
memoriesEnabled: false,
expect(status).toBe(200); })
expect(body).toMatchObject({ .set('Authorization', `Bearer ${admin.accessToken}`);
email: nonAdmin.userEmail,
shouldChangePassword: false,
});
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
});
it('should not allow user to change to a taken email', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
});
it('should update my email', async () => {
const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'non-admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
...before, ...before,
email: 'non-admin@immich.cloud',
updatedAt: expect.anything(), updatedAt: expect.anything(),
memoriesEnabled: false,
}); });
}); expect(before.updatedAt).not.toEqual(body.updatedAt);
});
describe('PUT /users/me/preferences', () => {
it('should update memories enabled', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ memories: { enabled: false } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ memories: { enabled: false } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401);
});
it('should get the user', async () => {
const { status, body } = await request(app)
.get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
expect(body).not.toMatchObject({
shouldChangePassword: expect.anything(),
storageLabel: expect.anything(),
});
}); });
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk'; import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
@@ -28,8 +28,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1); expect(assets.length).toBe(1);
}); });
it('should skip a duplicate file', async () => { it('should skip a duplicate file', async () => {
@@ -40,8 +40,8 @@ describe(`immich upload`, () => {
); );
expect(first.exitCode).toBe(0); expect(first.exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1); expect(assets.length).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
expect(second.stderr).toBe(''); expect(second.stderr).toBe('');
@@ -60,8 +60,8 @@ describe(`immich upload`, () => {
expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')])); expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -76,8 +76,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
}); });
it('dry run should handle duplicates', async () => { it('dry run should handle duplicates', async () => {
@@ -88,8 +88,8 @@ describe(`immich upload`, () => {
); );
expect(first.exitCode).toBe(0); expect(first.exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1); expect(assets.length).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
expect(second.stderr).toBe(''); expect(second.stderr).toBe('');
@@ -112,8 +112,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9); expect(assets.length).toBe(9);
}); });
}); });
@@ -135,8 +135,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9); expect(assets.length).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1); expect(albums.length).toBe(1);
@@ -151,8 +151,8 @@ describe(`immich upload`, () => {
expect(response1.stderr).toBe(''); expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0); expect(response1.exitCode).toBe(0);
const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets1.total).toBe(9); expect(assets1.length).toBe(9);
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0); expect(albums1.length).toBe(0);
@@ -167,8 +167,8 @@ describe(`immich upload`, () => {
expect(response2.stderr).toBe(''); expect(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0); expect(response2.exitCode).toBe(0);
const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets2.total).toBe(9); expect(assets2.length).toBe(9);
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums2.length).toBe(1); expect(albums2.length).toBe(1);
@@ -193,8 +193,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0); expect(albums.length).toBe(0);
@@ -219,8 +219,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9); expect(assets.length).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1); expect(albums.length).toBe(1);
@@ -245,8 +245,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0); expect(albums.length).toBe(0);
@@ -276,8 +276,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9); expect(assets.length).toBe(9);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -302,8 +302,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
}); });
}); });
@@ -328,8 +328,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1); expect(assets.length).toBe(1);
}); });
it('should throw an error if attempting dry run', async () => { it('should throw an error if attempting dry run', async () => {
@@ -344,8 +344,8 @@ describe(`immich upload`, () => {
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`); expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
expect(exitCode).not.toBe(0); expect(exitCode).not.toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
}); });
}); });
@@ -367,8 +367,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9); expect(assets.length).toBe(9);
}); });
it('should reject string argument', async () => { it('should reject string argument', async () => {
@@ -408,8 +408,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(8); expect(assets.length).toBe(8);
}); });
it('should ignore assets matching glob pattern', async () => { it('should ignore assets matching glob pattern', async () => {
@@ -429,8 +429,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1); expect(assets.length).toBe(1);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -451,8 +451,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0); expect(assets.length).toBe(0);
}); });
}); });
}); });

View File

@@ -1,3 +1,5 @@
import { UserAvatarColor } from '@immich/sdk';
export const uuidDto = { export const uuidDto = {
invalid: 'invalid-uuid', invalid: 'invalid-uuid',
// valid uuid v4 // valid uuid v4
@@ -68,6 +70,8 @@ export const userDto = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.Primary,
quotaSizeInBytes: null, quotaSizeInBytes: null,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
}, },
@@ -84,6 +88,8 @@ export const userDto = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.Primary,
quotaSizeInBytes: null, quotaSizeInBytes: null,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
}, },

View File

@@ -68,6 +68,7 @@ export const signupResponseDto = {
updatedAt: expect.any(String), updatedAt: expect.any(String),
deletedAt: null, deletedAt: null,
oauthId: '', oauthId: '',
memoriesEnabled: true,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
quotaSizeInBytes: null, quotaSizeInBytes: null,
status: 'active', status: 'active',

View File

@@ -1,23 +1,23 @@
import { import {
AllJobStatusResponseDto, AllJobStatusResponseDto,
AssetMediaCreateDto, AssetFileUploadResponseDto,
AssetMediaResponseDto,
AssetResponseDto, AssetResponseDto,
CreateAlbumDto, CreateAlbumDto,
CreateAssetDto,
CreateLibraryDto, CreateLibraryDto,
CreateUserDto,
MetadataSearchDto, MetadataSearchDto,
PersonCreateDto, PersonCreateDto,
SharedLinkCreateDto, SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto, ValidateLibraryDto,
createAlbum, createAlbum,
createApiKey, createApiKey,
createLibrary, createLibrary,
createPartner,
createPerson, createPerson,
createSharedLink, createSharedLink,
createUserAdmin, createUser,
deleteAssets, deleteAssets,
getAllAssets,
getAllJobsStatus, getAllJobsStatus,
getAssetInfo, getAssetInfo,
getConfigDefaults, getConfigDefaults,
@@ -273,8 +273,8 @@ export const utils = {
return response; return response;
}, },
userSetup: async (accessToken: string, dto: UserAdminCreateDto) => { userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUserAdmin({ userAdminCreateDto: dto }, { headers: asBearerAuth(accessToken) }); await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
return login({ return login({
loginCredentialDto: { email: dto.email, password: dto.password }, loginCredentialDto: { email: dto.email, password: dto.password },
}); });
@@ -292,7 +292,7 @@ export const utils = {
createAsset: async ( createAsset: async (
accessToken: string, accessToken: string,
dto?: Partial<Omit<AssetMediaCreateDto, 'assetData'>> & { assetData?: AssetData }, dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
) => { ) => {
const _dto = { const _dto = {
deviceAssetId: 'test-1', deviceAssetId: 'test-1',
@@ -310,7 +310,7 @@ export const utils = {
} }
const builder = request(app) const builder = request(app)
.post(`/assets`) .post(`/asset/upload`)
.attach('assetData', assetData, filename) .attach('assetData', assetData, filename)
.set('Authorization', `Bearer ${accessToken}`); .set('Authorization', `Bearer ${accessToken}`);
@@ -320,7 +320,7 @@ export const utils = {
const { body } = await builder; const { body } = await builder;
return body as AssetMediaResponseDto; return body as AssetFileUploadResponseDto;
}, },
createImageFile: (path: string) => { createImageFile: (path: string) => {
@@ -340,6 +340,8 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => { metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
}, },
@@ -386,8 +388,6 @@ export const utils = {
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) => validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string) => setAuthCookies: async (context: BrowserContext, accessToken: string) =>
await context.addCookies([ await context.addCookies([
{ {

View File

@@ -1,60 +0,0 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test('can be opened for shared links', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('cannot be opened for shared links with hidden metadata', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('description is visible for owner on shared links', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
const textarea = page.getByRole('textbox', { name: 'Add a description' });
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).toBeVisible();
await expect(textarea).not.toBeDisabled();
});
});

View File

@@ -1,52 +0,0 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Asset Viewer Navbar', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test.describe('shared link without metadata', () => {
test('visible guest actions', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
test('visible owner actions', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Share', 'Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
});
});

View File

@@ -1,6 +1,6 @@
import { import {
AlbumResponseDto, AlbumResponseDto,
AssetMediaResponseDto, AssetFileUploadResponseDto,
LoginResponseDto, LoginResponseDto,
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
@@ -11,7 +11,7 @@ import { asBearerAuth, utils } from 'src/utils';
test.describe('Shared Links', () => { test.describe('Shared Links', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset: AssetMediaResponseDto; let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
let sharedLink: SharedLinkResponseDto; let sharedLink: SharedLinkResponseDto;
let sharedLinkPassword: SharedLinkResponseDto; let sharedLinkPassword: SharedLinkResponseDto;

View File

@@ -95,5 +95,3 @@ COPY start.sh log_conf.json ./
COPY app . COPY app .
ENTRYPOINT ["tini", "--"] ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"] CMD ["./start.sh"]
HEALTHCHECK CMD python3 healthcheck.py

View File

@@ -1,14 +0,0 @@
import os
import sys
import requests
port = os.getenv("IMMICH_PORT", 3003)
try:
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
if response.status_code == 200:
sys.exit(0)
sys.exit(1)
except requests.RequestException:
sys.exit(1)

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:4688551ffd61358d5bebfd88e0aac12d5b4aed7a153c170dbc435da453476a13 as builder FROM mambaorg/micromamba:bookworm-slim@sha256:d5b82811074b396275ef69aadbf31098257dd8836e231371e9cdb393128e571c as builder
ENV TRANSFORMERS_CACHE=/cache \ ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \

View File

@@ -957,74 +957,74 @@ test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idn
[[package]] [[package]]
name = "geventhttpclient" name = "geventhttpclient"
version = "2.3.1" version = "2.2.1"
description = "HTTP client library for gevent" description = "HTTP client library for gevent"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da22ab7bf5af4ba3d07cffee6de448b42696e53e7ac1fe97ed289037733bf1c2"}, {file = "geventhttpclient-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:672c6b0239dc6651c02b54b5d3f67290af40fade700ee3ab48fc97f09c6a5dc6"},
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2399e3d4e2fae8bbd91756189da6e9d84adf8f3eaace5eef0667874a705a29f8"}, {file = "geventhttpclient-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f11fda0645c406c250e01db97a3e2d2f804c7b50eb1432d1e00f37225bcc4598"},
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e33e87d0d5b9f5782c4e6d3cb7e3592fea41af52713137d04776df7646d71b"}, {file = "geventhttpclient-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:34338eafa649a281d7f5453c3aaf88744137bbe099ad3ba157ae491cd88b96e0"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071db313866c3d0510feb6c0f40ec086ccf7e4a845701b6316c82c06e8b9b29"}, {file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1021556cc4329246a4493ad90ac8a55594c27c2b85093676dc937cf19d6de2"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f36f0c6ef88a27e60af8369d9c2189fe372c6f2943182a7568e0f2ad33bb69f1"}, {file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06c6cc714ce66f66e8f892575aecdbed2355afe4b39cb89d08eb8728b8523466"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4624843c03a5337282a42247d987c2531193e57255ee307b36eeb4f243a0c21"}, {file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df3788352d9ee10fa7c6cdfa45260e353e96466555e2a7d2ebcc394f607e0cce"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d614573621ba827c417786057e1e20e9f96c4f6b3878c55b1b7b54e1026693bc"}, {file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ebe1333f4f6b879f84576ac1aeacbe32a382716f05172f9aa38313bf1bbcf45"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d51330a40ac9762879d0e296c279c1beae8cfa6484bb196ac829242c416b709"}, {file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bcdb648301db9649d3a099d3f833919315ff34f26e47149f986b0ca2f5b0e186"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc9f2162d4e8cb86bb5322d99bfd552088a3eacd540a841298f06bb8bc1f1f03"}, {file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:81d6d5a6a0a93c0b7d395270d5d357bbcc4b4502ea2086e711869a65c0f9fc30"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e59d3397e63c65ecc7a7561a5289f0cf2e2c2252e29632741e792f57f5d124"}, {file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:6af2fc621ea8c7aae6fa49c2204bd80050a0c56ea349011f3ebe2f36d8623ad4"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4436eef515b3e0c1d4a453ae32e047290e780a623c1eddb11026ae9d5fb03d42"}, {file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff7bbc4b4b913631dbc6f23d3d3cbbf1d9b020181cbfa8a806e13ebb01e13219"},
{file = "geventhttpclient-2.3.1-cp310-cp310-win32.whl", hash = "sha256:5d1cf7d8a4f8e15cc8fd7d88ac4cdb058d6274203a42587e594cc9f0850ac862"}, {file = "geventhttpclient-2.2.1-cp310-cp310-win32.whl", hash = "sha256:cfa65f0c595ad2cf9f129f7cf18de076db4f72449fa8a6cc7f7cf554e5332832"},
{file = "geventhttpclient-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4deaebc121036f7ea95430c2d0f80ab085b15280e6ab677a6360b70e57020e7f"}, {file = "geventhttpclient-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:f4e1ae7ad0bd7a00c679874652ea49a6352f91690c35ee0da45bf63114ad433b"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0ae055b9ce1704f2ce72c0847df28f4e14dbb3eea79256cda6c909d82688ea3"}, {file = "geventhttpclient-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:438d3f8c2ba0a9a8b58d62f6ccd29bea468b41f71132f21eb9e8aff347e98c5d"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f087af2ac439495b5388841d6f3c4de8d2573ca9870593d78f7b554aa5cfa7f5"}, {file = "geventhttpclient-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e22e108b64d20c8767b1e78ebe230d3f2af5805e80246d6aa2afd1dab4a6f19"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76c367d175810facfe56281e516c9a5a4a191eff76641faaa30aa33882ed4b2f"}, {file = "geventhttpclient-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:164ec70971c915ea3716d4175d704c6cb0cb020a64eb6ea7f0a3277abd07f2fb"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58376d0d461fe0322ff2ad362553b437daee1eeb92b4c0e3b1ffef9e77defbe"}, {file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83589b7708f40b1366616dab832fcefb3f486cf61c65dac9bf2fe3196850d34d"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f440cc704f8a9869848a109b2c401805c17c070539b2014e7b884ecfc8591e33"}, {file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d735d39b9c64fb79f01b36d47f38653f8988d441d6b7dbaedac3d4b45f0cd21"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f10c62994f9052f23948c19de930b2d1f063240462c8bd7077c2b3290e61f4fa"}, {file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41b56ae8a616fa237b45e1a7bc9c474441d7e69fb46a1fac4f6edc1d462454d9"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c45d9f3dd9627844c12e9ca347258c7be585bed54046336220e25ea6eac155"}, {file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034961b2fafcdf1f54895f37980aca5bafa8740dde39d2eacbacb4e0995b99a5"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:77c1a2c6e3854bf87cd5588b95174640c8a881716bd07fa0d131d082270a6795"}, {file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eec11a2e3501e0170f057f4e292a5715d57e3362fefa75f804302fc4bd916b38"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ce649d4e25c2d56023471df0bf1e8e2ab67dfe4ff12ce3e8fe7e6fae30cd672a"}, {file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7387571fa24608e40230bd60641bb811dd0565f77dd52b7b3249eecb9293d01a"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:265d9f31b4ac8f688eebef0bd4c814ffb37a16f769ad0c8c8b8c24a84db8eab5"}, {file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f37e0f56ade9c308ef5f5359bcb9d69f8b6d6ee177f2e1965b5f75472dfb02f9"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2de436a9d61dae877e4e811fb3e2594e2a1df1b18f4280878f318aef48a562b9"}, {file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bbcf295b8987114437215ed5b2980811a5d135ddcdc1258add64caee679de8c"},
{file = "geventhttpclient-2.3.1-cp311-cp311-win32.whl", hash = "sha256:83e22178b9480b0a95edf0053d4f30b717d0b696b3c262beabe6964d9c5224b1"}, {file = "geventhttpclient-2.2.1-cp311-cp311-win32.whl", hash = "sha256:44e206dea6c5d11287f4ad96dd807d4cd85f8aad1a243f7b0d87a90dc877bdcd"},
{file = "geventhttpclient-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:97b072a282233384c1302a7dee88ad8bfedc916f06b1bc1da54f84980f1406a9"}, {file = "geventhttpclient-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:e5c55f3526bf3d9c47a6c4d789ad9cd224ed301740e15c1bdeb7bc067b38c7bf"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1c90abcc2735cd8dd2d2572a13da32f6625392dc04862decb5c6476a3ddee22"}, {file = "geventhttpclient-2.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:86f0372314515cc49bd88a1d733db31f8d746f77790cd3e9fcb2bfadbf06bf01"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5deb41c2f51247b4e568c14964f59d7b8e537eff51900564c88af3200004e678"}, {file = "geventhttpclient-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2203442640dc0f2178be7b7a2ed285deffeda31c80045162a291292f1269cf8b"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f1a56a66a90c4beae2f009b5e9d42db9a58ced165aa35441ace04d69cb7b37"}, {file = "geventhttpclient-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:005e4798af49bd017c19c7272f87e05bfd72ba7ff876de5a3457026587c16c33"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ee6e741849c29e3129b1ec3828ac3a5e5dcb043402f852ea92c52334fb8cabf"}, {file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4188f482cc7d970b7fe71e178199c853064c17c6bfa87a4f5f482bb2a2db3d2"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d0972096a63b1ddaa73fa3dab2c7a136e3ab8bf7999a2f85a5dee851fa77cdd"}, {file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f805eab5012133aabab802fc1efc7a865226f534340ce2617439c3be4f10925f"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00675ba682fb7d19d659c14686fa8a52a65e3f301b56c2a4ee6333b380dd9467"}, {file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75f2fff7785887441c4f57aa6004a5edf545952db089f060655f77dacc2f8a9f"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea77b67c186df90473416f4403839728f70ef6cf1689cec97b4f6bbde392a8a8"}, {file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c20f68942bea6789abe363a08abb8453017c6eda69bc69d9b6c52f166254375c"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ddcc3f0fdffd9a3801e1005b73026202cffed8199863fdef9315bea9a860a032"}, {file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d9ab6892e9b95a782a3af279f07e60ee4de98f94e0a9c78955c820a1e7bb821"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c9f1ef4ec048563cc621a47ff01a4f10048ff8b676d7a4d75e5433ed8e703e56"}, {file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69f71152c5ff9272c1d4ee653c0ba7357e2eada4c3af68ceaa3b866c0b7410e8"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:a364b30bec7a0a00dbe256e2b6807e4dc866bead7ac84aaa51ca5e2c3d15c258"}, {file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b173bc1d11ee2bef1d46f5159a23fa749f7c770b75127184aa855df976267a05"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25d255383d3d6a6fbd643bb51ae1a7e4f6f7b0dbd5f3225b537d0bd0432eaf39"}, {file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7a771dfbaba83ba558d438e5e3ece49f04c683e3af510ad366f94502af7c5f4e"},
{file = "geventhttpclient-2.3.1-cp312-cp312-win32.whl", hash = "sha256:ad0b507e354d2f398186dcb12fe526d0594e7c9387b514fb843f7a14fdf1729a"}, {file = "geventhttpclient-2.2.1-cp312-cp312-win32.whl", hash = "sha256:438ee39c11b83d737e6c8121467a0e72d2cabe8c5a3a8d432106a10c9c95df79"},
{file = "geventhttpclient-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:7924e0883bc2b177cfe27aa65af6bb9dd57f3e26905c7675a2d1f3ef69df7cca"}, {file = "geventhttpclient-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f125e37261e9cf1841cd3d81b196e051150d7fbbf74652aad40eafab08b19969"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fe912c6456faab196b952adcd63e9353a0d5c8deb31c8d733d38f4f0ab22e359"}, {file = "geventhttpclient-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:37030e799993c2576c30264b58e868e7de6bbd9ff6298dace713e7ba5c545346"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b599359779c2278018786c35d70664d441a7cd0d6baef2b2cd0d1685cf478ed"}, {file = "geventhttpclient-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ab382d7f736fa87a0f417b3b2b67b4ce8a81fceda38d1e6344725907b9d405"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34107b506e2c40ec7784efa282469bf86888cacddced463dceeb58c201834897"}, {file = "geventhttpclient-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f0691aaeb87f3ad8337b3d862c2f74d8910a2762076adfd32940094eb10a267"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34031905b2b31a80d88cd33d7e42b81812950e5304860ab6a65ee2803e2046"}, {file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e51627d3690a8829199ac39197d081cb13bc866c8c7fe9d9c383517b4bbbbfb"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50b54f67ba2087f4d9d2172065c5c5de0f0c7f865ac350116e5452de4be31444"}, {file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01f4ebcd0cae416cab27092f65c6b5a8c6bc9d50e9447f6278c6261995fb6629"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ddeb431836c2ef7fd33c505a06180dc907b474e0e8537a43ff12e12c9bf0307"}, {file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9cb660559b292d7a1e3d22938d384cc3c534d356ca308f50d9c3801bfc404cb"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4890713433ca19b081f70b5f7ad258a0979ec3354f9538b50b3ad7d0a86f88de"}, {file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eec345499bbdf9acccdbd08e9180ff93334bf339cb2b0250b57b6a74a742bd4"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8ca7dcbe94cb563341087b00b6fbd0fdd70b2acc1b5d963f9ebbfbc1e5e2893"}, {file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e013cb4fcadbb5e9ef36cbd8774bc8b70ea09f9b4d2ec84b9f3e2b5a203e1bfa"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05a1bbdd43ae36bcc10b3dbfa0806aefc5033a91efecfddfe56159446a46ea71"}, {file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2144d1900db9f6b5d5560ecba2bba39922829d09dbebaa794ebb0ad9e4747618"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f82c454595a88a5e510ae0985711ef398386998b6f37d90fc30e9ff1a2001280"}, {file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8b30fdd201893a8ed7cfd98df23925623f0e731737e42050a5602d7ed038e55e"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b032a5cdb1721921f4cd36aad620af318263b462962cfb23d648cdb93aab232"}, {file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ec607413b3ac1b62035c2bdf5e27d705c8d74a3ecd26851318380c66231909e2"},
{file = "geventhttpclient-2.3.1-cp39-cp39-win32.whl", hash = "sha256:ce2c7d18bac7ffdacc4a86cd490bea6136a7d1e1170f8624f2e3bbe3b189d5b8"}, {file = "geventhttpclient-2.2.1-cp39-cp39-win32.whl", hash = "sha256:a06342791b66e2c40b53e7d8ba0fad6b88704cc5e7dcf8d795bbe16e88f783c2"},
{file = "geventhttpclient-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ca50dd9761971d3557b897108933b34fb4a11533d52f0f2753840c740a2861a"}, {file = "geventhttpclient-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:a77fc38028c6fb8d9f712f9589c20e8da275368daf81c3efb3019cc2056b18a4"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c31431e38df45b3c79bf3c9427c796adb8263d622bc6fa25e2f6ba916c2aad93"}, {file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c078d03bc1edf2b484ef056312e132772cb9debd0cf0ac3f27144014b504228e"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855ab1e145575769b180b57accb0573a77cd6a7392f40a6ef7bc9a4926ebd77b"}, {file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45daaec4ab2b77861a0a81a8735bb82f2571b5035366323ffac9f80abd2973cd"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a374aad77c01539e786d0c7829bec2eba034ccd45733c1bf9811ad18d2a8ecd"}, {file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89cd7dc244e8052d0de7ae345aa009739f1ae32bbd2a0668a422321824bcd8b9"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c1e97460608304f400485ac099736fff3566d3d8db2038533d466f8cf5de5a"}, {file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4835f5486cdf84c64680bba49a59439a81fa9eb632e64c7e86956d074e56a7"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4f843f81ee44ba4c553a1b3f73115e0ad8f00044023c24db29f5b1df3da08465"}, {file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8685d152abecd58d9b546012b08a35d1ff0e37761039e817347960ef576fff68"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:321b73c73d73b85cfeff36b9b5ee04174ec8406fb3dadc129558a26ccb879360"}, {file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ff2f6b587e7834bebf8ced8be227372b11c24c5429615b9080e2d18401403329"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829d03c2a140edbe74ad1fb4f850384f585f3e06fc47cfe647d065412b93926f"}, {file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4aa373c83d4724066e528d7526f46139e03299a474ff442cc50f3c802e6cc0f"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994c543f156db7bce3bae15491a0e041eeb3f1cf467e0d1db0c161a900a90bec"}, {file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd354a3f7fa6b1d6bd1c4875e8d35861cb5021fd475d5120e65462b85c546b8e"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4beff505306aa9da5cdfe2f206b403ec7c8d06a22d6b7248365772858c4ee8c"}, {file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d488c914aeae9c740c0a90203ebffa195fac0bfc974a284df4677f39fc0d4d9"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fb0a9673074541ccda09a2423fa16f4528819ceb1ba19d252213f6aca7d4b44a"}, {file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0102e761996967bb28689c068a73c009cda43fa80a54b26253198c734926d043"},
{file = "geventhttpclient-2.3.1.tar.gz", hash = "sha256:b40ddac8517c456818942c7812f555f84702105c82783238c9fcb8dc12675185"}, {file = "geventhttpclient-2.2.1.tar.gz", hash = "sha256:29f7e02683e3cd4f0032fba67364ff322e8504fddd170d9de5541bcfade85a50"},
] ]
[package.dependencies] [package.dependencies]
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.23.2" version = "0.23.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.23.2-py3-none-any.whl", hash = "sha256:48727a16e704d409c4bb5913613308499664f22a99743435dc3a13b23c485827"}, {file = "huggingface_hub-0.23.0-py3-none-any.whl", hash = "sha256:075c30d48ee7db2bba779190dc526d2c11d422aed6f9044c5e2fdc2c432fdb91"},
{file = "huggingface_hub-0.23.2.tar.gz", hash = "sha256:f6829b62d5fdecb452a76fdbec620cba4c1573655a8d710c1df71735fd9edbd2"}, {file = "huggingface_hub-0.23.0.tar.gz", hash = "sha256:7126dedd10a4c6fac796ced4d87a8cf004efc722a5125c2c09299017fa366fa9"},
] ]
[package.dependencies] [package.dependencies]
@@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]] [[package]]
name = "locust" name = "locust"
version = "2.28.0" version = "2.27.0"
description = "Developer-friendly load testing framework" description = "Developer friendly load testing framework"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "locust-2.28.0-py3-none-any.whl", hash = "sha256:766be879db030c0118e7d9fca712f3538c4e628bdebf59468fa1c6c2fab217d3"}, {file = "locust-2.27.0-py3-none-any.whl", hash = "sha256:c4db5747eb9a3851216deae8147143d335db41978a9291ac32e113fa9ec8ad39"},
{file = "locust-2.28.0.tar.gz", hash = "sha256:260557eec866f7e34a767b6c916b5b278167562a280480aadb88f43d962fbdeb"}, {file = "locust-2.27.0.tar.gz", hash = "sha256:0c6d3d2523976dafe734012c41b2f7d9ad7120cbcea76d47d80cec5d6d139905"},
] ]
[package.dependencies] [package.dependencies]
@@ -1545,7 +1545,7 @@ flask = ">=2.0.0"
Flask-Cors = ">=3.0.10" Flask-Cors = ">=3.0.10"
Flask-Login = ">=0.6.3" Flask-Login = ">=0.6.3"
gevent = ">=22.10.2" gevent = ">=22.10.2"
geventhttpclient = ">=2.3.1" geventhttpclient = "2.2.1"
msgpack = ">=1.0.0" msgpack = ">=1.0.0"
psutil = ">=5.9.1" psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""} pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
@@ -1958,36 +1958,36 @@ reference = ["Pillow", "google-re2"]
[[package]] [[package]]
name = "onnxruntime" name = "onnxruntime"
version = "1.18.0" version = "1.17.3"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models" description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "onnxruntime-1.18.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:5a3b7993a5ecf4a90f35542a4757e29b2d653da3efe06cdd3164b91167bbe10d"}, {file = "onnxruntime-1.17.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d86dde9c0bb435d709e51bd25991c9fe5b9a5b168df45ce119769edc4d198b15"},
{file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15b944623b2cdfe7f7945690bfb71c10a4531b51997c8320b84e7b0bb59af902"}, {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d87b68bf931ac527b2d3c094ead66bb4381bac4298b65f46c54fe4d1e255865"},
{file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e61ce5005118064b1a0ed73ebe936bc773a102f067db34108ea6c64dd62a179"}, {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e950cf0333cf114a155f9142e71da344d2b08dfe202763a403ae81cc02ebd1"},
{file = "onnxruntime-1.18.0-cp310-cp310-win32.whl", hash = "sha256:a4fc8a2a526eb442317d280610936a9f73deece06c7d5a91e51570860802b93f"}, {file = "onnxruntime-1.17.3-cp310-cp310-win32.whl", hash = "sha256:0962a4d0f5acebf62e1f0bf69b6e0adf16649115d8de854c1460e79972324d68"},
{file = "onnxruntime-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:71ed219b768cab004e5cd83e702590734f968679bf93aa488c1a7ffbe6e220c3"}, {file = "onnxruntime-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:468ccb8a0faa25c681a41787b1594bf4448b0252d3efc8b62fd8b2411754340f"},
{file = "onnxruntime-1.18.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:3d24bd623872a72a7fe2f51c103e20fcca2acfa35d48f2accd6be1ec8633d960"}, {file = "onnxruntime-1.17.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e8cd90c1c17d13d47b89ab076471e07fb85467c01dcd87a8b8b5cdfbcb40aa51"},
{file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f15e41ca9b307a12550bfd2ec93f88905d9fba12bab7e578f05138ad0ae10d7b"}, {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a058b39801baefe454eeb8acf3ada298c55a06a4896fafc224c02d79e9037f60"},
{file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f45ca2887f62a7b847d526965686b2923efa72538c89b7703c7b3fe970afd59"}, {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f823d5eb4807007f3da7b27ca972263df6a1836e6f327384eb266274c53d05d"},
{file = "onnxruntime-1.18.0-cp311-cp311-win32.whl", hash = "sha256:9e24d9ecc8781323d9e2eeda019b4b24babc4d624e7d53f61b1fe1a929b0511a"}, {file = "onnxruntime-1.17.3-cp311-cp311-win32.whl", hash = "sha256:b66b23f9109e78ff2791628627a26f65cd335dcc5fbd67ff60162733a2f7aded"},
{file = "onnxruntime-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8608398976ed18aef450d83777ff6f77d0b64eced1ed07a985e1a7db8ea3771"}, {file = "onnxruntime-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:570760ca53a74cdd751ee49f13de70d1384dcf73d9888b8deac0917023ccda6d"},
{file = "onnxruntime-1.18.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f1d79941f15fc40b1ee67738b2ca26b23e0181bf0070b5fb2984f0988734698f"}, {file = "onnxruntime-1.17.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:77c318178d9c16e9beadd9a4070d8aaa9f57382c3f509b01709f0f010e583b99"},
{file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e8caf3a8565c853a22d323a3eebc2a81e3de7591981f085a4f74f7a60aab2d"}, {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23da8469049b9759082e22c41a444f44a520a9c874b084711b6343672879f50b"},
{file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:498d2b8380635f5e6ebc50ec1b45f181588927280f32390fb910301d234f97b8"}, {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2949730215af3f9289008b2e31e9bbef952012a77035b911c4977edea06f3f9e"},
{file = "onnxruntime-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ba7cc0ce2798a386c082aaa6289ff7e9bedc3dee622eef10e74830cff200a72e"}, {file = "onnxruntime-1.17.3-cp312-cp312-win32.whl", hash = "sha256:6c7555a49008f403fb3b19204671efb94187c5085976ae526cb625f6ede317bc"},
{file = "onnxruntime-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:1fa175bd43f610465d5787ae06050c81f7ce09da2bf3e914eb282cb8eab363ef"}, {file = "onnxruntime-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:58672cf20293a1b8a277a5c6c55383359fcdf6119b2f14df6ce3b140f5001c39"},
{file = "onnxruntime-1.18.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0284c579c20ec8b1b472dd190290a040cc68b6caec790edb960f065d15cf164a"}, {file = "onnxruntime-1.17.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4395ba86e3c1e93c794a00619ef1aec597ab78f5a5039f3c6d2e9d0695c0a734"},
{file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47353d036d8c380558a5643ea5f7964d9d259d31c86865bad9162c3e916d1f6"}, {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf354c04344ec38564fc22394e1fe08aa6d70d790df00159205a0055c4a4d3f"},
{file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:885509d2b9ba4b01f08f7fa28d31ee54b6477953451c7ccf124a84625f07c803"}, {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a94b600b7af50e922d44b95a57981e3e35103c6e3693241a03d3ca204740bbda"},
{file = "onnxruntime-1.18.0-cp38-cp38-win32.whl", hash = "sha256:8614733de3695656411d71fc2f39333170df5da6c7efd6072a59962c0bc7055c"}, {file = "onnxruntime-1.17.3-cp38-cp38-win32.whl", hash = "sha256:5a335c76f9c002a8586c7f38bc20fe4b3725ced21f8ead835c3e4e507e42b2ab"},
{file = "onnxruntime-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:47af3f803752fce23ea790fd8d130a47b2b940629f03193f780818622e856e7a"}, {file = "onnxruntime-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f56a86fbd0ddc8f22696ddeda0677b041381f4168a2ca06f712ef6ec6050d6d"},
{file = "onnxruntime-1.18.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:9153eb2b4d5bbab764d0aea17adadffcfc18d89b957ad191b1c3650b9930c59f"}, {file = "onnxruntime-1.17.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:e0ae39f5452278cd349520c296e7de3e90d62dc5b0157c6868e2748d7f28b871"},
{file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c7fd86eca727c989bb8d9c5104f3c45f7ee45f445cc75579ebe55d6b99dfd7c"}, {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ff2dc012bd930578aff5232afd2905bf16620815f36783a941aafabf94b3702"},
{file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac67a4de9c1326c4d87bcbfb652c923039b8a2446bb28516219236bec3b494f5"}, {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf6c37483782e4785019b56e26224a25e9b9a35b849d0169ce69189867a22bb1"},
{file = "onnxruntime-1.18.0-cp39-cp39-win32.whl", hash = "sha256:6ffb445816d06497df7a6dd424b20e0b2c39639e01e7fe210e247b82d15a23b9"}, {file = "onnxruntime-1.17.3-cp39-cp39-win32.whl", hash = "sha256:351bf5a1140dcc43bfb8d3d1a230928ee61fcd54b0ea664c8e9a889a8e3aa515"},
{file = "onnxruntime-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:46de6031cb6745f33f7eca9e51ab73e8c66037fb7a3b6b4560887c5b55ab5d5d"}, {file = "onnxruntime-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:57a3de15778da8d6cc43fbf6cf038e1e746146300b5f0b1fbf01f6f795dc6440"},
] ]
[package.dependencies] [package.dependencies]
@@ -2000,21 +2000,21 @@ sympy = "*"
[[package]] [[package]]
name = "onnxruntime-gpu" name = "onnxruntime-gpu"
version = "1.18.0" version = "1.17.1"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models" description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "onnxruntime_gpu-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:039be9a6b7f71c6739e97eec79f4bf240793a7c0c4108a09e0e1a27b4c33dbca"}, {file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"},
{file = "onnxruntime_gpu-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:afd4bc090b9412ab695cb34c05f4f92f88dbb6bd52d9b38658ad0115c50ff653"}, {file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"},
{file = "onnxruntime_gpu-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2280f94c6be2717f010a73c30a94c2721af853c6b7110e83afa52d03de6614a8"}, {file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"},
{file = "onnxruntime_gpu-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:9e3b4e9a0171e53a71001805b9b0e1a98cbad5a413d795c0e132b0f058b386d6"}, {file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"},
{file = "onnxruntime_gpu-1.18.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:546fbf2dcb7a2830ca69bde0c38665a88a9454e923ebb76bedf85eaed33a6f4a"}, {file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"},
{file = "onnxruntime_gpu-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:24146aa670c45734d9b8583cd78bd790363bc8695a3808d129ec913186064e4c"}, {file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"},
{file = "onnxruntime_gpu-1.18.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:75884c51c2aa47c349de3b5485df7f9573e1b89c607dd55984d3fe40615ef002"}, {file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"},
{file = "onnxruntime_gpu-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:f3b00a7443252dbfbd18ff72bcc2f44066fad9128eaa29bff8b315a834241701"}, {file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"},
{file = "onnxruntime_gpu-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:14f53bd74ad21c61fee55eca988758e5eec4c39450040c8986ec3a960cb127a8"}, {file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"},
{file = "onnxruntime_gpu-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:aea5c02b3a0ee6682214a61a2a0467773401b075afdcb41dc2ef595f41c2d185"}, {file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"},
] ]
[package.dependencies] [package.dependencies]
@@ -2438,13 +2438,13 @@ files = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.2.1" version = "8.2.0"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
] ]
[package.dependencies] [package.dependencies]
@@ -2460,13 +2460,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]] [[package]]
name = "pytest-asyncio" name = "pytest-asyncio"
version = "0.23.7" version = "0.23.6"
description = "Pytest support for asyncio" description = "Pytest support for asyncio"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"},
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"},
] ]
[package.dependencies] [package.dependencies]
@@ -2799,28 +2799,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.4.7" version = "0.4.4"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"}, {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"}, {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"}, {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"}, {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"}, {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"}, {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
] ]
[[package]] [[package]]
@@ -3234,13 +3234,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.30.1" version = "0.29.0"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
] ]
[package.dependencies] [package.dependencies]

View File

@@ -62,8 +62,6 @@ fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER" echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
npm --prefix server version "$SERVER_PUMP" npm --prefix server version "$SERVER_PUMP"
npm --prefix server ci
npm --prefix server run build
make open-api make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP" npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP" npm --prefix web version "$SERVER_PUMP"

View File

@@ -1,3 +1,3 @@
{ {
"flutter": "3.22.1" "flutter": "3.22.0"
} }

View File

@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.22.1", "dart.flutterSdkPath": ".fvm/versions/3.22.0",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },

Some files were not shown because too many files have changed in this diff Show More