Compare commits
113 Commits
feat/serve
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48318168b | ||
|
|
dda736840b | ||
|
|
4c7347d653 | ||
|
|
dca420ef70 | ||
|
|
21bd20fd75 | ||
|
|
351dd647a9 | ||
|
|
298370b7be | ||
|
|
e3d39837d0 | ||
|
|
38f4a02a14 | ||
|
|
fc5615eff6 | ||
|
|
6879bcb7a4 | ||
|
|
11152f9b3d | ||
|
|
75830a4878 | ||
|
|
e7c8501930 | ||
|
|
50f9b2d44e | ||
|
|
9628ea2d24 | ||
|
|
4d4bb8b6a7 | ||
|
|
99f0aa868a | ||
|
|
459fee9ee4 | ||
|
|
871f3ea468 | ||
|
|
98c4c683ae | ||
|
|
8a7b0f66a4 | ||
|
|
9e71256191 | ||
|
|
d5cf8e4bfe | ||
|
|
95012dc19b | ||
|
|
f197f5d530 | ||
|
|
7168707395 | ||
|
|
847cb90038 | ||
|
|
602f0a3499 | ||
|
|
fdaa0e5413 | ||
|
|
39d2c4f37b | ||
|
|
1f5d82e9d9 | ||
|
|
e98744f222 | ||
|
|
56ea07bcba | ||
|
|
b3b258f32f | ||
|
|
69b5eb005f | ||
|
|
3f44a33eac | ||
|
|
b2a0422efb | ||
|
|
562c43b6f5 | ||
|
|
4f21f6a2e1 | ||
|
|
76fdcc9863 | ||
|
|
57d94bce68 | ||
|
|
832d728940 | ||
|
|
8bfa6769a5 | ||
|
|
e7aa50425c | ||
|
|
a5e8b451b2 | ||
|
|
13cbdf6851 | ||
|
|
967d195a05 | ||
|
|
8f37784eae | ||
|
|
ecd018a826 | ||
|
|
202745f14b | ||
|
|
6a4c2e97c0 | ||
|
|
f6f82a5662 | ||
|
|
ae21781442 | ||
|
|
06ce8247cc | ||
|
|
a4887bfa7e | ||
|
|
27a02c75dc | ||
|
|
f8ee977b9e | ||
|
|
a3e7e8cc31 | ||
|
|
a341ab0050 | ||
|
|
61b850f0ce | ||
|
|
a3489d604b | ||
|
|
00b5ad3421 | ||
|
|
2ada4a8426 | ||
|
|
924e9f08cd | ||
|
|
91b835cfeb | ||
|
|
bb79df655d | ||
|
|
02e755bd92 | ||
|
|
0963a32a95 | ||
|
|
6119618ae2 | ||
|
|
4d044658a0 | ||
|
|
67fa598f44 | ||
|
|
9222b9d130 | ||
|
|
d6757fc2dd | ||
|
|
4f838eabbe | ||
|
|
143b9d6828 | ||
|
|
1df7be8436 | ||
|
|
5f25f28c42 | ||
|
|
ae92422df7 | ||
|
|
84d824d6a7 | ||
|
|
4353153fe6 | ||
|
|
c37bf9d5d0 | ||
|
|
5c8c0b2f5b | ||
|
|
39129721fa | ||
|
|
451416ec88 | ||
|
|
e39ee8a16f | ||
|
|
1e56352b04 | ||
|
|
470c95d614 | ||
|
|
1ad04f0b17 | ||
|
|
60427f18ce | ||
|
|
9aed736911 | ||
|
|
6b369e84fc | ||
|
|
136bb69bd0 | ||
|
|
975f2351ec | ||
|
|
2e62c7b417 | ||
|
|
2689178a35 | ||
|
|
d61418886f | ||
|
|
c03981ac1d | ||
|
|
4807fc40a6 | ||
|
|
101bc290f9 | ||
|
|
df42352f84 | ||
|
|
c8aa6a62c2 | ||
|
|
85aca2bb54 | ||
|
|
ff52300624 | ||
|
|
936a46b4ed | ||
|
|
d8eca168ca | ||
|
|
64636c0618 | ||
|
|
673e97e71d | ||
|
|
984aa8fb41 | ||
|
|
7f0f016f2e | ||
|
|
0589575154 | ||
|
|
73bf8f343a | ||
|
|
581b467b4b |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,8 +2,6 @@ mobile/openapi/**/*.md -diff -merge
|
|||||||
mobile/openapi/**/*.md linguist-generated=true
|
mobile/openapi/**/*.md linguist-generated=true
|
||||||
mobile/openapi/**/*.dart -diff -merge
|
mobile/openapi/**/*.dart -diff -merge
|
||||||
mobile/openapi/**/*.dart linguist-generated=true
|
mobile/openapi/**/*.dart linguist-generated=true
|
||||||
mobile/openapi/.openapi-generator/FILES -diff -merge
|
|
||||||
mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
|
||||||
|
|
||||||
mobile/lib/**/*.g.dart -diff -merge
|
mobile/lib/**/*.g.dart -diff -merge
|
||||||
mobile/lib/**/*.g.dart linguist-generated=true
|
mobile/lib/**/*.g.dart linguist-generated=true
|
||||||
|
|||||||
2
.github/workflows/build-mobile.yml
vendored
2
.github/workflows/build-mobile.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version: '3.19.3'
|
flutter-version: '3.22.0'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
|
|||||||
15
.github/workflows/pr-require-conventional-commit.yml
vendored
Normal file
15
.github/workflows/pr-require-conventional-commit.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: PR Conventional Commit Validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr-title:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: PR Conventional Commit Validation
|
||||||
|
uses: ytanikin/PRConventionalCommits@1.2.0
|
||||||
|
with:
|
||||||
|
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
|
||||||
|
add_label: 'false'
|
||||||
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
@@ -22,8 +22,8 @@ jobs:
|
|||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: 'stable'
|
||||||
flutter-version: "3.19.3"
|
flutter-version: '3.22.0'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
|||||||
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -208,7 +208,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version: '3.19.3'
|
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
|
||||||
@@ -260,9 +260,18 @@ jobs:
|
|||||||
name: OpenAPI Clients
|
name: OpenAPI Clients
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout code
|
||||||
|
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
|
||||||
@@ -270,6 +279,8 @@ 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: |
|
||||||
@@ -332,7 +343,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: npm run sql:generate
|
run: npm run sync:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,6 +14,9 @@ mobile/gradle.properties
|
|||||||
mobile/openapi/pubspec.lock
|
mobile/openapi/pubspec.lock
|
||||||
mobile/*.jks
|
mobile/*.jks
|
||||||
mobile/libisar.dylib
|
mobile/libisar.dylib
|
||||||
|
mobile/openapi/test
|
||||||
|
mobile/openapi/doc
|
||||||
|
mobile/openapi/.openapi-generator/FILES
|
||||||
|
|
||||||
open-api/typescript-sdk/build
|
open-api/typescript-sdk/build
|
||||||
mobile/android/fastlane/report.xml
|
mobile/android/fastlane/report.xml
|
||||||
|
|||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -1,6 +1,16 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"[javascript][typescript][css]": {
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -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 sql:generate
|
npm --prefix server run sync:sql
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|||||||
270
cli/package-lock.json
generated
270
cli/package-lock.json
generated
@@ -31,7 +31,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^53.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.12.12",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -174,9 +174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.22.20",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
|
||||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1113,12 +1113,6 @@
|
|||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
|
||||||
"version": "7.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.0",
|
"version": "4.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
|
||||||
@@ -1144,10 +1138,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.11",
|
"version": "20.12.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||||
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@@ -1158,28 +1153,21 @@
|
|||||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
|
||||||
"version": "7.5.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
|
||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
|
||||||
"integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
|
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@typescript-eslint/type-utils": "7.8.0",
|
"@typescript-eslint/type-utils": "7.9.0",
|
||||||
"@typescript-eslint/utils": "7.8.0",
|
"@typescript-eslint/utils": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0",
|
"@typescript-eslint/visitor-keys": "7.9.0",
|
||||||
"debug": "^4.3.4",
|
|
||||||
"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",
|
||||||
"semver": "^7.6.0",
|
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^1.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1200,15 +1188,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
|
||||||
"integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
|
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0",
|
"@typescript-eslint/visitor-keys": "7.9.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1228,13 +1217,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
|
||||||
"integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
|
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0"
|
"@typescript-eslint/visitor-keys": "7.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
@@ -1245,13 +1235,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
|
||||||
"integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
|
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0",
|
||||||
"@typescript-eslint/utils": "7.8.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"
|
||||||
},
|
},
|
||||||
@@ -1272,10 +1263,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
|
||||||
"integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
|
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -1285,13 +1277,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
|
||||||
"integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
|
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.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",
|
||||||
@@ -1313,18 +1306,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
|
||||||
"integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
|
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@types/semver": "^7.5.8",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0"
|
||||||
"@typescript-eslint/types": "7.8.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
|
||||||
"semver": "^7.6.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
@@ -1338,12 +1329,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
|
||||||
"integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
|
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1564,6 +1556,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1588,6 +1581,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -1822,12 +1816,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.36.0",
|
"version": "3.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||||
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
|
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.22.3"
|
"browserslist": "^4.23.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1897,6 +1891,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -2094,17 +2089,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "52.0.0",
|
"version": "53.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
|
||||||
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
|
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.22.20",
|
"@babel/helper-validator-identifier": "^7.24.5",
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@eslint/eslintrc": "^2.1.4",
|
"@eslint/eslintrc": "^3.0.2",
|
||||||
"ci-info": "^4.0.0",
|
"ci-info": "^4.0.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.34.0",
|
"core-js-compat": "^3.37.0",
|
||||||
"esquery": "^1.5.0",
|
"esquery": "^1.5.0",
|
||||||
"indent-string": "^4.0.0",
|
"indent-string": "^4.0.0",
|
||||||
"is-builtin-module": "^3.2.1",
|
"is-builtin-module": "^3.2.1",
|
||||||
@@ -2113,11 +2108,11 @@
|
|||||||
"read-pkg-up": "^7.0.1",
|
"read-pkg-up": "^7.0.1",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.10.0",
|
"regjsparser": "^0.10.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.6.1",
|
||||||
"strip-indent": "^3.0.0"
|
"strip-indent": "^3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18.18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
@@ -2126,6 +2121,92 @@
|
|||||||
"eslint": ">=8.56.0"
|
"eslint": ">=8.56.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^6.12.4",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"espree": "^10.0.1",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"ignore": "^5.2.0",
|
||||||
|
"import-fresh": "^3.2.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"minimatch": "^3.1.2",
|
||||||
|
"strip-json-comments": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.3",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"eslint-visitor-keys": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
@@ -2466,6 +2547,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"array-union": "^2.1.0",
|
"array-union": "^2.1.0",
|
||||||
"dir-glob": "^3.0.1",
|
"dir-glob": "^3.0.1",
|
||||||
@@ -2969,6 +3051,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -3232,6 +3315,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -3711,13 +3795,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
@@ -3725,18 +3806,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -3781,6 +3850,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -4407,12 +4477,6 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^53.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getMyUserInfo } from '@immich/sdk';
|
import { getMyUser } 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, userInfo] = await withError(getMyUserInfo());
|
const [error, user] = await withError(getMyUser());
|
||||||
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 ${userInfo.email}`);
|
console.log(`Logged in as ${user.email}`);
|
||||||
|
|
||||||
if (!existsSync(configDir)) {
|
if (!existsSync(configDir)) {
|
||||||
// Create config folder if it doesn't exist
|
// Create config folder if it doesn't exist
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
import { getAssetStatistics, getMyUser, 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({}),
|
||||||
getMyUserInfo(),
|
getMyUser(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(`Server Info (via ${userInfo.email})`);
|
console.log(`Server Info (via ${userInfo.email})`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
|
import { getMyUser, 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';
|
||||||
@@ -46,10 +46,9 @@ export const connect = async (url: string, key: string) => {
|
|||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults.baseUrl = url;
|
init({ baseUrl: url, apiKey: key });
|
||||||
defaults.headers = { 'x-api-key': key };
|
|
||||||
|
|
||||||
const [error] = await withError(getMyUserInfo());
|
const [error] = await withError(getMyUser());
|
||||||
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);
|
||||||
|
|||||||
@@ -4,32 +4,32 @@
|
|||||||
|
|
||||||
name: immich-dev
|
name: immich-dev
|
||||||
|
|
||||||
x-server-build: &server-common
|
|
||||||
image: immich-server-dev:latest
|
|
||||||
build:
|
|
||||||
context: ../
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
target: dev
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ../server:/usr/src/app
|
|
||||||
- ../open-api:/usr/src/open-api
|
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
|
||||||
- /usr/src/app/node_modules
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ulimits:
|
|
||||||
nofile:
|
|
||||||
soft: 1048576
|
|
||||||
hard: 1048576
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
<<: *server-common
|
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:
|
||||||
|
context: ../
|
||||||
|
dockerfile: server/Dockerfile
|
||||||
|
target: dev
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- ../open-api:/usr/src/open-api
|
||||||
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 1048576
|
||||||
|
hard: 1048576
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
- 9230:9230
|
- 9230:9230
|
||||||
@@ -37,19 +37,6 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
container_name: immich_microservices
|
|
||||||
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
|
||||||
<<: *server-common
|
|
||||||
# extends:
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
|
||||||
ports:
|
|
||||||
- 9231:9230
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
- immich-server
|
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
container_name: immich_web
|
container_name: immich_web
|
||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
@@ -97,7 +84,9 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -113,6 +102,11 @@ 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
|
||||||
|
|||||||
@@ -1,39 +1,26 @@
|
|||||||
name: immich-prod
|
name: immich-prod
|
||||||
|
|
||||||
x-server-build: &server-common
|
|
||||||
image: immich-server:latest
|
|
||||||
build:
|
|
||||||
context: ../
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['start.sh', 'immich']
|
image: immich-server:latest
|
||||||
<<: *server-common
|
# extends:
|
||||||
|
# file: hwaccel.transcoding.yml
|
||||||
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: server/Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
restart: always
|
||||||
immich-microservices:
|
|
||||||
container_name: immich_microservices
|
|
||||||
command: ['start.sh', 'microservices']
|
|
||||||
<<: *server-common
|
|
||||||
# extends:
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
- immich-server
|
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
@@ -54,7 +41,9 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -71,7 +60,13 @@ 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:
|
||||||
@@ -90,7 +85,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
|
image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ 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}
|
||||||
command: ['start.sh', 'immich']
|
# 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
|
||||||
@@ -25,23 +27,6 @@ services:
|
|||||||
- database
|
- database
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
container_name: immich_microservices
|
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
|
||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
|
||||||
command: ['start.sh', 'microservices']
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
||||||
@@ -58,12 +43,14 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
@@ -71,8 +58,13 @@ 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
|
||||||
restart: always
|
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
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
# Configurations for hardware-accelerated machine learning
|
# Configurations for hardware-accelerated machine learning
|
||||||
|
|
||||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||||
@@ -30,7 +28,7 @@ services:
|
|||||||
|
|
||||||
openvino:
|
openvino:
|
||||||
device_cgroup_rules:
|
device_cgroup_rules:
|
||||||
- "c 189:* rmw"
|
- 'c 189:* rmw'
|
||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
# Configurations for hardware-accelerated transcoding
|
# Configurations for hardware-accelerated transcoding
|
||||||
|
|
||||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn start
|
$ npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
|
|||||||
### Build
|
### Build
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn build
|
$ npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
|
|||||||
Using SSH:
|
Using SSH:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ USE_SSH=true yarn deploy
|
$ USE_SSH=true npm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
Not using SSH:
|
Not using SSH:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
$ GIT_USER=<Your GitHub username> npm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||||
|
|||||||
BIN
docs/docs/administration/img/google-example.webp
Normal file
BIN
docs/docs/administration/img/google-example.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/docs/administration/img/immich-google-example.webp
Normal file
BIN
docs/docs/administration/img/immich-google-example.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
@@ -110,8 +110,44 @@ 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:
|
||||||
|
|
||||||

|
<img src={require('./img/oauth-settings.png').default} title="OAuth settings" />
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Google Example</summary>
|
||||||
|
|
||||||
|
### Google Example
|
||||||
|
|
||||||
|
Configuration of Authorised redirect URIs (Google Console)
|
||||||
|
|
||||||
|
<img src={require('./img/google-example.webp').default} width='50%' title="Authorised redirect URIs" />
|
||||||
|
|
||||||
|
Configuration of OAuth in 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/
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ In any other situation, there are 3 different options that can appear:
|
|||||||
|
|
||||||
- MATCHES - These files are matched by their checksums.
|
- MATCHES - These files are matched by their checksums.
|
||||||
|
|
||||||
- OFFLINE PATHS - These files are the result of manually deleting files in the upload library or a failed file move in the past (losing track of a file).
|
- OFFLINE PATHS - These files are the result of manually deleting files from immich or a failed file move in the past (losing track of a file).
|
||||||
|
|
||||||
- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug.
|
- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
|
|||||||
- WSL2 does not support Quick Sync.
|
- WSL2 does not support Quick Sync.
|
||||||
- Raspberry Pi is currently not supported.
|
- Raspberry Pi is currently not supported.
|
||||||
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
|
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
|
||||||
- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding and tone-mapping.
|
- By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping.
|
||||||
|
- NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings.
|
||||||
- Hardware dependent
|
- Hardware dependent
|
||||||
- Codec support varies, but H.264 and HEVC are usually supported.
|
- Codec support varies, but H.264 and HEVC are usually supported.
|
||||||
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
|
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
|
||||||
@@ -65,6 +66,7 @@ For RKMPP to work:
|
|||||||
|
|
||||||
3. Redeploy the `immich-microservices` container with these updated settings.
|
3. Redeploy the `immich-microservices` container with these updated settings.
|
||||||
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
|
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
|
||||||
|
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
|
||||||
|
|
||||||
#### Single Compose File
|
#### Single Compose File
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
|
|
||||||
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
|
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
|
||||||
|
|
||||||
## The Upload Library
|
|
||||||
|
|
||||||
Immich comes preconfigured with an upload library for each user. All assets uploaded to Immich are added to this library. This library can be renamed, but not deleted. The upload library is the only library that can be synced with a mobile device. No items in an upload library is allowed to have the same sha1 hash as another item in the same library in order to prevent duplicates.
|
|
||||||
|
|
||||||
## External Libraries
|
## External Libraries
|
||||||
|
|
||||||
External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
|
External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ immich-machine-learning:
|
|||||||
Once this is done, you can redeploy the `immich-machine-learning` container.
|
Once this is done, you can redeploy the `immich-machine-learning` container.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
|
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `IMMICH_LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
|
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
|
||||||
|
|||||||
@@ -2,48 +2,52 @@
|
|||||||
|
|
||||||
A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
|
A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
|
||||||
|
|
||||||
:::note
|
|
||||||
|
|
||||||
In order to connect to the database the immich_postgres container **must be running**.
|
|
||||||
|
|
||||||
The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
|
|
||||||
|
|
||||||
**Optional:** To connect to the database **outside** of your Docker's network:
|
|
||||||
|
|
||||||
- Expose port 5432 in your `docker-compose.yml` file.
|
|
||||||
- Edit the PostgreSQL [`pg_hba.conf`](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) file.
|
|
||||||
- Make sure your firewall does not block access to port 5432.
|
|
||||||
Note that exposing the database port increases the risk of getting attacked by hackers.
|
|
||||||
Make sure to remove the binding port after finishing the database's tasks.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 1. Install pgAdmin
|
## 1. Install pgAdmin
|
||||||
|
|
||||||
Download and install [pgAdmin](https://www.pgadmin.org/download/) following the official documentation.
|
Add a file `docker-compose-pgadmin.yml` next to your `docker-compose.yml` with the following content:
|
||||||
|
|
||||||
|
```
|
||||||
|
name: immich
|
||||||
|
|
||||||
|
services:
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
container_name: pgadmin4_container
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8888:80"
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: strong-password
|
||||||
|
volumes:
|
||||||
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgadmin-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` in this file.
|
||||||
|
|
||||||
|
Run `docker compose -f docker-compose.yml -f docker-compose-pgadmin.yml up` to start immich along with `pgAdmin`.
|
||||||
|
|
||||||
## 2. Add a Server
|
## 2. Add a Server
|
||||||
|
|
||||||
Open pgAdmin and click "Add New Server".
|
Open [localhost:8888](http://localhost:8888) and login with the default credentials from above.
|
||||||
|
|
||||||
<img src={require('./img/add-new-server-option.png').default} width="50%" title="new server option" />
|
Right click on `Servers` and click on `Register >> Server..` then enter the values below in the `Connection` tab.
|
||||||
|
|
||||||
## 3. Enter Connection Details
|
<img src={require('./img/pgadmin-add-new-server.png').default} width="50%" title="new server option" />
|
||||||
|
|
||||||
| Name | Value |
|
:::note
|
||||||
| -------------------- | ----------- |
|
The parameters used here match those specified in the example `.env` file. If you have changed your `.env` file, you'll need to adjust accordingly.
|
||||||
| Host name/address | `localhost` |
|
:::
|
||||||
| Port | `5432` |
|
|
||||||
| Maintenance database | `immich` |
|
|
||||||
| Username | `postgres` |
|
|
||||||
| Password | `postgres` |
|
|
||||||
|
|
||||||
<img src={require('./img/Connection-Pgadmin.png').default} width="75%" title="Connection" />
|
| Name | Value |
|
||||||
|
| -------------------- | ----------------- |
|
||||||
## 4. Save Connection
|
| Host name/address | `immich_postgres` |
|
||||||
|
| Port | `5432` |
|
||||||
|
| Maintenance database | `immich` |
|
||||||
|
| Username | `postgres` |
|
||||||
|
| Password | `postgres` |
|
||||||
|
|
||||||
Click on "Save" to connect to the Immich database.
|
Click on "Save" to connect to the Immich database.
|
||||||
|
|
||||||
:::tip
|
|
||||||
View [Database Queries](/docs/guides/database-queries/) for common database queries.
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ SELECT * FROM "users";
|
|||||||
## System Config
|
## System Config
|
||||||
|
|
||||||
```sql title="Custom settings"
|
```sql title="Custom settings"
|
||||||
SELECT "key", "value" FROM "system_config";
|
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
|
||||||
```
|
```
|
||||||
|
|
||||||
(Only used when not using the [config file](/docs/install/config-file))
|
(Only used when not using the [config file](/docs/install/config-file))
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
BIN
docs/docs/guides/img/pgadmin-add-new-server.png
Normal file
BIN
docs/docs/guides/img/pgadmin-add-new-server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
name: immich_remote_ml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ The default configuration looks like this:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"modelName": "ViT-B-32__openai"
|
"modelName": "ViT-B-32__openai"
|
||||||
},
|
},
|
||||||
|
"duplicateDetection": {
|
||||||
|
"enabled": false,
|
||||||
|
"maxDistance": 0.03
|
||||||
|
},
|
||||||
"facialRecognition": {
|
"facialRecognition": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"modelName": "buffalo_l",
|
"modelName": "buffalo_l",
|
||||||
@@ -153,9 +157,6 @@ The default configuration looks like this:
|
|||||||
"server": {
|
"server": {
|
||||||
"externalDomain": "",
|
"externalDomain": "",
|
||||||
"loginPageMessage": ""
|
"loginPageMessage": ""
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"deleteDelay": 7
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
|||||||
| Variable | Description | Default | Services |
|
| Variable | Description | Default | Services |
|
||||||
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
|
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
|
||||||
| `TZ` | Timezone | | microservices |
|
| `TZ` | Timezone | | microservices |
|
||||||
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
|
||||||
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
|
| `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, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
||||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
||||||
@@ -59,13 +59,10 @@ It only need to be set if the Immich deployment method is changing.
|
|||||||
|
|
||||||
## Ports
|
## Ports
|
||||||
|
|
||||||
| Variable | Description | Default | Services |
|
| Variable | Description | Default |
|
||||||
| :---------------------- | :-------------------- | :-------: | :-------------------- |
|
| :------------ | :------------- | :------------------------------------: |
|
||||||
| `HOST` | Host | `0.0.0.0` | server, microservices |
|
| `IMMICH_HOST` | Listening host | `0.0.0.0` |
|
||||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
| `IMMICH_PORT` | Listening port | 3001 (server), 3003 (machine learning) |
|
||||||
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
|
|
||||||
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
|
|
||||||
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
|
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ interface CommunityGuidesProps {
|
|||||||
const guides: CommunityGuidesProps[] = [
|
const guides: CommunityGuidesProps[] = [
|
||||||
{
|
{
|
||||||
title: 'Cloudflare Tunnels with SSO/OAuth',
|
title: 'Cloudflare Tunnels with SSO/OAuth',
|
||||||
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
|
description: `Setting up Cloudflare Tunnels and a SaaS App for Immich.`,
|
||||||
url: 'https://github.com/immich-app/immich/discussions/8299',
|
url: 'https://github.com/immich-app/immich/discussions/8299',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Database backup in Truenas',
|
title: 'Database backup in TrueNAS',
|
||||||
description: `Create a database backup with pgAdmin in Truenas.`,
|
description: `Create a database backup with pgAdmin in TrueNAS.`,
|
||||||
url: 'https://github.com/immich-app/immich/discussions/8809',
|
url: 'https://github.com/immich-app/immich/discussions/8809',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Unraid backup scripts',
|
title: 'Unraid backup scripts',
|
||||||
description: `Back up your assets in Unarid with a pre-prepared script.`,
|
description: `Back up your assets in Unraid with a pre-prepared script.`,
|
||||||
url: 'https://github.com/immich-app/immich/discussions/8416',
|
url: 'https://github.com/immich-app/immich/discussions/8416',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface CommunityProjectProps {
|
|||||||
const projects: CommunityProjectProps[] = [
|
const projects: CommunityProjectProps[] = [
|
||||||
{
|
{
|
||||||
title: 'immich-go',
|
title: 'immich-go',
|
||||||
description: `An alternative to the immich-CLI command that doesn't depend on nodejs installation. It tries its best for importing google photos takeout archives.`,
|
description: `An alternative to the immich-CLI that doesn't depend on nodejs. It specializes in importing Google Photos Takeout archives.`,
|
||||||
url: 'https://github.com/simulot/immich-go',
|
url: 'https://github.com/simulot/immich-go',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,40 +2,32 @@ version: '3.8'
|
|||||||
|
|
||||||
name: immich-e2e
|
name: immich-e2e
|
||||||
|
|
||||||
x-server-build: &server-common
|
|
||||||
image: immich-server:latest
|
|
||||||
build:
|
|
||||||
context: ../
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
environment:
|
|
||||||
- DB_HOSTNAME=database
|
|
||||||
- DB_USERNAME=postgres
|
|
||||||
- DB_PASSWORD=postgres
|
|
||||||
- DB_DATABASE_NAME=immich
|
|
||||||
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
|
||||||
- IMMICH_METRICS=true
|
|
||||||
volumes:
|
|
||||||
- upload:/usr/src/app/upload
|
|
||||||
- ./test-assets:/test-assets
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
container_name: immich-e2e-server
|
||||||
command: ['./start.sh', 'immich']
|
command: ['./start.sh']
|
||||||
<<: *server-common
|
image: immich-server:latest
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: server/Dockerfile
|
||||||
|
environment:
|
||||||
|
- DB_HOSTNAME=database
|
||||||
|
- DB_USERNAME=postgres
|
||||||
|
- DB_PASSWORD=postgres
|
||||||
|
- DB_DATABASE_NAME=immich
|
||||||
|
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
||||||
|
- IMMICH_METRICS=true
|
||||||
|
volumes:
|
||||||
|
- upload:/usr/src/app/upload
|
||||||
|
- ./test-assets:/test-assets
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
container_name: immich-e2e-microservices
|
|
||||||
command: ['./start.sh', 'microservices']
|
|
||||||
<<: *server-common
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
|
|||||||
275
e2e/package-lock.json
generated
275
e2e/package-lock.json
generated
@@ -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.41.2",
|
"@playwright/test": "^1.44.1",
|
||||||
"@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",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^53.0.0",
|
||||||
"exiftool-vendored": "^26.0.0",
|
"exiftool-vendored": "^26.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^53.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
@@ -208,9 +208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.22.20",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
|
||||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -971,12 +971,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.44.0",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
|
||||||
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
|
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.44.0"
|
"playwright": "1.44.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -1217,12 +1217,6 @@
|
|||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
|
||||||
"version": "7.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||||
@@ -1236,10 +1230,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.11",
|
"version": "20.12.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||||
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@@ -1327,12 +1322,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
|
||||||
"version": "7.5.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
|
||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/superagent": {
|
"node_modules/@types/superagent": {
|
||||||
"version": "8.1.3",
|
"version": "8.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz",
|
||||||
@@ -1355,21 +1344,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
|
||||||
"integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
|
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@typescript-eslint/type-utils": "7.8.0",
|
"@typescript-eslint/type-utils": "7.9.0",
|
||||||
"@typescript-eslint/utils": "7.8.0",
|
"@typescript-eslint/utils": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0",
|
"@typescript-eslint/visitor-keys": "7.9.0",
|
||||||
"debug": "^4.3.4",
|
|
||||||
"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",
|
||||||
"semver": "^7.6.0",
|
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^1.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1390,15 +1378,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
|
||||||
"integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
|
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0",
|
"@typescript-eslint/visitor-keys": "7.9.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1418,13 +1407,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
|
||||||
"integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
|
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.0"
|
"@typescript-eslint/visitor-keys": "7.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
@@ -1435,13 +1425,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
|
||||||
"integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
|
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0",
|
||||||
"@typescript-eslint/utils": "7.8.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"
|
||||||
},
|
},
|
||||||
@@ -1462,10 +1453,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
|
||||||
"integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
|
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -1475,13 +1467,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
|
||||||
"integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
|
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/visitor-keys": "7.8.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",
|
||||||
@@ -1507,6 +1500,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -1516,6 +1510,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -1527,18 +1522,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
|
||||||
"integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
|
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@typescript-eslint/scope-manager": "7.9.0",
|
||||||
"@types/semver": "^7.5.8",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"@typescript-eslint/scope-manager": "7.8.0",
|
"@typescript-eslint/typescript-estree": "7.9.0"
|
||||||
"@typescript-eslint/types": "7.8.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "7.8.0",
|
|
||||||
"semver": "^7.6.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || >=20.0.0"
|
"node": "^18.18.0 || >=20.0.0"
|
||||||
@@ -1552,12 +1545,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.8.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
|
||||||
"integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
|
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.8.0",
|
"@typescript-eslint/types": "7.9.0",
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1785,6 +1779,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1840,6 +1835,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.0.1"
|
||||||
},
|
},
|
||||||
@@ -2121,12 +2117,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.36.0",
|
"version": "3.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||||
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
|
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.22.3"
|
"browserslist": "^4.23.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2247,6 +2243,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -2487,17 +2484,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "52.0.0",
|
"version": "53.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
|
||||||
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
|
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.22.20",
|
"@babel/helper-validator-identifier": "^7.24.5",
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@eslint/eslintrc": "^2.1.4",
|
"@eslint/eslintrc": "^3.0.2",
|
||||||
"ci-info": "^4.0.0",
|
"ci-info": "^4.0.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.34.0",
|
"core-js-compat": "^3.37.0",
|
||||||
"esquery": "^1.5.0",
|
"esquery": "^1.5.0",
|
||||||
"indent-string": "^4.0.0",
|
"indent-string": "^4.0.0",
|
||||||
"is-builtin-module": "^3.2.1",
|
"is-builtin-module": "^3.2.1",
|
||||||
@@ -2506,11 +2503,11 @@
|
|||||||
"read-pkg-up": "^7.0.1",
|
"read-pkg-up": "^7.0.1",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.10.0",
|
"regjsparser": "^0.10.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.6.1",
|
||||||
"strip-indent": "^3.0.0"
|
"strip-indent": "^3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18.18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
@@ -2519,6 +2516,70 @@
|
|||||||
"eslint": ">=8.56.0"
|
"eslint": ">=8.56.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^6.12.4",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"espree": "^10.0.1",
|
||||||
|
"globals": "^14.0.0",
|
||||||
|
"ignore": "^5.2.0",
|
||||||
|
"import-fresh": "^3.2.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"minimatch": "^3.1.2",
|
||||||
|
"strip-json-comments": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.3",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"eslint-visitor-keys": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
@@ -2692,6 +2753,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
@@ -2708,6 +2770,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
},
|
},
|
||||||
@@ -2759,6 +2822,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -3001,6 +3065,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"array-union": "^2.1.0",
|
"array-union": "^2.1.0",
|
||||||
"dir-glob": "^3.0.1",
|
"dir-glob": "^3.0.1",
|
||||||
@@ -3276,6 +3341,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
@@ -3491,18 +3557,6 @@
|
|||||||
"get-func-name": "^2.0.1"
|
"get-func-name": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.4.4",
|
"version": "3.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||||
@@ -3561,6 +3615,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
@@ -3579,6 +3634,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.2",
|
"braces": "^3.0.2",
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
@@ -4047,6 +4103,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -4175,6 +4232,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
@@ -4194,12 +4252,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.44.0",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
|
||||||
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
|
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.44.0"
|
"playwright-core": "1.44.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -4212,9 +4270,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.44.0",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
|
||||||
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
|
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
@@ -4710,13 +4768,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
@@ -4809,6 +4864,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -5135,6 +5191,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.41.2",
|
"@playwright/test": "^1.44.1",
|
||||||
"@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",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^53.0.0",
|
||||||
"exiftool-vendored": "^26.0.0",
|
"exiftool-vendored": "^26.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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('/activity', () => {
|
describe('/activities', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonOwner: LoginResponseDto;
|
let nonOwner: LoginResponseDto;
|
||||||
let asset: AssetFileUploadResponseDto;
|
let asset: AssetFileUploadResponseDto;
|
||||||
@@ -45,22 +45,24 @@ describe('/activity', () => {
|
|||||||
await utils.resetDatabase(['activity']);
|
await utils.resetDatabase(['activity']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /activity', () => {
|
describe('GET /activities', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/activity');
|
const { status, body } = await request(app).get('/activities');
|
||||||
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).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app)
|
||||||
|
.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('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -69,7 +71,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -78,7 +80,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.get('/activities')
|
||||||
.query({ albumId: album.id })
|
.query({ albumId: album.id })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual([]);
|
||||||
@@ -102,7 +104,7 @@ describe('/activity', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -121,7 +123,7 @@ describe('/activity', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -140,7 +142,7 @@ describe('/activity', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -152,7 +154,7 @@ describe('/activity', () => {
|
|||||||
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('/activity')
|
.get('/activities')
|
||||||
.query({ albumId: album.id, userId: uuidDto.notFound })
|
.query({ albumId: album.id, userId: uuidDto.notFound })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ describe('/activity', () => {
|
|||||||
expect(response1.body.length).toBe(0);
|
expect(response1.body.length).toBe(0);
|
||||||
|
|
||||||
const response2 = await request(app)
|
const response2 = await request(app)
|
||||||
.get('/activity')
|
.get('/activities')
|
||||||
.query({ albumId: album.id, userId: admin.userId })
|
.query({ albumId: album.id, userId: admin.userId })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
@@ -180,7 +182,7 @@ describe('/activity', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/activity')
|
.get('/activities')
|
||||||
.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);
|
||||||
@@ -189,16 +191,16 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /activity', () => {
|
describe('POST /activities', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/activity');
|
const { status, body } = await request(app).post('/activities');
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -207,7 +209,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -216,7 +218,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -236,7 +238,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -253,7 +255,7 @@ describe('/activity', () => {
|
|||||||
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('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -267,7 +269,7 @@ describe('/activity', () => {
|
|||||||
type: ReactionType.Like,
|
type: ReactionType.Like,
|
||||||
});
|
});
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -276,7 +278,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -297,7 +299,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
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('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -319,7 +321,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/activity')
|
.post('/activities')
|
||||||
.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);
|
||||||
@@ -327,16 +329,16 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /activity/:id', () => {
|
describe('DELETE /activities/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
|
const { status, body } = await request(app).delete(`/activities/${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(`/activity/${uuidDto.invalid}`)
|
.delete(`/activities/${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']));
|
||||||
@@ -349,7 +351,7 @@ describe('/activity', () => {
|
|||||||
comment: 'This is a test comment',
|
comment: 'This is a test comment',
|
||||||
});
|
});
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/activity/${reaction.id}`)
|
.delete(`/activities/${reaction.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(204);
|
expect(status).toEqual(204);
|
||||||
});
|
});
|
||||||
@@ -360,7 +362,7 @@ describe('/activity', () => {
|
|||||||
type: ReactionType.Like,
|
type: ReactionType.Like,
|
||||||
});
|
});
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/activity/${reaction.id}`)
|
.delete(`/activities/${reaction.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(204);
|
expect(status).toEqual(204);
|
||||||
});
|
});
|
||||||
@@ -373,7 +375,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/activity/${reaction.id}`)
|
.delete(`/activities/${reaction.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toEqual(204);
|
expect(status).toEqual(204);
|
||||||
@@ -387,7 +389,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/activity/${reaction.id}`)
|
.delete(`/activities/${reaction.id}`)
|
||||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
@@ -405,7 +407,7 @@ describe('/activity', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/activity/${reaction.id}`)
|
.delete(`/activities/${reaction.id}`)
|
||||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetFileUploadResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
AssetOrder,
|
AssetOrder,
|
||||||
deleteUser,
|
deleteUserAdmin,
|
||||||
getAlbumInfo,
|
getAlbumInfo,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
@@ -23,7 +23,7 @@ const user2SharedUser = 'user2SharedUser';
|
|||||||
const user2SharedLink = 'user2SharedLink';
|
const user2SharedLink = 'user2SharedLink';
|
||||||
const user2NotShared = 'user2NotShared';
|
const user2NotShared = 'user2NotShared';
|
||||||
|
|
||||||
describe('/album', () => {
|
describe('/albums', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user1Asset1: AssetFileUploadResponseDto;
|
let user1Asset1: AssetFileUploadResponseDto;
|
||||||
@@ -107,19 +107,19 @@ describe('/album', () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /album', () => {
|
describe('GET /albums', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/album');
|
const { status, body } = await request(app).get('/albums');
|
||||||
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('/album?shared=invalid')
|
.get('/albums?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('/album', () => {
|
|||||||
|
|
||||||
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('/album?assetId=invalid')
|
.get('/albums?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('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
|
|
||||||
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('/album?shared=true')
|
.get('/albums?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('/album', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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('/album').set('Authorization', `Bearer ${user1.accessToken}`);
|
const { status, body } = await request(app).get('/albums').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('/album', () => {
|
|||||||
|
|
||||||
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('/album?shared=true')
|
.get('/albums?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('/album', () => {
|
|||||||
|
|
||||||
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('/album?shared=false')
|
.get('/albums?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('/album', () => {
|
|||||||
|
|
||||||
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(`/album?assetId=${user1Asset2.id}`)
|
.get(`/albums?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('/album', () => {
|
|||||||
|
|
||||||
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(`/album?shared=true&assetId=${user1Asset1.id}`)
|
.get(`/albums?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('/album', () => {
|
|||||||
|
|
||||||
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(`/album?shared=false&assetId=${user1Asset1.id}`)
|
.get(`/albums?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 /album/:id', () => {
|
describe('GET /albums/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
|
const { status, body } = await request(app).get(`/albums/${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(`/album/${user1Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user2Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[3].id}?withoutAssets=false`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}?withoutAssets=true`)
|
.get(`/albums/${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('/album', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /album/count', () => {
|
describe('GET /albums/count', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/album/count');
|
const { status, body } = await request(app).get('/albums/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('/album/count')
|
.get('/albums/count')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -361,16 +361,16 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /album', () => {
|
describe('POST /albums', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
|
const { status, body } = await request(app).post('/albums').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('/album')
|
.post('/albums')
|
||||||
.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,7 +383,6 @@ describe('/album', () => {
|
|||||||
description: '',
|
description: '',
|
||||||
albumThumbnailAssetId: null,
|
albumThumbnailAssetId: null,
|
||||||
shared: false,
|
shared: false,
|
||||||
sharedUsers: [],
|
|
||||||
albumUsers: [],
|
albumUsers: [],
|
||||||
hasSharedLink: false,
|
hasSharedLink: false,
|
||||||
assets: [],
|
assets: [],
|
||||||
@@ -395,9 +394,9 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /album/:id/assets', () => {
|
describe('PUT /albums/:id/assets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
|
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -405,7 +404,7 @@ describe('/album', () => {
|
|||||||
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(`/album/${user1Albums[0].id}/assets`)
|
.put(`/albums/${user1Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
@@ -416,7 +415,7 @@ describe('/album', () => {
|
|||||||
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(`/album/${user2Albums[0].id}/assets`)
|
.put(`/albums/${user2Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
@@ -427,7 +426,7 @@ describe('/album', () => {
|
|||||||
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(`/album/${user1Albums[3].id}/assets`)
|
.put(`/albums/${user1Albums[3].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
@@ -438,7 +437,7 @@ describe('/album', () => {
|
|||||||
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(`/album/${user1Albums[0].id}/assets`)
|
.put(`/albums/${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] });
|
||||||
|
|
||||||
@@ -450,10 +449,10 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /album/:id', () => {
|
describe('PATCH /albums/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.patch(`/album/${uuidDto.notFound}`)
|
.patch(`/albums/${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);
|
||||||
@@ -464,7 +463,7 @@ describe('/album', () => {
|
|||||||
albumName: 'New album',
|
albumName: 'New album',
|
||||||
});
|
});
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.patch(`/album/${album.id}`)
|
.patch(`/albums/${album.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
albumName: 'New album name',
|
albumName: 'New album name',
|
||||||
@@ -481,7 +480,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[3].id}`)
|
.patch(`/albums/${user1Albums[3].id}`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ albumName: 'New album name' });
|
.send({ albumName: 'New album name' });
|
||||||
|
|
||||||
@@ -491,7 +490,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}`)
|
.patch(`/albums/${user1Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ albumName: 'New album name' });
|
.send({ albumName: 'New album name' });
|
||||||
|
|
||||||
@@ -500,10 +499,10 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /album/:id/assets', () => {
|
describe('DELETE /albums/: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(`/album/${user1Albums[0].id}/assets`)
|
.delete(`/albums/${user1Albums[0].id}/assets`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@@ -512,7 +511,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user2Albums[0].id}/assets`)
|
.delete(`/albums/${user2Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
@@ -528,7 +527,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}/assets`)
|
.delete(`/albums/${user1Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
@@ -544,7 +543,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[0].id}/assets`)
|
.delete(`/albums/${user1Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
@@ -554,7 +553,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user2Albums[0].id}/assets`)
|
.delete(`/albums/${user2Albums[0].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
@@ -564,7 +563,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[3].id}/assets`)
|
.delete(`/albums/${user1Albums[3].id}/assets`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
@@ -574,7 +573,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${user1Albums[1].id}/assets`)
|
.delete(`/albums/${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] });
|
||||||
|
|
||||||
@@ -596,7 +595,7 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -604,21 +603,25 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${album.id}/users`)
|
.put(`/albums/${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({
|
||||||
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
albumUsers: [
|
||||||
|
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(`/album/${album.id}/users`)
|
.put(`/albums/${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 }] });
|
||||||
|
|
||||||
@@ -628,12 +631,12 @@ describe('/album', () => {
|
|||||||
|
|
||||||
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(`/album/${album.id}/users`)
|
.put(`/albums/${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(`/album/${album.id}/users`)
|
.put(`/albums/${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 }] });
|
||||||
|
|
||||||
@@ -652,14 +655,16 @@ describe('/album', () => {
|
|||||||
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(`/album/${album.id}/user/${user2.userId}`)
|
.put(`/albums/${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).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`);
|
const { body } = await request(app)
|
||||||
|
.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 })],
|
||||||
@@ -676,7 +681,7 @@ describe('/album', () => {
|
|||||||
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(`/album/${album.id}/user/${user2.userId}`)
|
.put(`/albums/${album.id}/user/${user2.userId}`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||||
.send({ role: AlbumUserRole.Editor });
|
.send({ role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import {
|
|||||||
AssetFileUploadResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
LibraryResponseDto,
|
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
getAllLibraries,
|
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
|
getMyUser,
|
||||||
updateAssets,
|
updateAssets,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
@@ -73,7 +72,7 @@ describe('/asset', () => {
|
|||||||
let stackAssets: AssetFileUploadResponseDto[];
|
let stackAssets: AssetFileUploadResponseDto[];
|
||||||
let locationAsset: AssetFileUploadResponseDto;
|
let locationAsset: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
const setupTests = async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
|
||||||
@@ -87,6 +86,8 @@ describe('/asset', () => {
|
|||||||
utils.userSetup(admin.accessToken, createUserDto.create('stack')),
|
utils.userSetup(admin.accessToken, createUserDto.create('stack')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await utils.createPartner(user1.accessToken, user2.userId);
|
||||||
|
|
||||||
// asset location
|
// asset location
|
||||||
locationAsset = await utils.createAsset(admin.accessToken, {
|
locationAsset = await utils.createAsset(admin.accessToken, {
|
||||||
assetData: {
|
assetData: {
|
||||||
@@ -156,7 +157,8 @@ describe('/asset', () => {
|
|||||||
assetId: user1Assets[0].id,
|
assetId: user1Assets[0].id,
|
||||||
personId: person1.id,
|
personId: person1.id,
|
||||||
});
|
});
|
||||||
}, 30_000);
|
};
|
||||||
|
beforeAll(setupTests, 30_000);
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
utils.disconnectWebsocket(websocket);
|
utils.disconnectWebsocket(websocket);
|
||||||
@@ -233,6 +235,35 @@ describe('/asset', () => {
|
|||||||
expect(data.status).toBe(200);
|
expect(data.status).toBe(200);
|
||||||
expect(data.body).toMatchObject({ people: [] });
|
expect(data.body).toMatchObject({ people: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('partner assets', () => {
|
||||||
|
it('should get the asset info', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/asset/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id: user1Assets[0].id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows viewing archived assets', async () => {
|
||||||
|
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.get(`/asset/${asset.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows viewing trashed assets', async () => {
|
||||||
|
const asset = await utils.createAsset(user1.accessToken);
|
||||||
|
await utils.deleteAssets(user1.accessToken, [asset.id]);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.get(`/asset/${asset.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /asset/statistics', () => {
|
describe('GET /asset/statistics', () => {
|
||||||
@@ -540,14 +571,321 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/thumbnail/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include gps data for webp thumbnails', async () => {
|
||||||
|
await utils.waitForWebsocketEvent({
|
||||||
|
event: 'assetUpload',
|
||||||
|
id: locationAsset.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/webp');
|
||||||
|
|
||||||
|
const exifData = await readTags(body, 'thumbnail.webp');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include gps data for jpeg thumbnails', async () => {
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/jpeg');
|
||||||
|
|
||||||
|
const exifData = await readTags(body, 'thumbnail.jpg');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/file/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should download the original', async () => {
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/file/${locationAsset.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/jpeg');
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
|
||||||
|
|
||||||
|
const original = await readFile(locationAssetFilepath);
|
||||||
|
const originalChecksum = utils.sha1(original);
|
||||||
|
const downloadChecksum = utils.sha1(body);
|
||||||
|
|
||||||
|
expect(originalChecksum).toBe(downloadChecksum);
|
||||||
|
expect(downloadChecksum).toBe(asset.checksum);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/map-marker', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const files = [
|
||||||
|
'formats/avif/8bit-sRGB.avif',
|
||||||
|
'formats/jpg/el_torcal_rocks.jpg',
|
||||||
|
'formats/jxl/8bit-sRGB.jxl',
|
||||||
|
'formats/heic/IMG_2682.heic',
|
||||||
|
'formats/png/density_plot.png',
|
||||||
|
'formats/raw/Nikon/D80/glarus.nef',
|
||||||
|
'formats/raw/Nikon/D700/philadelphia.nef',
|
||||||
|
'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
|
||||||
|
'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
|
||||||
|
'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
|
||||||
|
];
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
const uploads = files.map((f) => uploadFile(f));
|
||||||
|
await Promise.all(uploads);
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/asset/map-marker');
|
||||||
|
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('/asset/map-marker')
|
||||||
|
.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('/asset/map-marker')
|
||||||
|
.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('PUT /asset', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/asset');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid parent id', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require access to the parent', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add stack children', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||||
|
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(204);
|
||||||
|
|
||||||
|
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||||
|
expect(asset.stack).not.toBeUndefined();
|
||||||
|
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove stack children', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||||
|
.send({ removeParent: true, ids: [stackAssets[1].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(204);
|
||||||
|
|
||||||
|
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||||
|
expect(asset.stack).not.toBeUndefined();
|
||||||
|
expect(asset.stack).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: stackAssets[2].id }),
|
||||||
|
expect.objectContaining({ id: stackAssets[3].id }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove all stack children', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||||
|
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(204);
|
||||||
|
|
||||||
|
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||||
|
expect(asset.stack).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge stack children', async () => {
|
||||||
|
// create stack after previous test removed stack children
|
||||||
|
await updateAssets(
|
||||||
|
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
|
||||||
|
{ headers: asBearerAuth(stackUser.accessToken) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.put('/asset')
|
||||||
|
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||||
|
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
|
||||||
|
|
||||||
|
expect(status).toBe(204);
|
||||||
|
|
||||||
|
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||||
|
expect(asset.stack).not.toBeUndefined();
|
||||||
|
expect(asset.stack).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: stackAssets[0].id }),
|
||||||
|
expect.objectContaining({ id: stackAssets[1].id }),
|
||||||
|
expect.objectContaining({ id: stackAssets[2].id }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /asset/stack/parent', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/asset/stack/parent');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid id', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put('/asset/stack/parent')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require access', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put('/asset/stack/parent')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make old parent child of new parent', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.put('/asset/stack/parent')
|
||||||
|
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||||
|
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||||
|
|
||||||
|
// new parent
|
||||||
|
expect(asset.stack).not.toBeUndefined();
|
||||||
|
expect(asset.stack).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: stackAssets[1].id }),
|
||||||
|
expect.objectContaining({ id: stackAssets[2].id }),
|
||||||
|
expect.objectContaining({ id: stackAssets[3].id }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('POST /asset/upload', () => {
|
describe('POST /asset/upload', () => {
|
||||||
|
beforeAll(setupTests, 30_000);
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/asset/upload`);
|
const { status, body } = await request(app).post(`/asset/upload`);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
const invalid = [
|
it.each([
|
||||||
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
|
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
|
||||||
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
|
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
|
||||||
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
|
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
|
||||||
@@ -556,21 +894,17 @@ describe('/asset', () => {
|
|||||||
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
|
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
|
||||||
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
|
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
|
||||||
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
|
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
|
||||||
];
|
])('should $should', async ({ dto }) => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/asset/upload')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.attach('assetData', makeRandomImage(), 'example.png')
|
||||||
|
.field(dto);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
for (const { should, dto } of invalid) {
|
it.each([
|
||||||
it(`should ${should}`, async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/asset/upload')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.attach('assetData', makeRandomImage(), 'example.png')
|
|
||||||
.field(dto);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tests = [
|
|
||||||
{
|
{
|
||||||
input: 'formats/avif/8bit-sRGB.avif',
|
input: 'formats/avif/8bit-sRGB.avif',
|
||||||
expected: {
|
expected: {
|
||||||
@@ -786,26 +1120,22 @@ describe('/asset', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
|
||||||
|
const filepath = join(testAssetDir, input);
|
||||||
for (const { input, expected } of tests) {
|
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
|
||||||
it(`should upload and generate a thumbnail for ${input}`, async () => {
|
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
||||||
const filepath = join(testAssetDir, input);
|
|
||||||
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
|
|
||||||
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(duplicate).toBe(false);
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
|
|
||||||
|
|
||||||
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
|
||||||
|
|
||||||
expect(asset.exifInfo).toBeDefined();
|
|
||||||
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
|
||||||
expect(asset).toMatchObject(expected);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
expect(duplicate).toBe(false);
|
||||||
|
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
||||||
|
expect(asset).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle a duplicate', async () => {
|
it('should handle a duplicate', async () => {
|
||||||
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
|
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
|
||||||
@@ -819,25 +1149,6 @@ describe('/asset', () => {
|
|||||||
expect(duplicate).toBe(true);
|
expect(duplicate).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not upload to another user's library", async () => {
|
|
||||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
const library = libraries.find((library) => library.ownerId === user1.userId) as LibraryResponseDto;
|
|
||||||
|
|
||||||
const { body, status } = await request(app)
|
|
||||||
.post('/asset/upload')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.field('libraryId', library.id)
|
|
||||||
.field('deviceAssetId', 'example-image')
|
|
||||||
.field('deviceId', 'e2e')
|
|
||||||
.field('fileCreatedAt', new Date().toISOString())
|
|
||||||
.field('fileModifiedAt', new Date().toISOString())
|
|
||||||
.field('duration', '0:00:00.000000')
|
|
||||||
.attach('assetData', makeRandomImage(), 'example.png');
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Not found or no asset.upload access'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update the used quota', async () => {
|
it('should update the used quota', async () => {
|
||||||
const { body, status } = await request(app)
|
const { body, status } = await request(app)
|
||||||
.post('/asset/upload')
|
.post('/asset/upload')
|
||||||
@@ -851,7 +1162,7 @@ describe('/asset', () => {
|
|||||||
expect(body).toEqual({ id: expect.any(String), duplicate: false });
|
expect(body).toEqual({ id: expect.any(String), duplicate: false });
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
|
|
||||||
const { body: user } = await request(app).get('/user/me').set('Authorization', `Bearer ${quotaUser.accessToken}`);
|
const user = await getMyUser({ headers: asBearerAuth(quotaUser.accessToken) });
|
||||||
|
|
||||||
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
|
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
|
||||||
});
|
});
|
||||||
@@ -875,7 +1186,7 @@ describe('/asset', () => {
|
|||||||
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
|
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
|
||||||
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
|
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
|
||||||
// into the test here.
|
// into the test here.
|
||||||
const motionTests = [
|
it.each([
|
||||||
{
|
{
|
||||||
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
|
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
|
||||||
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
|
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
|
||||||
@@ -888,329 +1199,23 @@ describe('/asset', () => {
|
|||||||
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
|
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
|
||||||
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
|
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
|
||||||
},
|
},
|
||||||
];
|
])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
|
||||||
|
const response = await utils.createAsset(admin.accessToken, {
|
||||||
for (const { filepath, checksum } of motionTests) {
|
assetData: {
|
||||||
it(`should extract motionphoto video from ${filepath}`, async () => {
|
bytes: await readFile(join(testAssetDir, filepath)),
|
||||||
const response = await utils.createAsset(admin.accessToken, {
|
filename: basename(filepath),
|
||||||
assetData: {
|
},
|
||||||
bytes: await readFile(join(testAssetDir, filepath)),
|
|
||||||
filename: basename(filepath),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
|
|
||||||
|
|
||||||
expect(response.duplicate).toBe(false);
|
|
||||||
|
|
||||||
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
|
|
||||||
expect(asset.livePhotoVideoId).toBeDefined();
|
|
||||||
|
|
||||||
const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
|
|
||||||
expect(video.checksum).toStrictEqual(checksum);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/thumbnail/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include gps data for webp thumbnails', async () => {
|
|
||||||
await utils.waitForWebsocketEvent({
|
|
||||||
event: 'assetUpload',
|
|
||||||
id: locationAsset.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, body, type } = await request(app)
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
|
||||||
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(response.duplicate).toBe(false);
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/webp');
|
|
||||||
|
|
||||||
const exifData = await readTags(body, 'thumbnail.webp');
|
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
|
||||||
expect(exifData).not.toHaveProperty('GPSLongitude');
|
expect(asset.livePhotoVideoId).toBeDefined();
|
||||||
expect(exifData).not.toHaveProperty('GPSLatitude');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include gps data for jpeg thumbnails', async () => {
|
const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
|
||||||
const { status, body, type } = await request(app)
|
expect(video.checksum).toStrictEqual(checksum);
|
||||||
.get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/jpeg');
|
|
||||||
|
|
||||||
const exifData = await readTags(body, 'thumbnail.jpg');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLongitude');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLatitude');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/file/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download the original', async () => {
|
|
||||||
const { status, body, type } = await request(app)
|
|
||||||
.get(`/asset/file/${locationAsset.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/jpeg');
|
|
||||||
|
|
||||||
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
|
|
||||||
|
|
||||||
const original = await readFile(locationAssetFilepath);
|
|
||||||
const originalChecksum = utils.sha1(original);
|
|
||||||
const downloadChecksum = utils.sha1(body);
|
|
||||||
|
|
||||||
expect(originalChecksum).toBe(downloadChecksum);
|
|
||||||
expect(downloadChecksum).toBe(asset.checksum);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/map-marker', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/map-marker');
|
|
||||||
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('/asset/map-marker')
|
|
||||||
.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('/asset/map-marker')
|
|
||||||
.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 /asset', () => {
|
|
||||||
it('should return stack data', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
|
||||||
|
|
||||||
const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(stack).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
stackCount: 3,
|
|
||||||
stack:
|
|
||||||
// Response includes children at the root level
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: stackAssets[1].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[2].id }),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /asset', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put('/asset');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require a valid parent id', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require access to the parent', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add stack children', async () => {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
|
||||||
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
|
||||||
expect(asset.stack).not.toBeUndefined();
|
|
||||||
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove stack children', async () => {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
|
||||||
.send({ removeParent: true, ids: [stackAssets[1].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
|
||||||
expect(asset.stack).not.toBeUndefined();
|
|
||||||
expect(asset.stack).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: stackAssets[2].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[3].id }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove all stack children', async () => {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
|
||||||
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
|
||||||
expect(asset.stack).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge stack children', async () => {
|
|
||||||
// create stack after previous test removed stack children
|
|
||||||
await updateAssets(
|
|
||||||
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
|
|
||||||
{ headers: asBearerAuth(stackUser.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.put('/asset')
|
|
||||||
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
|
||||||
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
|
|
||||||
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
|
||||||
expect(asset.stack).not.toBeUndefined();
|
|
||||||
expect(asset.stack).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: stackAssets[0].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[1].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[2].id }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /asset/stack/parent', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put('/asset/stack/parent');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require a valid id', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put('/asset/stack/parent')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require access', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put('/asset/stack/parent')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should make old parent child of new parent', async () => {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.put('/asset/stack/parent')
|
|
||||||
.set('Authorization', `Bearer ${stackUser.accessToken}`)
|
|
||||||
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
|
||||||
|
|
||||||
// new parent
|
|
||||||
expect(asset.stack).not.toBeUndefined();
|
|
||||||
expect(asset.stack).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: stackAssets[1].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[2].id }),
|
|
||||||
expect.objectContaining({ id: stackAssets[3].id }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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('/audit', () => {
|
describe('/audits', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
||||||
LibraryResponseDto,
|
|
||||||
LibraryType,
|
|
||||||
LoginResponseDto,
|
|
||||||
ScanLibraryDto,
|
|
||||||
getAllLibraries,
|
|
||||||
scanLibrary,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { cpSync, existsSync } from 'node:fs';
|
import { cpSync, existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
@@ -18,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('/library', () => {
|
describe('/libraries', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user: LoginResponseDto;
|
let user: LoginResponseDto;
|
||||||
let library: LibraryResponseDto;
|
let library: LibraryResponseDto;
|
||||||
@@ -29,7 +22,7 @@ describe('/library', () => {
|
|||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
await utils.resetAdminConfig(admin.accessToken);
|
await utils.resetAdminConfig(admin.accessToken);
|
||||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||||
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
||||||
@@ -44,44 +37,26 @@ describe('/library', () => {
|
|||||||
utils.resetEvents();
|
utils.resetEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /library', () => {
|
describe('GET /libraries', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/library');
|
const { status, body } = await request(app).get('/libraries');
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should start with a default upload library', async () => {
|
|
||||||
const { status, body } = await request(app).get('/library').set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.Upload,
|
|
||||||
name: 'Default Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /library', () => {
|
describe('POST /libraries', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/library').send({});
|
const { status, body } = await request(app).post('/libraries').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('/library')
|
.post('/libraries')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.External });
|
.send({ ownerId: admin.userId });
|
||||||
|
|
||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
@@ -89,15 +64,14 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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('/library')
|
.post('/libraries')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.External });
|
.send({ ownerId: admin.userId });
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'New External Library',
|
name: 'New External Library',
|
||||||
refreshedAt: null,
|
refreshedAt: null,
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
@@ -109,11 +83,10 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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('/library')
|
.post('/libraries')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path/to/import'],
|
importPaths: ['/path/to/import'],
|
||||||
exclusionPatterns: ['**/Raw/**'],
|
exclusionPatterns: ['**/Raw/**'],
|
||||||
@@ -130,11 +103,10 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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('/library')
|
.post('/libraries')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path', '/path'],
|
importPaths: ['/path', '/path'],
|
||||||
exclusionPatterns: ['**/Raw/**'],
|
exclusionPatterns: ['**/Raw/**'],
|
||||||
@@ -146,11 +118,10 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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('/library')
|
.post('/libraries')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'My Awesome Library',
|
name: 'My Awesome Library',
|
||||||
importPaths: ['/path/to/import'],
|
importPaths: ['/path/to/import'],
|
||||||
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
|
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
|
||||||
@@ -159,72 +130,18 @@ describe('/library', () => {
|
|||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create an upload library with defaults', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.Upload });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.Upload,
|
|
||||||
name: 'New Upload Library',
|
|
||||||
refreshedAt: null,
|
|
||||||
assetCount: 0,
|
|
||||||
importPaths: [],
|
|
||||||
exclusionPatterns: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an upload library with options', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, name: 'My Awesome Library' });
|
|
||||||
|
|
||||||
expect(status).toBe(201);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'My Awesome Library',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow upload libraries to have import paths', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow upload libraries to have exclusion patterns', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/library')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /library/:id', () => {
|
describe('PUT /libraries/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
|
const { status, body } = await request(app).put(`/libraries/${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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ name: 'New Library Name' });
|
.send({ name: 'New Library Name' });
|
||||||
|
|
||||||
@@ -238,7 +155,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ name: '' });
|
.send({ name: '' });
|
||||||
|
|
||||||
@@ -248,7 +165,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ importPaths: [testAssetDirInternal] });
|
.send({ importPaths: [testAssetDirInternal] });
|
||||||
|
|
||||||
@@ -262,7 +179,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ importPaths: [''] });
|
.send({ importPaths: [''] });
|
||||||
|
|
||||||
@@ -272,7 +189,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ importPaths: ['/path', '/path'] });
|
.send({ importPaths: ['/path', '/path'] });
|
||||||
|
|
||||||
@@ -282,7 +199,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ exclusionPatterns: ['**/Raw/**'] });
|
.send({ exclusionPatterns: ['**/Raw/**'] });
|
||||||
|
|
||||||
@@ -296,7 +213,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||||
|
|
||||||
@@ -306,7 +223,7 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${library.id}`)
|
.put(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ exclusionPatterns: [''] });
|
.send({ exclusionPatterns: [''] });
|
||||||
|
|
||||||
@@ -315,9 +232,9 @@ describe('/library', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /library/:id', () => {
|
describe('GET /libraries/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
|
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}`);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -325,27 +242,23 @@ describe('/library', () => {
|
|||||||
|
|
||||||
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(`/library/${uuidDto.notFound}`)
|
.get(`/libraries/${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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get library by id', async () => {
|
it('should get library by id', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.External,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/library/${library.id}`)
|
.get(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
name: 'New External Library',
|
name: 'New External Library',
|
||||||
refreshedAt: null,
|
refreshedAt: null,
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
@@ -356,41 +269,26 @@ describe('/library', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /library/:id/statistics', () => {
|
describe('GET /libraries/:id/statistics', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
|
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}/statistics`);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /library/:id/scan', () => {
|
describe('POST /libraries/:id/scan', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
|
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/scan`).send({});
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not scan an upload library', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.Upload,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post(`/library/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Can only refresh external libraries'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should scan external library', async () => {
|
it('should scan external library', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -406,7 +304,6 @@ describe('/library', () => {
|
|||||||
it('should scan external library with exclusion pattern', async () => {
|
it('should scan external library with exclusion pattern', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
exclusionPatterns: ['**/directoryA'],
|
exclusionPatterns: ['**/directoryA'],
|
||||||
});
|
});
|
||||||
@@ -423,7 +320,6 @@ describe('/library', () => {
|
|||||||
it('should scan multiple import paths', async () => {
|
it('should scan multiple import paths', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,7 +336,6 @@ describe('/library', () => {
|
|||||||
it('should pick up new files', async () => {
|
it('should pick up new files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -466,7 +361,6 @@ describe('/library', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -493,7 +387,6 @@ describe('/library', () => {
|
|||||||
it('should scan new files', async () => {
|
it('should scan new files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -521,7 +414,6 @@ describe('/library', () => {
|
|||||||
it('should reimport modified files', async () => {
|
it('should reimport modified files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -549,7 +441,6 @@ describe('/library', () => {
|
|||||||
it('should not reimport unmodified files', async () => {
|
it('should not reimport unmodified files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -579,7 +470,6 @@ describe('/library', () => {
|
|||||||
it('should reimport all files', async () => {
|
it('should reimport all files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -606,9 +496,9 @@ describe('/library', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /library/:id/removeOffline', () => {
|
describe('POST /libraries/:id/removeOffline', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
|
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -617,7 +507,6 @@ describe('/library', () => {
|
|||||||
it('should remove offline files', async () => {
|
it('should remove offline files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -643,7 +532,7 @@ describe('/library', () => {
|
|||||||
expect(offlineAssets.count).toBe(1);
|
expect(offlineAssets.count).toBe(1);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.post(`/library/${library.id}/removeOffline`)
|
.post(`/libraries/${library.id}/removeOffline`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send();
|
.send();
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
@@ -658,7 +547,6 @@ describe('/library', () => {
|
|||||||
it('should not remove online files', async () => {
|
it('should not remove online files', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -669,7 +557,7 @@ describe('/library', () => {
|
|||||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.post(`/library/${library.id}/removeOffline`)
|
.post(`/libraries/${library.id}/removeOffline`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send();
|
.send();
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
@@ -681,9 +569,9 @@ describe('/library', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /library/:id/validate', () => {
|
describe('POST /libraries/:id/validate', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
|
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/validate`).send({});
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -729,54 +617,25 @@ describe('/library', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /library/:id', () => {
|
describe('DELETE /libraries/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
const { status, body } = await request(app).delete(`/libraries/${uuidDto.notFound}`);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete the last upload library', async () => {
|
|
||||||
const libraries = await getAllLibraries(
|
|
||||||
{ $type: LibraryType.Upload },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
|
|
||||||
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
|
|
||||||
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
|
|
||||||
|
|
||||||
// delete all but the last upload library
|
|
||||||
for (const library of adminLibraries) {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.delete(`/library/${library.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/library/${lastLibrary.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete an external library', async () => {
|
it('should delete an external library', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||||
ownerId: admin.userId,
|
|
||||||
type: LibraryType.External,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/library/${library.id}`)
|
.delete(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
expect(body).toEqual({});
|
expect(body).toEqual({});
|
||||||
|
|
||||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(libraries).not.toEqual(
|
expect(libraries).not.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -789,7 +648,6 @@ describe('/library', () => {
|
|||||||
it('should delete an external library with assets', async () => {
|
it('should delete an external library with assets', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
type: LibraryType.External,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -797,13 +655,13 @@ describe('/library', () => {
|
|||||||
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(`/library/${library.id}`)
|
.delete(`/libraries/${library.id}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
expect(body).toEqual({});
|
expect(body).toEqual({});
|
||||||
|
|
||||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(libraries).not.toEqual(
|
expect(libraries).not.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -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('/partner', () => {
|
describe('/partners', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
@@ -28,9 +28,9 @@ describe('/partner', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /partner', () => {
|
describe('GET /partners', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/partner');
|
const { status, body } = await request(app).get('/partners');
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -38,7 +38,7 @@ describe('/partner', () => {
|
|||||||
|
|
||||||
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('/partner')
|
.get('/partners')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.query({ direction: 'shared-by' });
|
.query({ direction: 'shared-by' });
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ describe('/partner', () => {
|
|||||||
|
|
||||||
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('/partner')
|
.get('/partners')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.query({ direction: 'shared-with' });
|
.query({ direction: 'shared-with' });
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ describe('/partner', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /partner/:id', () => {
|
describe('POST /partners/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
|
const { status, body } = await request(app).post(`/partners/${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('/partner', () => {
|
|||||||
|
|
||||||
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(`/partner/${user3.userId}`)
|
.post(`/partners/${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('/partner', () => {
|
|||||||
|
|
||||||
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(`/partner/${user2.userId}`)
|
.post(`/partners/${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('/partner', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /partner/:id', () => {
|
describe('PUT /partners/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
|
const { status, body } = await request(app).put(`/partners/${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('/partner', () => {
|
|||||||
|
|
||||||
it('should update partner', async () => {
|
it('should update partner', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/partner/${user2.userId}`)
|
.put(`/partners/${user2.userId}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ inTimeline: false });
|
.send({ inTimeline: false });
|
||||||
|
|
||||||
@@ -103,9 +103,9 @@ describe('/partner', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /partner/:id', () => {
|
describe('DELETE /partners/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
|
const { status, body } = await request(app).delete(`/partners/${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('/partner', () => {
|
|||||||
|
|
||||||
it('should delete partner', async () => {
|
it('should delete partner', async () => {
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/partner/${user3.userId}`)
|
.delete(`/partners/${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('/partner', () => {
|
|||||||
|
|
||||||
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(`/partner/${user3.userId}`)
|
.delete(`/partners/${user3.userId}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
|
|||||||
@@ -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('/person', () => {
|
describe('/people', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let visiblePerson: PersonResponseDto;
|
let visiblePerson: PersonResponseDto;
|
||||||
let hiddenPerson: PersonResponseDto;
|
let hiddenPerson: PersonResponseDto;
|
||||||
@@ -47,11 +47,11 @@ describe('/person', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /person', () => {
|
describe('GET /people', () => {
|
||||||
beforeEach(async () => {});
|
beforeEach(async () => {});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/person');
|
const { status, body } = await request(app).get('/people');
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -59,7 +59,7 @@ describe('/person', () => {
|
|||||||
|
|
||||||
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('/person')
|
.get('/people')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.query({ withHidden: true });
|
.query({ withHidden: true });
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return only visible people', async () => {
|
it('should return only visible people', async () => {
|
||||||
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
@@ -90,9 +90,9 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /person/:id', () => {
|
describe('GET /people/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
|
const { status, body } = await request(app).get(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${uuidDto.notFound}`)
|
.get(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${visiblePerson.id}`)
|
.get(`/people/${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('/person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /person/:id/statistics', () => {
|
describe('GET /people/:id/statistics', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`);
|
const { status, body } = await request(app).get(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${uuidDto.notFound}/statistics`)
|
.get(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${multipleAssetsPerson.id}/statistics`)
|
.get(`/people/${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('/person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /person', () => {
|
describe('POST /people', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/person`);
|
const { status, body } = await request(app).post(`/people`);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -154,7 +154,7 @@ describe('/person', () => {
|
|||||||
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(`/person`)
|
.post(`/people`)
|
||||||
.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('/person', () => {
|
|||||||
|
|
||||||
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(`/person`)
|
.post(`/people`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({
|
.send({
|
||||||
name: 'New Person',
|
name: 'New Person',
|
||||||
@@ -179,9 +179,9 @@ describe('/person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /person/:id', () => {
|
describe('PUT /people/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
|
const { status, body } = await request(app).put(`/people/${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('/person', () => {
|
|||||||
]) {
|
]) {
|
||||||
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(`/person/${visiblePerson.id}`)
|
.put(`/people/${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('/person', () => {
|
|||||||
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(`/person/${visiblePerson.id}`)
|
.put(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${visiblePerson.id}`)
|
.put(`/people/${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('/person', () => {
|
|||||||
|
|
||||||
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(`/person/${visiblePerson.id}`)
|
.put(`/people/${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);
|
||||||
|
|||||||
@@ -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', () => {
|
describe('GET /server-info/storage', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/server-info');
|
const { status, body } = await request(app).get('/server-info/storage');
|
||||||
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')
|
.get('/server-info/storage')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
@@ -66,6 +66,7 @@ describe('/server-info', () => {
|
|||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
smartSearch: false,
|
smartSearch: false,
|
||||||
configFile: false,
|
configFile: false,
|
||||||
|
duplicateDetection: false,
|
||||||
facialRecognition: false,
|
facialRecognition: false,
|
||||||
map: true,
|
map: true,
|
||||||
reverseGeocoding: true,
|
reverseGeocoding: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
deleteUser,
|
deleteUserAdmin,
|
||||||
} 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,7 +13,7 @@ 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-link', () => {
|
describe('/shared-links', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let asset1: AssetFileUploadResponseDto;
|
||||||
let asset2: AssetFileUploadResponseDto;
|
let asset2: AssetFileUploadResponseDto;
|
||||||
@@ -86,7 +86,7 @@ describe('/shared-link', () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /share/${key}', () => {
|
describe('GET /share/${key}', () => {
|
||||||
@@ -114,9 +114,9 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link', () => {
|
describe('GET /shared-links', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-link');
|
const { status, body } = await request(app).get('/shared-links');
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -124,7 +124,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.get('/shared-links')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -142,7 +142,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.get('/shared-links')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -150,15 +150,15 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link/me', () => {
|
describe('GET /shared-links/me', () => {
|
||||||
it('should not require admin authentication', async () => {
|
it('should not require admin authentication', async () => {
|
||||||
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status } = await request(app).get('/shared-links/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-link/me').query({ key: linkWithAlbum.key });
|
const { status, body } = await request(app).get('/shared-links/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-link', () => {
|
|||||||
|
|
||||||
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-link/me')
|
.get('/shared-links/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-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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-link/me').query({ key: linkWithDeletedAlbum.key });
|
const { status, body } = await request(app).get('/shared-links/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-link/me').query({ key: linkWithPassword.key });
|
const { status, body } = await request(app).get('/shared-links/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-link', () => {
|
|||||||
|
|
||||||
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-link/me')
|
.get('/shared-links/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-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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-link/me').query({ key: linkWithMetadata.key });
|
const { status, body } = await request(app).get('/shared-links/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-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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-link/me').query({ key: linkWithoutMetadata.key });
|
const { status, body } = await request(app).get('/shared-links/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-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link/:id', () => {
|
describe('GET /shared-links/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
|
const { status, body } = await request(app).get(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAlbum.id}`)
|
.get(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAlbum.id}`)
|
.get(`/shared-links/${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-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /shared-link', () => {
|
describe('POST /shared-links', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/shared-link')
|
.post('/shared-links')
|
||||||
.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-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.post('/shared-links')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
@@ -293,7 +293,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.post('/shared-links')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ type: SharedLinkType.Album });
|
.send({ type: SharedLinkType.Album });
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.post('/shared-links')
|
||||||
.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-link', () => {
|
|||||||
|
|
||||||
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-link')
|
.post('/shared-links')
|
||||||
.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-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /shared-link/:id', () => {
|
describe('PATCH /shared-links/: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-link/${linkWithAlbum.id}`)
|
.patch(`/shared-links/${linkWithAlbum.id}`)
|
||||||
.send({ description: 'foo' });
|
.send({ description: 'foo' });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@@ -339,7 +339,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link/${uuidDto.notFound}`)
|
.patch(`/shared-links/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ description: 'foo' });
|
.send({ description: 'foo' });
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAlbum.id}`)
|
.patch(`/shared-links/${linkWithAlbum.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ description: 'foo' });
|
.send({ description: 'foo' });
|
||||||
|
|
||||||
@@ -364,10 +364,10 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /shared-link/:id/assets', () => {
|
describe('PUT /shared-links/: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-link/${linkWithAlbum.id}/assets`)
|
.put(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAssets.id}/assets`)
|
.put(`/shared-links/${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-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /shared-link/:id/assets', () => {
|
describe('DELETE /shared-links/: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-link/${linkWithAlbum.id}/assets`)
|
.delete(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAssets.id}/assets`)
|
.delete(`/shared-links/${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-link', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /shared-link/:id', () => {
|
describe('DELETE /shared-links/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
|
const { status, body } = await request(app).delete(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${uuidDto.notFound}`)
|
.delete(`/shared-links/${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-link', () => {
|
|||||||
|
|
||||||
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-link/${linkWithAlbum.id}`)
|
.delete(`/shared-links/${linkWithAlbum.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
|
import { LoginResponseDto, getAssetInfo, getAssetStatistics } 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,17 +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 getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
|
||||||
expect(before.length).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
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 getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after.length).toBe(0);
|
expect(after.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,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 utils.getAssetInfo(admin.accessToken, assetId);
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before.isTrashed).toBe(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 utils.getAssetInfo(admin.accessToken, assetId);
|
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after.isTrashed).toBe(false);
|
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
317
e2e/src/api/specs/user-admin.e2e-spec.ts
Normal file
317
e2e/src/api/specs/user-admin.e2e-spec.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { LoginResponseDto, deleteUserAdmin, getMyUser, getUserAdmin, 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',
|
||||||
|
'memoriesEnabled',
|
||||||
|
'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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a user without memories enabled', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/admin/users`)
|
||||||
|
.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('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', 'memoriesEnabled']) {
|
||||||
|
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 memories enabled', async () => {
|
||||||
|
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/admin/users/${admin.userId}`)
|
||||||
|
.send({ memoriesEnabled: false })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
...before,
|
||||||
|
updatedAt: expect.anything(),
|
||||||
|
memoriesEnabled: false,
|
||||||
|
});
|
||||||
|
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('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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,288 +1,105 @@
|
|||||||
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
|
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk';
|
||||||
import { Socket } from 'socket.io-client';
|
import { createUserDto } from 'src/fixtures';
|
||||||
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 { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { 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 });
|
||||||
|
|
||||||
[websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
|
[deletedUser, nonAdmin] = 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 deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin(
|
||||||
|
{ id: deletedUser.userId, userAdminDeleteDto: {} },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
describe('GET /users', () => {
|
||||||
utils.disconnectWebsocket(websocket);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /user', () => {
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/user');
|
const { status, body } = await request(app).get('/users');
|
||||||
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('/user').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(2);
|
||||||
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 /user/info/:id', () => {
|
describe('GET /users/me', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status } = await request(app).get(`/user/info/${admin.userId}`);
|
const { status, body } = await request(app).get(`/users/me`);
|
||||||
expect(status).toEqual(401);
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the user info', async () => {
|
it('should not work for shared links', async () => {
|
||||||
const { status, body } = await request(app)
|
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
|
||||||
.get(`/user/info/${admin.userId}`)
|
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
type: SharedLinkType.Album,
|
||||||
|
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',
|
||||||
|
memoriesEnabled: true,
|
||||||
|
quotaUsageInBytes: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /user/me', () => {
|
describe('PUT /users/me', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(`/user/me`);
|
const { status, body } = await request(app).put(`/users/me`);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get my info', async () => {
|
for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) {
|
||||||
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)
|
||||||
.post(`/user`)
|
.put(`/users/me`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...createUserDto.user1, [key]: null });
|
.send(dto);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should ignore `isAdmin`', async () => {
|
|
||||||
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)
|
|
||||||
.put(`/user`)
|
|
||||||
.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 () => {
|
it('should update first and last name', async () => {
|
||||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/users/me`)
|
||||||
.send({
|
.send({ name: 'Name' })
|
||||||
id: admin.userId,
|
|
||||||
name: 'Name',
|
|
||||||
})
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -291,17 +108,13 @@ describe('/user', () => {
|
|||||||
updatedAt: expect.any(String),
|
updatedAt: expect.any(String),
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
});
|
});
|
||||||
expect(before.updatedAt).not.toEqual(body.updatedAt);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update memories enabled', async () => {
|
it('should update memories enabled', async () => {
|
||||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/users/me`)
|
||||||
.send({
|
.send({ memoriesEnabled: false })
|
||||||
id: admin.userId,
|
|
||||||
memoriesEnabled: false,
|
|
||||||
})
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -310,7 +123,80 @@ describe('/user', () => {
|
|||||||
updatedAt: expect.anything(),
|
updatedAt: expect.anything(),
|
||||||
memoriesEnabled: false,
|
memoriesEnabled: false,
|
||||||
});
|
});
|
||||||
expect(before.updatedAt).not.toEqual(body.updatedAt);
|
|
||||||
|
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after.memoriesEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
it('should allow a user to change their password (deprecated)', async () => {
|
||||||
|
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
||||||
|
|
||||||
|
expect(user.shouldChangePassword).toBe(true);
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/users/me`)
|
||||||
|
.send({ password: 'super-secret' })
|
||||||
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
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(body).toMatchObject({
|
||||||
|
...before,
|
||||||
|
email: 'non-admin@immich.cloud',
|
||||||
|
updatedAt: expect.anything(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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(),
|
||||||
|
memoriesEnabled: expect.anything(),
|
||||||
|
storageLabel: expect.anything(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { LoginResponseDto, getAllAlbums, getAssetStatistics } 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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets1.length).toBe(9);
|
expect(assets1.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets2.length).toBe(9);
|
expect(assets2.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,8 +328,8 @@ describe(`immich upload`, () => {
|
|||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,8 +367,8 @@ describe(`immich upload`, () => {
|
|||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(9);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(8);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.total).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 getAllAssets({}, { headers: asKeyAuth(key) });
|
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||||
expect(assets.length).toBe(0);
|
expect(assets.total).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,11 +51,6 @@ export const errorDto = {
|
|||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'The server already has an admin',
|
message: 'The server already has an admin',
|
||||||
},
|
},
|
||||||
noDeleteUploadLibrary: {
|
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'Cannot delete the last upload library',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signupResponseDto = {
|
export const signupResponseDto = {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const setup = async () => {
|
|||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const input = data.toString();
|
const input = data.toString();
|
||||||
console.log(input);
|
console.log(input);
|
||||||
if (input.includes('Immich Microservices is listening')) {
|
if (input.includes('Immich Microservices is running')) {
|
||||||
_resolve();
|
_resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ import {
|
|||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
CreateUserDto,
|
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
|
UserAdminCreateDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
createLibrary,
|
createLibrary,
|
||||||
|
createPartner,
|
||||||
createPerson,
|
createPerson,
|
||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUserAdmin,
|
||||||
defaults,
|
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
getAllAssets,
|
|
||||||
getAllJobsStatus,
|
getAllJobsStatus,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
login,
|
login,
|
||||||
searchMetadata,
|
searchMetadata,
|
||||||
|
setBaseUrl,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
updateAdminOnboarding,
|
updateAdminOnboarding,
|
||||||
updateAlbumUser,
|
updateAlbumUser,
|
||||||
@@ -145,7 +145,6 @@ export const utils = {
|
|||||||
'sessions',
|
'sessions',
|
||||||
'users',
|
'users',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
'system_config',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const sql: string[] = [];
|
const sql: string[] = [];
|
||||||
@@ -256,8 +255,8 @@ export const utils = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setApiEndpoint: () => {
|
initSdk: () => {
|
||||||
defaults.baseUrl = app;
|
setBaseUrl(app);
|
||||||
},
|
},
|
||||||
|
|
||||||
adminSetup: async (options?: AdminSetupOptions) => {
|
adminSetup: async (options?: AdminSetupOptions) => {
|
||||||
@@ -274,8 +273,8 @@ export const utils = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: UserAdminCreateDto) => {
|
||||||
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
await createUserAdmin({ userAdminCreateDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
});
|
});
|
||||||
@@ -341,8 +340,6 @@ 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) });
|
||||||
},
|
},
|
||||||
@@ -389,6 +386,8 @@ 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([
|
||||||
{
|
{
|
||||||
@@ -463,7 +462,7 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.setApiEndpoint();
|
utils.initSdk();
|
||||||
|
|
||||||
if (!existsSync(`${testAssetDir}/albums`)) {
|
if (!existsSync(`${testAssetDir}/albums`)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
60
e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts
Normal file
60
e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { AssetFileUploadResponseDto, 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: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
52
e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts
Normal file
52
e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { AssetFileUploadResponseDto, 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: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { utils } from 'src/utils';
|
|||||||
|
|
||||||
test.describe('Registration', () => {
|
test.describe('Registration', () => {
|
||||||
test.beforeAll(() => {
|
test.beforeAll(() => {
|
||||||
utils.setApiEndpoint();
|
utils.initSdk();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ test.describe('Shared Links', () => {
|
|||||||
let sharedLinkPassword: SharedLinkResponseDto;
|
let sharedLinkPassword: SharedLinkResponseDto;
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
utils.setApiEndpoint();
|
utils.initSdk();
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
asset = await utils.createAsset(admin.accessToken);
|
asset = await utils.createAsset(admin.accessToken);
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ FROM python:3.11-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b
|
|||||||
|
|
||||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
|
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
|
||||||
USER root
|
USER root
|
||||||
|
# TODO: remove this once the image has the fix for https://github.com/intel/compute-runtime/issues/710
|
||||||
|
ENV NEOReadDebugKeys=1 \
|
||||||
|
OverrideGpuAddressSpace=48
|
||||||
|
|
||||||
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
|
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
|
||||||
|
|
||||||
@@ -74,8 +77,7 @@ RUN apt-get update && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/opt/venv/bin:$PATH" \
|
PATH="/opt/venv/bin:$PATH" \
|
||||||
@@ -93,3 +95,5 @@ 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
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
|
|
||||||
class LogSettings(BaseSettings):
|
class LogSettings(BaseSettings):
|
||||||
log_level: str = "info"
|
immich_log_level: str = "info"
|
||||||
no_color: bool = False
|
no_color: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -77,7 +77,7 @@ LOG_LEVELS: dict[str, int] = {
|
|||||||
settings = Settings()
|
settings = Settings()
|
||||||
log_settings = LogSettings()
|
log_settings = LogSettings()
|
||||||
|
|
||||||
LOG_LEVEL = LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO)
|
LOG_LEVEL = LOG_LEVELS.get(log_settings.immich_log_level.lower(), logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class CustomRichHandler(RichHandler):
|
class CustomRichHandler(RichHandler):
|
||||||
|
|||||||
14
machine-learning/app/healthcheck.py
Normal file
14
machine-learning/app/healthcheck.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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)
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:abcb3ae7e3521d08e1fdeaff63131765b34e4f29b6a8a2c28660036b53841569 as builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:d5b82811074b396275ef69aadbf31098257dd8836e231371e9cdb393128e571c as builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/opt/venv/bin:$PATH" \
|
PATH="/opt/venv/bin:$PATH" \
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ if ! [ "$DEVICE" = "openvino" ]; then
|
|||||||
export LD_BIND_NOW=1
|
export LD_BIND_NOW=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
: "${MACHINE_LEARNING_HOST:=[::]}"
|
: "${IMMICH_HOST:=[::]}"
|
||||||
: "${MACHINE_LEARNING_PORT:=3003}"
|
: "${IMMICH_PORT:=3003}"
|
||||||
: "${MACHINE_LEARNING_WORKERS:=1}"
|
: "${MACHINE_LEARNING_WORKERS:=1}"
|
||||||
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
|
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
|
||||||
|
|
||||||
gunicorn app.main:app \
|
gunicorn app.main:app \
|
||||||
-k app.config.CustomUvicornWorker \
|
-k app.config.CustomUvicornWorker \
|
||||||
|
-b "$IMMICH_HOST":"$IMMICH_PORT" \
|
||||||
-w "$MACHINE_LEARNING_WORKERS" \
|
-w "$MACHINE_LEARNING_WORKERS" \
|
||||||
-b "$MACHINE_LEARNING_HOST":"$MACHINE_LEARNING_PORT" \
|
|
||||||
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
|
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
|
||||||
--log-config-json log_conf.json \
|
--log-config-json log_conf.json \
|
||||||
--graceful-timeout 0
|
--graceful-timeout 0
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ 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"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.19.6"
|
"flutter": "3.22.0"
|
||||||
}
|
}
|
||||||
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.19.3",
|
"dart.flutterSdkPath": ".fvm/versions/3.22.0",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.929.0)
|
aws-partitions (1.932.0)
|
||||||
aws-sdk-core (3.196.1)
|
aws-sdk-core (3.196.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
@@ -169,7 +169,8 @@ GEM
|
|||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.8)
|
||||||
|
strscan (>= 3.0.9)
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
@@ -182,6 +183,7 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
|
strscan (3.1.0)
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def kotlin_version = '1.9.24'
|
def kotlin_version = '1.9.24'
|
||||||
def kotlin_coroutines_version = '1.8.0'
|
def kotlin_coroutines_version = '1.8.1'
|
||||||
def work_version = '2.9.0'
|
def work_version = '2.9.0'
|
||||||
def concurrent_version = '1.1.0'
|
def concurrent_version = '1.1.0'
|
||||||
def guava_version = '33.2.0-android'
|
def guava_version = '33.2.0-android'
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
allprojects {
|
allprojects {
|
||||||
|
ext.kotlin_version = '1.9.24'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.929.0)
|
aws-partitions (1.932.0)
|
||||||
aws-sdk-core (3.196.1)
|
aws-sdk-core (3.196.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
@@ -169,7 +169,8 @@ GEM
|
|||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.8)
|
||||||
|
strscan (>= 3.0.9)
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
@@ -182,6 +183,7 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
|
strscan (3.1.0)
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ SPEC CHECKSUMS:
|
|||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
||||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||||
|
|||||||
@@ -145,9 +145,10 @@ class Album {
|
|||||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
if (dto.sharedUsers.isNotEmpty) {
|
if (dto.albumUsers.isNotEmpty) {
|
||||||
final users = await db.users
|
final users = await db.users.getAllById(
|
||||||
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
|
dto.albumUsers.map((e) => e.user.id).toList(growable: false),
|
||||||
|
);
|
||||||
a.sharedUsers.addAll(users.cast());
|
a.sharedUsers.addAll(users.cast());
|
||||||
}
|
}
|
||||||
if (dto.assets.isNotEmpty) {
|
if (dto.assets.isNotEmpty) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class User {
|
|||||||
|
|
||||||
Id get isarId => fastHash(id);
|
Id get isarId => fastHash(id);
|
||||||
|
|
||||||
User.fromUserDto(UserResponseDto dto)
|
User.fromUserDto(UserAdminResponseDto dto)
|
||||||
: id = dto.id,
|
: id = dto.id,
|
||||||
updatedAt = dto.updatedAt,
|
updatedAt = dto.updatedAt,
|
||||||
email = dto.email,
|
email = dto.email,
|
||||||
@@ -44,21 +44,21 @@ class User {
|
|||||||
|
|
||||||
User.fromPartnerDto(PartnerResponseDto dto)
|
User.fromPartnerDto(PartnerResponseDto dto)
|
||||||
: id = dto.id,
|
: id = dto.id,
|
||||||
updatedAt = dto.updatedAt,
|
updatedAt = DateTime.now(),
|
||||||
email = dto.email,
|
email = dto.email,
|
||||||
name = dto.name,
|
name = dto.name,
|
||||||
isPartnerSharedBy = false,
|
isPartnerSharedBy = false,
|
||||||
isPartnerSharedWith = false,
|
isPartnerSharedWith = false,
|
||||||
profileImagePath = dto.profileImagePath,
|
profileImagePath = dto.profileImagePath,
|
||||||
isAdmin = dto.isAdmin,
|
isAdmin = false,
|
||||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
memoryEnabled = false,
|
||||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||||
inTimeline = dto.inTimeline ?? false,
|
inTimeline = dto.inTimeline ?? false,
|
||||||
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
|
quotaUsageInBytes = 0,
|
||||||
quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
|
quotaSizeInBytes = 0;
|
||||||
|
|
||||||
/// Base user dto used where the complete user object is not required
|
/// Base user dto used where the complete user object is not required
|
||||||
User.fromSimpleUserDto(UserDto dto)
|
User.fromSimpleUserDto(UserResponseDto dto)
|
||||||
: id = dto.id,
|
: id = dto.id,
|
||||||
email = dto.email,
|
email = dto.email,
|
||||||
name = dto.name,
|
name = dto.name,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ServerDiskInfo {
|
|||||||
return 'ServerDiskInfo(diskAvailable: $diskAvailable, diskSize: $diskSize, diskUse: $diskUse, diskUsagePercentage: $diskUsagePercentage)';
|
return 'ServerDiskInfo(diskAvailable: $diskAvailable, diskSize: $diskSize, diskUse: $diskUse, diskUsagePercentage: $diskUsagePercentage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerDiskInfo.fromDto(ServerInfoResponseDto dto)
|
ServerDiskInfo.fromDto(ServerStorageResponseDto dto)
|
||||||
: diskAvailable = dto.diskAvailable,
|
: diskAvailable = dto.diskAvailable,
|
||||||
diskSize = dto.diskSize,
|
diskSize = dto.diskSize,
|
||||||
diskUse = dto.diskUse,
|
diskUse = dto.diskUse,
|
||||||
|
|||||||
@@ -55,10 +55,9 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
|||||||
child: Chip(
|
child: Chip(
|
||||||
backgroundColor: context.primaryColor.withOpacity(0.15),
|
backgroundColor: context.primaryColor.withOpacity(0.15),
|
||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -88,13 +87,20 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
|||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: buildTileIcon(users[index]),
|
leading: buildTileIcon(users[index]),
|
||||||
|
dense: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
users[index].email,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (sharedUsersList.value.contains(users[index])) {
|
if (sharedUsersList.value.contains(users[index])) {
|
||||||
sharedUsersList.value = sharedUsersList.value
|
sharedUsersList.value = sharedUsersList.value
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget buildTitle(Album album) {
|
Widget buildTitle(Album album) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 24),
|
padding: const EdgeInsets.only(left: 8, right: 8),
|
||||||
child: userId == album.ownerId && album.isRemote
|
child: userId == album.ownerId && album.isRemote
|
||||||
? AlbumViewerEditableTitle(
|
? AlbumViewerEditableTitle(
|
||||||
album: album,
|
album: album,
|
||||||
@@ -228,9 +228,30 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: ref.watch(multiselectProvider)
|
body: Stack(
|
||||||
? null
|
children: [
|
||||||
: album.when(
|
album.widgetWhen(
|
||||||
|
onData: (data) => MultiselectGrid(
|
||||||
|
renderListProvider: albumRenderlistProvider(albumId),
|
||||||
|
topWidget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildHeader(data),
|
||||||
|
if (data.isRemote) buildControlButton(data),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
|
editEnabled: data.ownerId == userId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
top: ref.watch(multiselectProvider)
|
||||||
|
? -(kToolbarHeight + MediaQuery.of(context).padding.top)
|
||||||
|
: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: album.when(
|
||||||
data: (data) => AlbumViewerAppbar(
|
data: (data) => AlbumViewerAppbar(
|
||||||
titleFocusNode: titleFocusNode,
|
titleFocusNode: titleFocusNode,
|
||||||
album: data,
|
album: data,
|
||||||
@@ -242,19 +263,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||||
loading: () => AppBar(),
|
loading: () => AppBar(),
|
||||||
),
|
),
|
||||||
body: album.widgetWhen(
|
|
||||||
onData: (data) => MultiselectGrid(
|
|
||||||
renderListProvider: albumRenderlistProvider(albumId),
|
|
||||||
topWidget: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
buildHeader(data),
|
|
||||||
if (data.isRemote) buildControlButton(data),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
],
|
||||||
editEnabled: data.ownerId == userId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,11 +138,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
|
|
||||||
Future<bool> changePassword(String newPassword) async {
|
Future<bool> changePassword(String newPassword) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.userApi.updateUser(
|
await _apiService.userApi.updateMyUser(
|
||||||
UpdateUserDto(
|
UserUpdateMeDto(
|
||||||
id: state.userId,
|
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
shouldChangePassword: false,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,9 +176,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
user = offlineUser;
|
user = offlineUser;
|
||||||
retResult = false;
|
retResult = false;
|
||||||
} else {
|
} else {
|
||||||
UserResponseDto? userResponseDto;
|
UserAdminResponseDto? userResponseDto;
|
||||||
try {
|
try {
|
||||||
userResponseDto = await _apiService.userApi.getMyUserInfo();
|
userResponseDto = await _apiService.userApi.getMyUser();
|
||||||
} on ApiException catch (error, stackTrace) {
|
} on ApiException catch (error, stackTrace) {
|
||||||
_log.severe(
|
_log.severe(
|
||||||
"Error getting user information from the server [API EXCEPTION]",
|
"Error getting user information from the server [API EXCEPTION]",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||||
await _getBackupAlbumsInfo();
|
await _getBackupAlbumsInfo();
|
||||||
await updateServerInfo();
|
await updateDiskInfo();
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
} else {
|
} else {
|
||||||
log.warning("cannot get backup info - background backup is in progress!");
|
log.warning("cannot get backup info - background backup is in progress!");
|
||||||
@@ -542,7 +542,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_updatePersistentAlbumsSelection();
|
_updatePersistentAlbumsSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServerInfo();
|
updateDiskInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
@@ -579,13 +579,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateServerInfo() async {
|
Future<void> updateDiskInfo() async {
|
||||||
final serverInfo = await _serverInfoService.getServerInfo();
|
final diskInfo = await _serverInfoService.getDiskInfo();
|
||||||
|
|
||||||
// Update server info
|
// Update server info
|
||||||
if (serverInfo != null) {
|
if (diskInfo != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
serverInfo: serverInfo,
|
serverInfo: diskInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||||||
bool isDuplicated,
|
bool isDuplicated,
|
||||||
) {
|
) {
|
||||||
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
||||||
_backupProvider.updateServerInfo();
|
_backupProvider.updateDiskInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
|||||||
|
|
||||||
refresh() async {
|
refresh() async {
|
||||||
try {
|
try {
|
||||||
final user = await _apiService.userApi.getMyUserInfo();
|
final user = await _apiService.userApi.getMyUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
Store.put(
|
Store.put(
|
||||||
StoreKey.currentUser,
|
StoreKey.currentUser,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||||||
// Update user info
|
// Update user info
|
||||||
try {
|
try {
|
||||||
final userResponseDto =
|
final userResponseDto =
|
||||||
await ref.read(apiServiceProvider).userApi.getMyUserInfo();
|
await ref.read(apiServiceProvider).userApi.getMyUser();
|
||||||
|
|
||||||
if (userResponseDto == null) {
|
if (userResponseDto == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -180,7 +180,14 @@ class AlbumService {
|
|||||||
CreateAlbumDto(
|
CreateAlbumDto(
|
||||||
albumName: albumName,
|
albumName: albumName,
|
||||||
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
||||||
sharedWithUserIds: sharedUsers.map((e) => e.id).toList(),
|
albumUsers: sharedUsers
|
||||||
|
.map(
|
||||||
|
(e) => AlbumUserCreateDto(
|
||||||
|
userId: e.id,
|
||||||
|
role: AlbumUserRole.editor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (remote != null) {
|
if (remote != null) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
@@ -590,6 +591,7 @@ enum IosBackgroundTask { fetch, processing }
|
|||||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void _nativeEntry() {
|
void _nativeEntry() {
|
||||||
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
BackgroundService backgroundService = BackgroundService();
|
BackgroundService backgroundService = BackgroundService();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:isar/isar.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
import '../utils/string_helper.dart';
|
||||||
|
|
||||||
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
||||||
return MemoryService(
|
return MemoryService(
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
@@ -36,13 +38,13 @@ class MemoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Memory> memories = [];
|
List<Memory> memories = [];
|
||||||
for (final MemoryLaneResponseDto(:title, :assets) in data) {
|
for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
|
||||||
final dbAssets =
|
final dbAssets =
|
||||||
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
||||||
if (dbAssets.isNotEmpty) {
|
if (dbAssets.isNotEmpty) {
|
||||||
memories.add(
|
memories.add(
|
||||||
Memory(
|
Memory(
|
||||||
title: title,
|
title: '$yearsAgo year${s(yearsAgo)} ago',
|
||||||
assets: dbAssets,
|
assets: dbAssets,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ class ServerInfoService {
|
|||||||
|
|
||||||
ServerInfoService(this._apiService);
|
ServerInfoService(this._apiService);
|
||||||
|
|
||||||
Future<ServerDiskInfo?> getServerInfo() async {
|
Future<ServerDiskInfo?> getDiskInfo() async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.serverInfoApi.getServerInfo();
|
final dto = await _apiService.serverInfoApi.getStorage();
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
return ServerDiskInfo.fromDto(dto);
|
return ServerDiskInfo.fromDto(dto);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getServerInfo] ${e.toString()}");
|
debugPrint("Error [getDiskInfo] ${e.toString()}");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,15 +362,15 @@ class SyncService {
|
|||||||
// update shared users
|
// update shared users
|
||||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||||
dto.sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
||||||
final List<String> userIdsToAdd = [];
|
final List<String> userIdsToAdd = [];
|
||||||
final List<User> usersToUnlink = [];
|
final List<User> usersToUnlink = [];
|
||||||
diffSortedListsSync(
|
diffSortedListsSync(
|
||||||
dto.sharedUsers,
|
dto.albumUsers,
|
||||||
sharedUsers,
|
sharedUsers,
|
||||||
compare: (UserResponseDto a, User b) => a.id.compareTo(b.id),
|
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
||||||
both: (a, b) => false,
|
both: (a, b) => false,
|
||||||
onlyFirst: (UserResponseDto a) => userIdsToAdd.add(a.id),
|
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
||||||
onlySecond: (User a) => usersToUnlink.add(a),
|
onlySecond: (User a) => usersToUnlink.add(a),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -905,7 +905,7 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
|||||||
dto.albumName != a.name ||
|
dto.albumName != a.name ||
|
||||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
||||||
dto.shared != a.shared ||
|
dto.shared != a.shared ||
|
||||||
dto.sharedUsers.length != a.sharedUsers.length ||
|
dto.albumUsers.length != a.sharedUsers.length ||
|
||||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
||||||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
||||||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ class UserService {
|
|||||||
this._partnerService,
|
this._partnerService,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<User>?> _getAllUsers({required bool isAll}) async {
|
Future<List<User>?> _getAllUsers() async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
final dto = await _apiService.userApi.searchUsers();
|
||||||
return dto?.map(User.fromUserDto).toList();
|
return dto?.map(User.fromSimpleUserDto).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.warning("Failed get all users", e);
|
_log.warning("Failed get all users", e);
|
||||||
return null;
|
return null;
|
||||||
@@ -71,7 +71,7 @@ class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<User>?> getUsersFromServer() async {
|
Future<List<User>?> getUsersFromServer() async {
|
||||||
final List<User>? users = await _getAllUsers(isAll: true);
|
final List<User>? users = await _getAllUsers();
|
||||||
final List<User>? sharedBy =
|
final List<User>? sharedBy =
|
||||||
await _partnerService.getPartners(PartnerDirection.sharedBy);
|
await _partnerService.getPartners(PartnerDirection.sharedBy);
|
||||||
final List<User>? sharedWith =
|
final List<User>? sharedWith =
|
||||||
|
|||||||
@@ -77,5 +77,5 @@ String getThumbnailUrlForRemoteId(
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getFaceThumbnailUrl(final String personId) {
|
String getFaceThumbnailUrl(final String personId) {
|
||||||
return '${Store.get(StoreKey.serverEndpoint)}/person/$personId/thumbnail';
|
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,12 +121,12 @@ final ThemeData immichLightTheme = ThemeData(
|
|||||||
),
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
indicatorColor: Colors.indigo.withOpacity(0.15),
|
indicatorColor: Colors.indigo.withOpacity(0.15),
|
||||||
iconTheme: MaterialStatePropertyAll(
|
iconTheme: WidgetStatePropertyAll(
|
||||||
IconThemeData(color: Colors.grey[700]),
|
IconThemeData(color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
labelTextStyle: MaterialStatePropertyAll(
|
labelTextStyle: WidgetStatePropertyAll(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -249,12 +249,12 @@ final ThemeData immichDarkTheme = ThemeData(
|
|||||||
),
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
|
indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
|
||||||
iconTheme: MaterialStatePropertyAll(
|
iconTheme: WidgetStatePropertyAll(
|
||||||
IconThemeData(color: Colors.grey[500]),
|
IconThemeData(color: Colors.grey[500]),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.grey[900],
|
backgroundColor: Colors.grey[900],
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
labelTextStyle: MaterialStatePropertyAll(
|
labelTextStyle: WidgetStatePropertyAll(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ extension StringExtension on String {
|
|||||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String s(num count) => (count == 1 ? '' : 's');
|
||||||
@@ -36,58 +36,62 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return TextField(
|
return Material(
|
||||||
onChanged: (value) {
|
color: Colors.transparent,
|
||||||
if (value.isEmpty) {
|
child: TextField(
|
||||||
} else {
|
onChanged: (value) {
|
||||||
ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
|
if (value.isEmpty) {
|
||||||
}
|
} else {
|
||||||
},
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
|
||||||
focusNode: titleFocusNode,
|
}
|
||||||
style: context.textTheme.headlineMedium,
|
},
|
||||||
controller: titleTextEditController,
|
focusNode: titleFocusNode,
|
||||||
onTap: () {
|
style: context.textTheme.headlineMedium,
|
||||||
FocusScope.of(context).requestFocus(titleFocusNode);
|
controller: titleTextEditController,
|
||||||
|
onTap: () {
|
||||||
|
FocusScope.of(context).requestFocus(titleFocusNode);
|
||||||
|
|
||||||
ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name);
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name);
|
||||||
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
if (titleTextEditController.text == 'Untitled') {
|
if (titleTextEditController.text == 'Untitled') {
|
||||||
titleTextEditController.clear();
|
titleTextEditController.clear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
contentPadding:
|
||||||
suffixIcon: titleFocusNode.hasFocus
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
? IconButton(
|
suffixIcon: titleFocusNode.hasFocus
|
||||||
onPressed: () {
|
? IconButton(
|
||||||
titleTextEditController.clear();
|
onPressed: () {
|
||||||
},
|
titleTextEditController.clear();
|
||||||
icon: Icon(
|
},
|
||||||
Icons.cancel_rounded,
|
icon: Icon(
|
||||||
color: context.primaryColor,
|
Icons.cancel_rounded,
|
||||||
),
|
color: context.primaryColor,
|
||||||
splashRadius: 10,
|
),
|
||||||
)
|
splashRadius: 10,
|
||||||
: null,
|
)
|
||||||
enabledBorder: OutlineInputBorder(
|
: null,
|
||||||
borderSide: const BorderSide(color: Colors.transparent),
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
),
|
borderRadius: BorderRadius.circular(10),
|
||||||
focusedBorder: OutlineInputBorder(
|
),
|
||||||
borderSide: const BorderSide(color: Colors.transparent),
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
),
|
borderRadius: BorderRadius.circular(10),
|
||||||
focusColor: Colors.grey[300],
|
),
|
||||||
fillColor: context.isDarkTheme
|
focusColor: Colors.grey[300],
|
||||||
? const Color.fromARGB(255, 32, 33, 35)
|
fillColor: context.isDarkTheme
|
||||||
: Colors.grey[200],
|
? const Color.fromARGB(255, 32, 33, 35)
|
||||||
filled: titleFocusNode.hasFocus,
|
: Colors.grey[200],
|
||||||
hintText: 'share_add_title'.tr(),
|
filled: titleFocusNode.hasFocus,
|
||||||
hintStyle: TextStyle(
|
hintText: 'share_add_title'.tr(),
|
||||||
fontSize: 28,
|
hintStyle: TextStyle(
|
||||||
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -238,8 +238,10 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool appBarOffset() {
|
bool appBarOffset() {
|
||||||
return ref.watch(tabProvider).index == 0 &&
|
return (ref.watch(tabProvider).index == 0 &&
|
||||||
ModalRoute.of(context)?.settings.name == TabControllerRoute.name;
|
ModalRoute.of(context)?.settings.name ==
|
||||||
|
TabControllerRoute.name) ||
|
||||||
|
(ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
final listWidget = ScrollablePositionedList.builder(
|
final listWidget = ScrollablePositionedList.builder(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
ref.read(backupProvider.notifier).updateServerInfo();
|
ref.read(backupProvider.notifier).updateDiskInfo();
|
||||||
ref.read(currentUserProvider.notifier).refresh();
|
ref.read(currentUserProvider.notifier).refresh();
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class _DateTimePicker extends HookWidget {
|
|||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
menuStyle: const MenuStyle(
|
menuStyle: const MenuStyle(
|
||||||
fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
|
fixedSize: WidgetStatePropertyAll(Size.fromWidth(350)),
|
||||||
alignment: Alignment(-1.25, 0.5),
|
alignment: Alignment(-1.25, 0.5),
|
||||||
),
|
),
|
||||||
onSelected: (value) => tzOffset.value = value!,
|
onSelected: (value) => tzOffset.value = value!,
|
||||||
@@ -175,7 +175,7 @@ class _DateTimePicker extends HookWidget {
|
|||||||
value: t,
|
value: t,
|
||||||
label: t.display,
|
label: t.display,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
textStyle: MaterialStatePropertyAll(
|
textStyle: WidgetStatePropertyAll(
|
||||||
context.textTheme.bodyMedium,
|
context.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class _ManualPicker extends HookWidget {
|
|||||||
decorationText: "location_picker_longitude",
|
decorationText: "location_picker_longitude",
|
||||||
hintText: "location_picker_longitude_hint",
|
hintText: "location_picker_longitude_hint",
|
||||||
errorText: "location_picker_longitude_error",
|
errorText: "location_picker_longitude_error",
|
||||||
focusNode: latitiudeFocusNode,
|
focusNode: longitudeFocusNode,
|
||||||
validator: _validateLong,
|
validator: _validateLong,
|
||||||
onUpdated: onLongitudeEditingCompleted,
|
onUpdated: onLongitudeEditingCompleted,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
|||||||
|
|
||||||
Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
||||||
final url =
|
final url =
|
||||||
"${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${u.id}";
|
"${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
|
||||||
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
|
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: radius,
|
radius: radius,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
final profileImageUrl =
|
final profileImageUrl =
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
|
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||||
|
|
||||||
final textIcon = Text(
|
final textIcon = Text(
|
||||||
user.name[0].toUpperCase(),
|
user.name[0].toUpperCase(),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class CameraPicker extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final menuStyle = MenuStyle(
|
final menuStyle = MenuStyle(
|
||||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
shape: WidgetStatePropertyAll<OutlinedBorder>(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class LocationPicker extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final menuStyle = MenuStyle(
|
final menuStyle = MenuStyle(
|
||||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
shape: WidgetStatePropertyAll<OutlinedBorder>(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user