Merge branch 'main' of https://github.com/immich-app/immich into chore/library-init-micro
This commit is contained in:
@@ -1,30 +1,31 @@
|
||||
.vscode/
|
||||
.github/
|
||||
.git/
|
||||
|
||||
design/
|
||||
docker/
|
||||
docs/
|
||||
e2e/
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
mobile/
|
||||
|
||||
server/node_modules/
|
||||
cli/coverage/
|
||||
cli/dist/
|
||||
cli/node_modules/
|
||||
|
||||
open-api/typescript-sdk/build/
|
||||
open-api/typescript-sdk/node_modules/
|
||||
|
||||
server/coverage/
|
||||
server/.reverse-geocoding-dump/
|
||||
server/node_modules/
|
||||
server/upload/
|
||||
server/dist/
|
||||
server/www/
|
||||
server/test/assets/
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
web/build/
|
||||
|
||||
cli/node_modules/
|
||||
cli/.reverse-geocoding-dump/
|
||||
cli/upload/
|
||||
cli/dist/
|
||||
|
||||
e2e/
|
||||
|
||||
open-api/typescript-sdk/node_modules/
|
||||
open-api/typescript-sdk/build/
|
||||
|
||||
@@ -16,4 +16,4 @@ max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
quote_type = double
|
||||
quote_type = single
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -8,8 +8,6 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
||||
mobile/lib/**/*.g.dart -diff -merge
|
||||
mobile/lib/**/*.g.dart linguist-generated=true
|
||||
|
||||
open-api/typescript-sdk/axios-client/**/* -diff -merge
|
||||
open-api/typescript-sdk/axios-client/**/* linguist-generated=true
|
||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||
|
||||
|
||||
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make server-e2e-jobs
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -194,25 +194,40 @@ jobs:
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run setup cli
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./cli
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
run: npx playwright install --with-deps chromium
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Docker build
|
||||
run: docker compose build
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run e2e tests (api & cli)
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run e2e tests (web)
|
||||
run: npx playwright test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
mobile-unit-tests:
|
||||
name: Mobile
|
||||
@@ -222,8 +237,8 @@ jobs:
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
channel: 'stable'
|
||||
flutter-version: '3.16.9'
|
||||
- name: Run tests
|
||||
working-directory: ./mobile
|
||||
run: flutter test -j 1
|
||||
@@ -241,7 +256,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: "poetry"
|
||||
cache: 'poetry'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev --with cpu
|
||||
|
||||
8
cli/package-lock.json
generated
8
cli/package-lock.json
generated
@@ -54,14 +54,6 @@
|
||||
"@oazapfts/runtime": "^1.0.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.6.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"axios": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../server": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# - https://immich.app/docs/developer/setup
|
||||
# - https://immich.app/docs/developer/troubleshooting
|
||||
|
||||
version: "3.8"
|
||||
version: '3.8'
|
||||
|
||||
name: immich-dev
|
||||
|
||||
@@ -30,7 +30,7 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
||||
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 3001:3001
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
||||
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
image: immich-web-dev:latest
|
||||
build:
|
||||
context: ../web
|
||||
command: [ "/usr/src/app/bin/immich-web" ]
|
||||
command: ['/usr/src/app/bin/immich-web']
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "3.8"
|
||||
version: '3.8'
|
||||
|
||||
name: immich-prod
|
||||
|
||||
@@ -17,7 +17,7 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: [ "start.sh", "immich" ]
|
||||
command: ['start.sh', 'immich']
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 2283:3001
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: [ "start.sh", "microservices" ]
|
||||
command: ['start.sh', 'microservices']
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "3.8"
|
||||
version: '3.8'
|
||||
|
||||
#
|
||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
command: ['start.sh', 'immich']
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
# 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" ]
|
||||
command: ['start.sh', 'microservices']
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
31
e2e/.eslintrc.cjs
Normal file
31
e2e/.eslintrc.cjs
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
},
|
||||
};
|
||||
16
e2e/.prettierignore
Normal file
16
e2e/.prettierignore
Normal file
@@ -0,0 +1,16 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.md
|
||||
*.json
|
||||
coverage
|
||||
dist
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
8
e2e/.prettierrc
Normal file
8
e2e/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"organizeImportsSkipDestructiveCodeActions": true,
|
||||
"plugins": ["prettier-plugin-organize-imports"]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "3.8"
|
||||
version: '3.8'
|
||||
|
||||
name: immich-e2e
|
||||
|
||||
@@ -23,14 +23,14 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich-e2e-server
|
||||
command: [ "./start.sh", "immich" ]
|
||||
command: ['./start.sh', 'immich']
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 2283:3001
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich-e2e-microservices
|
||||
command: [ "./start.sh", "microservices" ]
|
||||
command: ['./start.sh', 'microservices']
|
||||
<<: *server-common
|
||||
|
||||
redis:
|
||||
|
||||
2213
e2e/package-lock.json
generated
2213
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,11 @@
|
||||
"scripts": {
|
||||
"test": "vitest --config vitest.config.ts",
|
||||
"test:web": "npx playwright test",
|
||||
"start:web": "npx playwright test --ui"
|
||||
"start:web": "npx playwright test --ui",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -19,11 +23,21 @@
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.1",
|
||||
"exiftool-vendored": "^24.5.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -20,10 +20,7 @@ describe('/activity', () => {
|
||||
let album: AlbumResponseDto;
|
||||
|
||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||
create(
|
||||
{ activityCreateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken || admin.accessToken) },
|
||||
);
|
||||
create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
@@ -56,13 +53,9 @@ describe('/activity', () => {
|
||||
});
|
||||
|
||||
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('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
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 () => {
|
||||
@@ -71,9 +64,7 @@ describe('/activity', () => {
|
||||
.query({ albumId: uuidDto.invalid })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
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 assetId', async () => {
|
||||
@@ -82,9 +73,7 @@ describe('/activity', () => {
|
||||
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should start off empty', async () => {
|
||||
@@ -160,9 +149,7 @@ describe('/activity', () => {
|
||||
});
|
||||
|
||||
it('should filter by userId', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
||||
]);
|
||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||
|
||||
const response1 = await request(app)
|
||||
.get('/activity')
|
||||
@@ -215,9 +202,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: uuidDto.invalid });
|
||||
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 require a comment when type is comment', async () => {
|
||||
@@ -226,12 +211,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'comment must be a string',
|
||||
'comment should not be empty',
|
||||
]),
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
|
||||
});
|
||||
|
||||
it('should add a comment to an album', async () => {
|
||||
@@ -271,9 +251,7 @@ describe('/activity', () => {
|
||||
});
|
||||
|
||||
it('should return a 200 for a duplicate like on the album', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
||||
]);
|
||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/activity')
|
||||
@@ -356,9 +334,7 @@ describe('/activity', () => {
|
||||
|
||||
describe('DELETE /activity/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/activity/${uuidDto.notFound}`,
|
||||
);
|
||||
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -420,9 +396,7 @@ describe('/activity', () => {
|
||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no activity.delete access'),
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
|
||||
});
|
||||
|
||||
it('should let a non-owner remove their own comment', async () => {
|
||||
|
||||
@@ -93,10 +93,7 @@ describe('/album', () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
await deleteUser(
|
||||
{ id: user3.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
await deleteUser({ id: user3.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
});
|
||||
|
||||
describe('GET /album', () => {
|
||||
@@ -111,9 +108,7 @@ describe('/album', () => {
|
||||
.get('/album?shared=invalid')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
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']));
|
||||
});
|
||||
|
||||
it('should reject an invalid assetId param', async () => {
|
||||
@@ -153,9 +148,7 @@ describe('/album', () => {
|
||||
});
|
||||
|
||||
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('/album').set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(3);
|
||||
expect(body).toEqual(
|
||||
@@ -250,9 +243,7 @@ describe('/album', () => {
|
||||
|
||||
describe('GET /album/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/album/${user1Albums[0].id}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -265,7 +256,7 @@ describe('/album', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -277,7 +268,7 @@ describe('/album', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user2Albums[0],
|
||||
assets: [expect.objectContaining(user2Albums[0].assets[0])],
|
||||
assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -289,7 +280,7 @@ describe('/album', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -326,9 +317,7 @@ describe('/album', () => {
|
||||
|
||||
describe('POST /album', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/album')
|
||||
.send({ albumName: 'New album' });
|
||||
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -360,9 +349,7 @@ describe('/album', () => {
|
||||
|
||||
describe('PUT /album/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/album/${user1Albums[0].id}/assets`,
|
||||
);
|
||||
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -375,9 +362,7 @@ describe('/album', () => {
|
||||
.send({ ids: [asset.id] });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({ id: asset.id, success: true }),
|
||||
]);
|
||||
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
||||
});
|
||||
|
||||
it('should be able to add own asset to shared album', async () => {
|
||||
@@ -388,9 +373,7 @@ describe('/album', () => {
|
||||
.send({ ids: [asset.id] });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({ id: asset.id, success: true }),
|
||||
]);
|
||||
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -473,9 +456,7 @@ describe('/album', () => {
|
||||
.send({ ids: [user1Asset1.id] });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
||||
]);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
||||
});
|
||||
|
||||
it('should be able to remove own asset from shared album', async () => {
|
||||
@@ -485,9 +466,7 @@ describe('/album', () => {
|
||||
.send({ ids: [user1Asset1.id] });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
||||
]);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -501,9 +480,7 @@ describe('/album', () => {
|
||||
});
|
||||
|
||||
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(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
|
||||
@@ -13,21 +13,15 @@ import { basename, join } from 'node:path';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import {
|
||||
apiUtils,
|
||||
app,
|
||||
dbUtils,
|
||||
tempDir,
|
||||
testAssetDir,
|
||||
wsUtils,
|
||||
} from 'src/utils';
|
||||
import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||
|
||||
const sha1 = (bytes: Buffer) =>
|
||||
createHash('sha1').update(bytes).digest('base64');
|
||||
const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64');
|
||||
|
||||
const readTags = async (bytes: Buffer, filename: string) => {
|
||||
const filepath = join(tempDir, filename);
|
||||
@@ -65,50 +59,45 @@ describe('/asset', () => {
|
||||
]);
|
||||
|
||||
// asset location
|
||||
assetLocation = await apiUtils.createAsset(
|
||||
admin.accessToken,
|
||||
{},
|
||||
{
|
||||
assetLocation = await apiUtils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename: 'thompson-springs.jpg',
|
||||
bytes: await readFile(locationAssetFilepath),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id });
|
||||
|
||||
user1Assets = await Promise.all([
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(
|
||||
user1.accessToken,
|
||||
{
|
||||
isFavorite: true,
|
||||
isExternal: true,
|
||||
isReadOnly: true,
|
||||
fileCreatedAt: yesterday.toISO(),
|
||||
fileModifiedAt: yesterday.toISO(),
|
||||
},
|
||||
{ filename: 'example.mp4' },
|
||||
),
|
||||
apiUtils.createAsset(user1.accessToken, {
|
||||
isFavorite: true,
|
||||
isReadOnly: true,
|
||||
fileCreatedAt: yesterday.toISO(),
|
||||
fileModifiedAt: yesterday.toISO(),
|
||||
assetData: { filename: 'example.mp4' },
|
||||
}),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
apiUtils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
|
||||
|
||||
for (const asset of [...user1Assets, ...user2Assets]) {
|
||||
expect(asset.duplicate).toBe(false);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
// stats
|
||||
apiUtils.createAsset(userStats.accessToken),
|
||||
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
|
||||
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
|
||||
apiUtils.createAsset(
|
||||
userStats.accessToken,
|
||||
{
|
||||
isArchived: true,
|
||||
isFavorite: true,
|
||||
},
|
||||
{ filename: 'example.mp4' },
|
||||
),
|
||||
apiUtils.createAsset(userStats.accessToken, {
|
||||
isArchived: true,
|
||||
isFavorite: true,
|
||||
assetData: { filename: 'example.mp4' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const person1 = await apiUtils.createPerson(user1.accessToken, {
|
||||
@@ -126,9 +115,7 @@ describe('/asset', () => {
|
||||
|
||||
describe('GET /asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/${uuidDto.notFound}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
expect(status).toBe(401);
|
||||
});
|
||||
@@ -163,9 +150,7 @@ describe('/asset', () => {
|
||||
assetIds: [user1Assets[0].id],
|
||||
});
|
||||
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id: user1Assets[0].id });
|
||||
});
|
||||
@@ -195,9 +180,7 @@ describe('/asset', () => {
|
||||
assetIds: [user1Assets[0].id],
|
||||
});
|
||||
|
||||
const data = await request(app).get(
|
||||
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
|
||||
);
|
||||
const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
|
||||
expect(data.status).toBe(200);
|
||||
expect(data.body).toMatchObject({ people: [] });
|
||||
});
|
||||
@@ -280,7 +263,7 @@ describe('/asset', () => {
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it.each(Array(10))('should return 1 random assets', async () => {
|
||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/random')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
@@ -290,14 +273,9 @@ describe('/asset', () => {
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(1);
|
||||
expect(assets[0].ownerId).toBe(user1.userId);
|
||||
|
||||
// assets owned by user1
|
||||
expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id);
|
||||
// assets owned by user2
|
||||
expect([user1Assets.map(({ id }) => id)]).not.toContain(assets[0].id);
|
||||
});
|
||||
|
||||
it.each(Array(10))('should return 2 random assets', async () => {
|
||||
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/asset/random?count=2')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
@@ -309,24 +287,18 @@ describe('/asset', () => {
|
||||
|
||||
for (const asset of assets) {
|
||||
expect(asset.ownerId).toBe(user1.userId);
|
||||
// assets owned by user1
|
||||
expect([user1Assets.map(({ id }) => id)]).toContain(asset.id);
|
||||
// assets owned by user2
|
||||
expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.id);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(Array(10))(
|
||||
it.each(TEN_TIMES)(
|
||||
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
||||
async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/[]asset/random')
|
||||
.get('/asset/random')
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({ id: user2Assets[0].id }),
|
||||
]);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -341,9 +313,7 @@ describe('/asset', () => {
|
||||
|
||||
describe('PUT /asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/asset/:${uuidDto.notFound}`,
|
||||
);
|
||||
const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -365,10 +335,7 @@ describe('/asset', () => {
|
||||
});
|
||||
|
||||
it('should favorite an asset', async () => {
|
||||
const before = await apiUtils.getAssetInfo(
|
||||
user1.accessToken,
|
||||
user1Assets[0].id,
|
||||
);
|
||||
const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
||||
expect(before.isFavorite).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
@@ -380,10 +347,7 @@ describe('/asset', () => {
|
||||
});
|
||||
|
||||
it('should archive an asset', async () => {
|
||||
const before = await apiUtils.getAssetInfo(
|
||||
user1.accessToken,
|
||||
user1Assets[0].id,
|
||||
);
|
||||
const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
||||
expect(before.isArchived).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
@@ -497,9 +461,7 @@ describe('/asset', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['each value in ids must be a UUID']),
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
|
||||
it('should throw an error when the id is not found', async () => {
|
||||
@@ -509,9 +471,7 @@ describe('/asset', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no asset.delete access'),
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
|
||||
});
|
||||
|
||||
it('should move an asset to the trash', async () => {
|
||||
@@ -647,11 +607,9 @@ describe('/asset', () => {
|
||||
for (const { input, expected } of tests) {
|
||||
it(`should generate a thumbnail for ${input}`, async () => {
|
||||
const filepath = join(testAssetDir, input);
|
||||
const { id, duplicate } = await apiUtils.createAsset(
|
||||
admin.accessToken,
|
||||
{},
|
||||
{ bytes: await readFile(filepath), filename: basename(filepath) },
|
||||
);
|
||||
const { id, duplicate } = await apiUtils.createAsset(admin.accessToken, {
|
||||
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
||||
});
|
||||
|
||||
expect(duplicate).toBe(false);
|
||||
|
||||
@@ -667,14 +625,12 @@ describe('/asset', () => {
|
||||
|
||||
it('should handle a duplicate', async () => {
|
||||
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
|
||||
const { duplicate } = await apiUtils.createAsset(
|
||||
admin.accessToken,
|
||||
{},
|
||||
{
|
||||
const { duplicate } = await apiUtils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
bytes: await readFile(join(testAssetDir, filepath)),
|
||||
filename: basename(filepath),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(duplicate).toBe(true);
|
||||
});
|
||||
@@ -701,29 +657,21 @@ describe('/asset', () => {
|
||||
|
||||
for (const { filepath, checksum } of motionTests) {
|
||||
it(`should extract motionphoto video from ${filepath}`, async () => {
|
||||
const response = await apiUtils.createAsset(
|
||||
admin.accessToken,
|
||||
{},
|
||||
{
|
||||
const response = await apiUtils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
bytes: await readFile(join(testAssetDir, filepath)),
|
||||
filename: basename(filepath),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await wsUtils.waitForEvent({ event: 'upload', assetId: response.id });
|
||||
|
||||
expect(response.duplicate).toBe(false);
|
||||
|
||||
const asset = await apiUtils.getAssetInfo(
|
||||
admin.accessToken,
|
||||
response.id,
|
||||
);
|
||||
const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id);
|
||||
expect(asset.livePhotoVideoId).toBeDefined();
|
||||
|
||||
const video = await apiUtils.getAssetInfo(
|
||||
admin.accessToken,
|
||||
asset.livePhotoVideoId as string,
|
||||
);
|
||||
const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
|
||||
expect(video.checksum).toStrictEqual(checksum);
|
||||
});
|
||||
}
|
||||
@@ -731,9 +679,7 @@ describe('/asset', () => {
|
||||
|
||||
describe('GET /asset/thumbnail/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/thumbnail/${assetLocation.id}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -775,9 +721,7 @@ describe('/asset', () => {
|
||||
|
||||
describe('GET /asset/file/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/asset/thumbnail/${assetLocation.id}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -792,10 +736,7 @@ describe('/asset', () => {
|
||||
expect(body).toBeDefined();
|
||||
expect(type).toBe('image/jpeg');
|
||||
|
||||
const asset = await apiUtils.getAssetInfo(
|
||||
admin.accessToken,
|
||||
assetLocation.id,
|
||||
);
|
||||
const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id);
|
||||
|
||||
const original = await readFile(locationAssetFilepath);
|
||||
const originalChecksum = sha1(original);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
deleteAssets,
|
||||
getAuditFiles,
|
||||
updateAsset,
|
||||
type LoginResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
|
||||
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -20,17 +15,14 @@ describe('/audit', () => {
|
||||
|
||||
describe('GET :/file-report', () => {
|
||||
it('excludes assets without issues from report', async () => {
|
||||
const [trashedAsset, archivedAsset, _] = await Promise.all([
|
||||
const [trashedAsset, archivedAsset] = await Promise.all([
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
deleteAssets(
|
||||
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
),
|
||||
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
updateAsset(
|
||||
{
|
||||
id: archivedAsset.id,
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import {
|
||||
LoginResponseDto,
|
||||
getAuthDevices,
|
||||
login,
|
||||
signUpAdmin,
|
||||
} from '@immich/sdk';
|
||||
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||
import {
|
||||
deviceDto,
|
||||
errorDto,
|
||||
loginResponseDto,
|
||||
signupResponseDto,
|
||||
} from 'src/responses';
|
||||
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
@@ -48,18 +38,14 @@ describe(`/auth/admin-sign-up`, () => {
|
||||
|
||||
for (const { should, data } of invalid) {
|
||||
it(`should ${should}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(data);
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
}
|
||||
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(signupDto.admin);
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(signupResponseDto.admin);
|
||||
});
|
||||
@@ -86,9 +72,7 @@ describe(`/auth/admin-sign-up`, () => {
|
||||
it('should not allow a second admin to sign up', async () => {
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(signupDto.admin);
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||
@@ -107,9 +91,7 @@ describe('/auth/*', () => {
|
||||
|
||||
describe(`POST /auth/login`, () => {
|
||||
it('should reject an incorrect password', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({ email, password: 'incorrect' });
|
||||
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.incorrectLogin);
|
||||
});
|
||||
@@ -125,9 +107,7 @@ describe('/auth/*', () => {
|
||||
}
|
||||
|
||||
it('should accept a correct password', async () => {
|
||||
const { status, body, headers } = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({ email, password });
|
||||
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(loginResponseDto.admin);
|
||||
|
||||
@@ -136,15 +116,9 @@ describe('/auth/*', () => {
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0]).toEqual(
|
||||
`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`
|
||||
);
|
||||
expect(cookies[1]).toEqual(
|
||||
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||
);
|
||||
expect(cookies[2]).toEqual(
|
||||
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||
);
|
||||
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
||||
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,18 +150,12 @@ describe('/auth/*', () => {
|
||||
await login({ loginCredentialDto: loginDto.admin });
|
||||
}
|
||||
|
||||
await expect(
|
||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||
).resolves.toHaveLength(6);
|
||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
|
||||
|
||||
const { status } = await request(app)
|
||||
.delete(`/auth/devices`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await expect(
|
||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||
).resolves.toHaveLength(1);
|
||||
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error for a non-existent device id', async () => {
|
||||
@@ -195,9 +163,7 @@ describe('/auth/*', () => {
|
||||
.delete(`/auth/devices/${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no authDevice.delete access')
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
|
||||
});
|
||||
|
||||
it('should logout a device', async () => {
|
||||
@@ -219,9 +185,7 @@ describe('/auth/*', () => {
|
||||
|
||||
describe('POST /auth/validateToken', () => {
|
||||
it('should reject an invalid token', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/validateToken`)
|
||||
.set('Authorization', 'Bearer 123');
|
||||
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidToken);
|
||||
});
|
||||
|
||||
@@ -42,9 +42,7 @@ describe('/download', () => {
|
||||
|
||||
describe('POST /download/asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(
|
||||
`/download/asset/${asset1.id}`,
|
||||
);
|
||||
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -56,7 +54,7 @@ describe('/download', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-type']).toEqual('image/jpeg');
|
||||
expect(response.headers['content-type']).toEqual('image/png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,16 +15,9 @@ describe(`/oauth`, () => {
|
||||
|
||||
describe('POST /oauth/authorize', () => {
|
||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/authorize')
|
||||
.send({});
|
||||
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'redirectUri must be a string',
|
||||
'redirectUri should not be empty',
|
||||
])
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,14 +24,8 @@ describe('/partner', () => {
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
createPartner(
|
||||
{ id: user2.userId },
|
||||
{ headers: asBearerAuth(user1.accessToken) }
|
||||
),
|
||||
createPartner(
|
||||
{ id: user1.userId },
|
||||
{ headers: asBearerAuth(user2.accessToken) }
|
||||
),
|
||||
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
|
||||
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -66,9 +60,7 @@ describe('/partner', () => {
|
||||
|
||||
describe('POST /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(
|
||||
`/partner/${user3.userId}`
|
||||
);
|
||||
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -89,17 +81,13 @@ describe('/partner', () => {
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Partner already exists' })
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/partner/${user2.userId}`
|
||||
);
|
||||
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -112,17 +100,13 @@ describe('/partner', () => {
|
||||
.send({ inTimeline: false });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ id: user2.userId, inTimeline: false })
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/partner/${user3.userId}`
|
||||
);
|
||||
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -142,9 +126,7 @@ describe('/partner', () => {
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Partner not found' })
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,9 +65,7 @@ describe('/activity', () => {
|
||||
});
|
||||
|
||||
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('/person').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
@@ -80,9 +78,7 @@ describe('/activity', () => {
|
||||
|
||||
describe('GET /person/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/person/${uuidDto.notFound}`
|
||||
);
|
||||
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -109,9 +105,7 @@ describe('/activity', () => {
|
||||
|
||||
describe('PUT /person/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(
|
||||
`/person/${uuidDto.notFound}`
|
||||
);
|
||||
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -139,7 +133,7 @@ describe('/activity', () => {
|
||||
birthDate: '123567',
|
||||
response: 'Not found or no person.write access',
|
||||
},
|
||||
{ birthDate: 123567, response: 'Not found or no person.write access' },
|
||||
{ birthDate: 123_567, response: 'Not found or no person.write access' },
|
||||
]) {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/person/${uuidDto.notFound}`)
|
||||
|
||||
@@ -97,9 +97,7 @@ describe('/server-info', () => {
|
||||
|
||||
describe('GET /server-info/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
'/server-info/statistics'
|
||||
);
|
||||
const { status, body } = await request(app).get('/server-info/statistics');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -145,9 +143,7 @@ describe('/server-info', () => {
|
||||
|
||||
describe('GET /server-info/media-types', () => {
|
||||
it('should return accepted media types', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
'/server-info/media-types'
|
||||
);
|
||||
const { status, body } = await request(app).get('/server-info/media-types');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
sidecar: ['.xmp'],
|
||||
|
||||
@@ -46,14 +46,8 @@ describe('/shared-link', () => {
|
||||
]);
|
||||
|
||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||
createAlbum(
|
||||
{ createAlbumDto: { albumName: 'album' } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
),
|
||||
createAlbum(
|
||||
{ createAlbumDto: { albumName: 'deleted album' } },
|
||||
{ headers: asBearerAuth(user2.accessToken) },
|
||||
),
|
||||
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
|
||||
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
createAlbum(
|
||||
{
|
||||
createAlbumDto: {
|
||||
@@ -65,47 +59,38 @@ describe('/shared-link', () => {
|
||||
),
|
||||
]);
|
||||
|
||||
[
|
||||
linkWithDeletedAlbum,
|
||||
linkWithAlbum,
|
||||
linkWithAssets,
|
||||
linkWithPassword,
|
||||
linkWithMetadata,
|
||||
linkWithoutMetadata,
|
||||
] = await Promise.all([
|
||||
apiUtils.createSharedLink(user2.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: deletedAlbum.id,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
password: 'foo',
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
showMetadata: true,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
showMetadata: false,
|
||||
}),
|
||||
]);
|
||||
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
|
||||
await Promise.all([
|
||||
apiUtils.createSharedLink(user2.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: deletedAlbum.id,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
password: 'foo',
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
showMetadata: true,
|
||||
}),
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
showMetadata: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
await deleteUser(
|
||||
{ id: user2.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
await deleteUser({ id: user2.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
});
|
||||
|
||||
describe('GET /shared-link', () => {
|
||||
@@ -146,17 +131,13 @@ describe('/shared-link', () => {
|
||||
|
||||
describe('GET /shared-link/me', () => {
|
||||
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-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(403);
|
||||
});
|
||||
|
||||
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-link/me').query({ key: linkWithAlbum.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
@@ -178,18 +159,14 @@ describe('/shared-link', () => {
|
||||
});
|
||||
|
||||
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-link/me').query({ key: linkWithDeletedAlbum.key });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidShareKey);
|
||||
});
|
||||
|
||||
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-link/me').query({ key: linkWithPassword.key });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidSharePassword);
|
||||
@@ -211,9 +188,7 @@ describe('/shared-link', () => {
|
||||
});
|
||||
|
||||
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-link/me').query({ key: linkWithMetadata.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(1);
|
||||
@@ -229,9 +204,7 @@ describe('/shared-link', () => {
|
||||
});
|
||||
|
||||
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-link/me').query({ key: linkWithoutMetadata.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(1);
|
||||
@@ -247,9 +220,7 @@ describe('/shared-link', () => {
|
||||
|
||||
describe('GET /shared-link/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
`/shared-link/${linkWithAlbum.id}`,
|
||||
);
|
||||
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
@@ -276,9 +247,7 @@ describe('/shared-link', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Shared link not found' }),
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,9 +277,7 @@ describe('/shared-link', () => {
|
||||
.send({ type: SharedLinkType.Album });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Invalid albumId' }),
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
@@ -320,9 +287,7 @@ describe('/shared-link', () => {
|
||||
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Invalid assetIds' }),
|
||||
);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
|
||||
});
|
||||
|
||||
it('should create a shared link', async () => {
|
||||
@@ -424,9 +389,7 @@ describe('/shared-link', () => {
|
||||
|
||||
describe('DELETE /shared-link/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/shared-link/${linkWithAlbum.id}`,
|
||||
);
|
||||
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
|
||||
@@ -18,9 +18,7 @@ describe('/system-config', () => {
|
||||
|
||||
describe('GET /system-config/map/style.json', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(
|
||||
'/system-config/map/style.json'
|
||||
);
|
||||
const { status, body } = await request(app).get('/system-config/map/style.json');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -32,11 +30,7 @@ describe('/system-config', () => {
|
||||
.query({ theme })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'theme must be one of the following values: light, dark',
|
||||
])
|
||||
);
|
||||
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,24 +32,16 @@ describe('/trash', () => {
|
||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
||||
|
||||
const before = await getAllAssets(
|
||||
{},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
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);
|
||||
|
||||
await wsUtils.waitForEvent({ event: 'delete', assetId });
|
||||
|
||||
const after = await getAllAssets(
|
||||
{},
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(after.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -69,9 +61,7 @@ describe('/trash', () => {
|
||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
expect(before.isTrashed).toBe(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);
|
||||
|
||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||
|
||||
@@ -22,10 +22,7 @@ describe('/server-info', () => {
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
]);
|
||||
|
||||
await deleteUser(
|
||||
{ id: deletedUser.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
});
|
||||
|
||||
describe('GET /user', () => {
|
||||
@@ -36,9 +33,7 @@ describe('/server-info', () => {
|
||||
});
|
||||
|
||||
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('/user').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toHaveLength(4);
|
||||
expect(body).toEqual(
|
||||
@@ -47,7 +42,7 @@ describe('/server-info', () => {
|
||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||
])
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -63,7 +58,7 @@ describe('/server-info', () => {
|
||||
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||
])
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,7 +76,7 @@ describe('/server-info', () => {
|
||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||
])
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -112,9 +107,7 @@ describe('/server-info', () => {
|
||||
});
|
||||
|
||||
it('should get my info', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/user/me`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
id: admin.userId,
|
||||
@@ -125,9 +118,7 @@ describe('/server-info', () => {
|
||||
|
||||
describe('POST /user', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/user`)
|
||||
.send(createUserDto.user1);
|
||||
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -181,9 +172,7 @@ describe('/server-info', () => {
|
||||
|
||||
describe('DELETE /user/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/user/${userToDelete.userId}`
|
||||
);
|
||||
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -241,10 +230,7 @@ describe('/server-info', () => {
|
||||
});
|
||||
|
||||
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
@@ -261,10 +247,7 @@ describe('/server-info', () => {
|
||||
});
|
||||
|
||||
it('should update first and last name', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
@@ -284,10 +267,7 @@ describe('/server-info', () => {
|
||||
});
|
||||
|
||||
it('should update memories enabled', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
|
||||
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich login-key`, () => {
|
||||
beforeAll(() => {
|
||||
@@ -24,25 +24,15 @@ describe(`immich login-key`, () => {
|
||||
});
|
||||
|
||||
it('should require a valid key', async () => {
|
||||
const { stderr, exitCode } = await immichCli([
|
||||
'login-key',
|
||||
app,
|
||||
'immich-is-so-cool',
|
||||
]);
|
||||
expect(stderr).toContain(
|
||||
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
|
||||
);
|
||||
const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
|
||||
expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401');
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('should login', async () => {
|
||||
const admin = await apiUtils.adminSetup();
|
||||
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||
const { stdout, stderr, exitCode } = await immichCli([
|
||||
'login-key',
|
||||
app,
|
||||
`${key.secret}`,
|
||||
]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in...',
|
||||
'Logged in as admin@immich.cloud',
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||
import {
|
||||
apiUtils,
|
||||
asKeyAuth,
|
||||
cliUtils,
|
||||
dbUtils,
|
||||
immichCli,
|
||||
testAssetDir,
|
||||
} from 'src/utils';
|
||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||
import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich upload`, () => {
|
||||
@@ -25,16 +18,10 @@ describe(`immich upload`, () => {
|
||||
|
||||
describe('immich upload --recursive', () => {
|
||||
it('should upload a folder recursively', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
]);
|
||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
@@ -70,15 +57,9 @@ describe(`immich upload`, () => {
|
||||
});
|
||||
|
||||
it('should add existing assets to albums', async () => {
|
||||
const response1 = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
]);
|
||||
const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||
expect(response1.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||
);
|
||||
expect(response1.stderr).toBe('');
|
||||
expect(response1.exitCode).toBe(0);
|
||||
@@ -89,17 +70,10 @@ describe(`immich upload`, () => {
|
||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums1.length).toBe(0);
|
||||
|
||||
const response2 = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
'--album',
|
||||
]);
|
||||
const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
|
||||
expect(response2.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'All assets were already uploaded, nothing to do.',
|
||||
),
|
||||
expect.stringContaining('All assets were already uploaded, nothing to do.'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
);
|
||||
@@ -147,17 +121,10 @@ describe(`immich upload`, () => {
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
||||
for (const file of filesToLink) {
|
||||
await symlink(
|
||||
`${testAssetDir}/albums/nature/${file}`,
|
||||
`/tmp/albums/nature/${file}`,
|
||||
);
|
||||
await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
|
||||
}
|
||||
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`/tmp/albums/nature`,
|
||||
'--delete',
|
||||
]);
|
||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
|
||||
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
|
||||
@@ -44,7 +44,6 @@ export const userDto = {
|
||||
email: signupDto.admin.email,
|
||||
password: signupDto.admin.password,
|
||||
storageLabel: 'admin',
|
||||
externalPath: null,
|
||||
oauthId: '',
|
||||
shouldChangePassword: false,
|
||||
profileImagePath: '',
|
||||
@@ -63,7 +62,6 @@ export const userDto = {
|
||||
email: createUserDto.user1.email,
|
||||
password: createUserDto.user1.password,
|
||||
storageLabel: null,
|
||||
externalPath: null,
|
||||
oauthId: '',
|
||||
shouldChangePassword: false,
|
||||
profileImagePath: '',
|
||||
|
||||
31
e2e/src/generators.ts
Normal file
31
e2e/src/generators.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { PNG } from 'pngjs';
|
||||
|
||||
const createPNG = (r: number, g: number, b: number) => {
|
||||
const image = new PNG({ width: 1, height: 1 });
|
||||
image.data[0] = r;
|
||||
image.data[1] = g;
|
||||
image.data[2] = b;
|
||||
image.data[3] = 255;
|
||||
return PNG.sync.write(image);
|
||||
};
|
||||
|
||||
function* newPngFactory() {
|
||||
for (let r = 0; r < 255; r++) {
|
||||
for (let g = 0; g < 255; g++) {
|
||||
for (let b = 0; b < 255; b++) {
|
||||
yield createPNG(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pngFactory = newPngFactory();
|
||||
|
||||
export const makeRandomImage = () => {
|
||||
const { value } = pngFactory.next();
|
||||
if (!value) {
|
||||
throw new Error('Ran out of random asset data');
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -65,7 +65,6 @@ export const signupResponseDto = {
|
||||
name: 'Immich Admin',
|
||||
email: 'admin@immich.cloud',
|
||||
storageLabel: 'admin',
|
||||
externalPath: null,
|
||||
profileImagePath: '',
|
||||
// why? lol
|
||||
shouldChangePassword: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, exec } from 'child_process';
|
||||
import { exec, spawn } from 'node:child_process';
|
||||
|
||||
export default async () => {
|
||||
let _resolve: () => unknown;
|
||||
@@ -19,8 +19,6 @@ export default async () => {
|
||||
await ready;
|
||||
|
||||
return async () => {
|
||||
await new Promise<void>((resolve) =>
|
||||
exec('docker compose down', () => resolve()),
|
||||
);
|
||||
await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
|
||||
};
|
||||
};
|
||||
|
||||
114
e2e/src/utils.ts
114
e2e/src/utils.ts
@@ -21,15 +21,14 @@ import {
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { exec, spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { access } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import request from 'supertest';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
@@ -70,20 +69,12 @@ let client: pg.Client | null = null;
|
||||
|
||||
export const fileUtils = {
|
||||
reset: async () => {
|
||||
await execPromise(
|
||||
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
|
||||
);
|
||||
await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
|
||||
},
|
||||
};
|
||||
|
||||
export const dbUtils = {
|
||||
createFace: async ({
|
||||
assetId,
|
||||
personId,
|
||||
}: {
|
||||
assetId: string;
|
||||
personId: string;
|
||||
}) => {
|
||||
createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
@@ -91,27 +82,23 @@ export const dbUtils = {
|
||||
const vector = Array.from({ length: 512 }, Math.random);
|
||||
const embedding = `[${vector.join(',')}]`;
|
||||
|
||||
await client.query(
|
||||
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
||||
[assetId, personId, embedding],
|
||||
);
|
||||
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
|
||||
assetId,
|
||||
personId,
|
||||
embedding,
|
||||
]);
|
||||
},
|
||||
setPersonThumbnail: async (personId: string) => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
||||
[personId],
|
||||
);
|
||||
await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
|
||||
},
|
||||
reset: async (tables?: string[]) => {
|
||||
try {
|
||||
if (!client) {
|
||||
client = new pg.Client(
|
||||
'postgres://postgres:postgres@127.0.0.1:5433/immich',
|
||||
);
|
||||
client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich');
|
||||
await client.connect();
|
||||
}
|
||||
|
||||
@@ -223,12 +210,8 @@ export const wsUtils = {
|
||||
return new Promise<Socket>((resolve) => {
|
||||
websocket
|
||||
.on('connect', () => resolve(websocket))
|
||||
.on('on_upload_success', (data: AssetResponseDto) =>
|
||||
onEvent({ event: 'upload', assetId: data.id }),
|
||||
)
|
||||
.on('on_asset_delete', (assetId: string) =>
|
||||
onEvent({ event: 'delete', assetId }),
|
||||
)
|
||||
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
|
||||
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
|
||||
.connect();
|
||||
});
|
||||
},
|
||||
@@ -241,21 +224,14 @@ export const wsUtils = {
|
||||
set.clear();
|
||||
}
|
||||
},
|
||||
waitForEvent: async ({
|
||||
event,
|
||||
assetId,
|
||||
timeout: ms,
|
||||
}: WaitOptions): Promise<void> => {
|
||||
waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
const set = events[event];
|
||||
if (set.has(assetId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error(`Timed out waiting for ${event} event`)),
|
||||
ms || 5000,
|
||||
);
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000);
|
||||
|
||||
callbacks[assetId] = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -265,6 +241,8 @@ export const wsUtils = {
|
||||
},
|
||||
};
|
||||
|
||||
type AssetData = { bytes?: Buffer; filename: string };
|
||||
|
||||
export const apiUtils = {
|
||||
setup: () => {
|
||||
defaults.baseUrl = app;
|
||||
@@ -281,86 +259,60 @@ export const apiUtils = {
|
||||
return response;
|
||||
},
|
||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||
await createUser(
|
||||
{ createUserDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
);
|
||||
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||
return login({
|
||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||
});
|
||||
},
|
||||
createApiKey: (accessToken: string) => {
|
||||
return createApiKey(
|
||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
);
|
||||
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||
createAlbum(
|
||||
{ createAlbumDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
),
|
||||
createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||
createAsset: async (
|
||||
accessToken: string,
|
||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
|
||||
data?: {
|
||||
bytes?: Buffer;
|
||||
filename?: string;
|
||||
},
|
||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
|
||||
) => {
|
||||
const _dto = {
|
||||
deviceAssetId: 'test-1',
|
||||
deviceId: 'test',
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
fileModifiedAt: new Date().toISOString(),
|
||||
...(dto || {}),
|
||||
...dto,
|
||||
};
|
||||
|
||||
const _assetData = {
|
||||
bytes: randomBytes(32),
|
||||
filename: 'example.jpg',
|
||||
...(data || {}),
|
||||
};
|
||||
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
||||
const filename = dto?.assetData?.filename || 'example.png';
|
||||
|
||||
const builder = request(app)
|
||||
.post(`/asset/upload`)
|
||||
.attach('assetData', _assetData.bytes, _assetData.filename)
|
||||
.attach('assetData', assetData, filename)
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
for (const [key, value] of Object.entries(_dto)) {
|
||||
builder.field(key, String(value));
|
||||
void builder.field(key, String(value));
|
||||
}
|
||||
|
||||
const { body } = await builder;
|
||||
|
||||
return body as AssetFileUploadResponseDto;
|
||||
},
|
||||
getAssetInfo: (accessToken: string, id: string) =>
|
||||
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
||||
deleteAssets(
|
||||
{ assetBulkDeleteDto: { ids } },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
),
|
||||
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
||||
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
||||
// TODO fix createPerson to accept a body
|
||||
let person = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||
const person = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||
await dbUtils.setPersonThumbnail(person.id);
|
||||
|
||||
if (!dto) {
|
||||
return person;
|
||||
}
|
||||
|
||||
return updatePerson(
|
||||
{ id: person.id, personUpdateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
);
|
||||
return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||
createSharedLink(
|
||||
{ sharedLinkCreateDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) },
|
||||
),
|
||||
createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||
};
|
||||
|
||||
export const cliUtils = {
|
||||
@@ -380,7 +332,7 @@ export const webUtils = {
|
||||
value: accessToken,
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
@@ -390,7 +342,7 @@ export const webUtils = {
|
||||
value: 'password',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
@@ -400,7 +352,7 @@ export const webUtils = {
|
||||
value: 'true',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { apiUtils, dbUtils, webUtils } from 'src/utils';
|
||||
|
||||
test.describe('Registration', () => {
|
||||
@@ -68,7 +68,7 @@ test.describe('Registration', () => {
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
// change password
|
||||
expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||
await expect(page).toHaveURL('/auth/change-password');
|
||||
await page.getByLabel('New Password').fill('new-password');
|
||||
await page.getByLabel('Confirm Password').fill('new-password');
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
AssetResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkType,
|
||||
createAlbum,
|
||||
createSharedLink,
|
||||
} from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
|
||||
|
||||
test.describe('Shared Links', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset: AssetResponseDto;
|
||||
let asset: AssetFileUploadResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
let sharedLink: SharedLinkResponseDto;
|
||||
let sharedLinkPassword: SharedLinkResponseDto;
|
||||
@@ -29,7 +28,7 @@ test.describe('Shared Links', () => {
|
||||
assetIds: [asset.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
{ headers: asBearerAuth(admin.accessToken) },
|
||||
);
|
||||
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
@@ -53,7 +52,7 @@ test.describe('Shared Links', () => {
|
||||
await page.waitForSelector('#asset-group-by-date svg');
|
||||
await page.getByRole('checkbox').click();
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING').waitFor();
|
||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('enter password for a shared link', async ({ page }) => {
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
"rootDirs": ["src"],
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
|
||||
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@@ -135,8 +135,8 @@ Class | Method | HTTP request | Description
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
||||
*LibraryApi* | [**deleteLibrary**](doc//LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
||||
*LibraryApi* | [**getLibraries**](doc//LibraryApi.md#getlibraries) | **GET** /library |
|
||||
*LibraryApi* | [**getLibraryInfo**](doc//LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
|
||||
*LibraryApi* | [**getAllLibraries**](doc//LibraryApi.md#getalllibraries) | **GET** /library |
|
||||
*LibraryApi* | [**getLibrary**](doc//LibraryApi.md#getlibrary) | **GET** /library/{id} |
|
||||
*LibraryApi* | [**getLibraryStatistics**](doc//LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
||||
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||
|
||||
1
mobile/openapi/doc/CreateLibraryDto.md
generated
1
mobile/openapi/doc/CreateLibraryDto.md
generated
@@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
||||
**isVisible** | **bool** | | [optional]
|
||||
**isWatched** | **bool** | | [optional]
|
||||
**name** | **String** | | [optional]
|
||||
**ownerId** | **String** | | [optional]
|
||||
**type** | [**LibraryType**](LibraryType.md) | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
1
mobile/openapi/doc/CreateUserDto.md
generated
1
mobile/openapi/doc/CreateUserDto.md
generated
@@ -9,7 +9,6 @@ import 'package:openapi/api.dart';
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**email** | **String** | |
|
||||
**externalPath** | **String** | | [optional]
|
||||
**memoriesEnabled** | **bool** | | [optional]
|
||||
**name** | **String** | |
|
||||
**password** | **String** | |
|
||||
|
||||
26
mobile/openapi/doc/LibraryApi.md
generated
26
mobile/openapi/doc/LibraryApi.md
generated
@@ -11,8 +11,8 @@ Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**createLibrary**](LibraryApi.md#createlibrary) | **POST** /library |
|
||||
[**deleteLibrary**](LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
||||
[**getLibraries**](LibraryApi.md#getlibraries) | **GET** /library |
|
||||
[**getLibraryInfo**](LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
|
||||
[**getAllLibraries**](LibraryApi.md#getalllibraries) | **GET** /library |
|
||||
[**getLibrary**](LibraryApi.md#getlibrary) | **GET** /library/{id} |
|
||||
[**getLibraryStatistics**](LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
||||
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||
@@ -129,8 +129,8 @@ void (empty response body)
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getLibraries**
|
||||
> List<LibraryResponseDto> getLibraries()
|
||||
# **getAllLibraries**
|
||||
> List<LibraryResponseDto> getAllLibraries(type)
|
||||
|
||||
|
||||
|
||||
@@ -153,17 +153,21 @@ import 'package:openapi/api.dart';
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = LibraryApi();
|
||||
final type = ; // LibraryType |
|
||||
|
||||
try {
|
||||
final result = api_instance.getLibraries();
|
||||
final result = api_instance.getAllLibraries(type);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling LibraryApi->getLibraries: $e\n');
|
||||
print('Exception when calling LibraryApi->getAllLibraries: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**type** | [**LibraryType**](.md)| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
@@ -180,8 +184,8 @@ This endpoint does not need any parameter.
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getLibraryInfo**
|
||||
> LibraryResponseDto getLibraryInfo(id)
|
||||
# **getLibrary**
|
||||
> LibraryResponseDto getLibrary(id)
|
||||
|
||||
|
||||
|
||||
@@ -207,10 +211,10 @@ final api_instance = LibraryApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getLibraryInfo(id);
|
||||
final result = api_instance.getLibrary(id);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling LibraryApi->getLibraryInfo: $e\n');
|
||||
print('Exception when calling LibraryApi->getLibrary: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
1
mobile/openapi/doc/PartnerResponseDto.md
generated
1
mobile/openapi/doc/PartnerResponseDto.md
generated
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
|
||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||
**email** | **String** | |
|
||||
**externalPath** | **String** | |
|
||||
**id** | **String** | |
|
||||
**inTimeline** | **bool** | | [optional]
|
||||
**isAdmin** | **bool** | |
|
||||
|
||||
1
mobile/openapi/doc/UpdateUserDto.md
generated
1
mobile/openapi/doc/UpdateUserDto.md
generated
@@ -10,7 +10,6 @@ Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional]
|
||||
**email** | **String** | | [optional]
|
||||
**externalPath** | **String** | | [optional]
|
||||
**id** | **String** | |
|
||||
**isAdmin** | **bool** | | [optional]
|
||||
**memoriesEnabled** | **bool** | | [optional]
|
||||
|
||||
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
|
||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||
**email** | **String** | |
|
||||
**externalPath** | **String** | |
|
||||
**id** | **String** | |
|
||||
**isAdmin** | **bool** | |
|
||||
**memoriesEnabled** | **bool** | | [optional]
|
||||
|
||||
22
mobile/openapi/lib/api/library_api.dart
generated
22
mobile/openapi/lib/api/library_api.dart
generated
@@ -104,7 +104,10 @@ class LibraryApi {
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /library' operation and returns the [Response].
|
||||
Future<Response> getLibrariesWithHttpInfo() async {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LibraryType] type:
|
||||
Future<Response> getAllLibrariesWithHttpInfo({ LibraryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/library';
|
||||
|
||||
@@ -115,6 +118,10 @@ class LibraryApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -129,8 +136,11 @@ class LibraryApi {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<LibraryResponseDto>?> getLibraries() async {
|
||||
final response = await getLibrariesWithHttpInfo();
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [LibraryType] type:
|
||||
Future<List<LibraryResponseDto>?> getAllLibraries({ LibraryType? type, }) async {
|
||||
final response = await getAllLibrariesWithHttpInfo( type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -151,7 +161,7 @@ class LibraryApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getLibraryInfoWithHttpInfo(String id,) async {
|
||||
Future<Response> getLibraryWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/library/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
@@ -180,8 +190,8 @@ class LibraryApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<LibraryResponseDto?> getLibraryInfo(String id,) async {
|
||||
final response = await getLibraryInfoWithHttpInfo(id,);
|
||||
Future<LibraryResponseDto?> getLibrary(String id,) async {
|
||||
final response = await getLibraryWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
19
mobile/openapi/lib/model/create_library_dto.dart
generated
19
mobile/openapi/lib/model/create_library_dto.dart
generated
@@ -18,6 +18,7 @@ class CreateLibraryDto {
|
||||
this.isVisible,
|
||||
this.isWatched,
|
||||
this.name,
|
||||
this.ownerId,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@@ -49,6 +50,14 @@ class CreateLibraryDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? ownerId;
|
||||
|
||||
LibraryType type;
|
||||
|
||||
@override
|
||||
@@ -58,6 +67,7 @@ class CreateLibraryDto {
|
||||
other.isVisible == isVisible &&
|
||||
other.isWatched == isWatched &&
|
||||
other.name == name &&
|
||||
other.ownerId == ownerId &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
@@ -68,10 +78,11 @@ class CreateLibraryDto {
|
||||
(isVisible == null ? 0 : isVisible!.hashCode) +
|
||||
(isWatched == null ? 0 : isWatched!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(ownerId == null ? 0 : ownerId!.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, type=$type]';
|
||||
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, ownerId=$ownerId, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -91,6 +102,11 @@ class CreateLibraryDto {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.ownerId != null) {
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
} else {
|
||||
// json[r'ownerId'] = null;
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
@@ -113,6 +129,7 @@ class CreateLibraryDto {
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
||||
isWatched: mapValueOfType<bool>(json, r'isWatched'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId'),
|
||||
type: LibraryType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
|
||||
13
mobile/openapi/lib/model/create_user_dto.dart
generated
13
mobile/openapi/lib/model/create_user_dto.dart
generated
@@ -14,7 +14,6 @@ class CreateUserDto {
|
||||
/// Returns a new [CreateUserDto] instance.
|
||||
CreateUserDto({
|
||||
required this.email,
|
||||
this.externalPath,
|
||||
this.memoriesEnabled,
|
||||
required this.name,
|
||||
required this.password,
|
||||
@@ -24,8 +23,6 @@ class CreateUserDto {
|
||||
|
||||
String email;
|
||||
|
||||
String? externalPath;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -45,7 +42,6 @@ class CreateUserDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CreateUserDto &&
|
||||
other.email == email &&
|
||||
other.externalPath == externalPath &&
|
||||
other.memoriesEnabled == memoriesEnabled &&
|
||||
other.name == name &&
|
||||
other.password == password &&
|
||||
@@ -56,7 +52,6 @@ class CreateUserDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(email.hashCode) +
|
||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||
(name.hashCode) +
|
||||
(password.hashCode) +
|
||||
@@ -64,16 +59,11 @@ class CreateUserDto {
|
||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
|
||||
String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'email'] = this.email;
|
||||
if (this.externalPath != null) {
|
||||
json[r'externalPath'] = this.externalPath;
|
||||
} else {
|
||||
// json[r'externalPath'] = null;
|
||||
}
|
||||
if (this.memoriesEnabled != null) {
|
||||
json[r'memoriesEnabled'] = this.memoriesEnabled;
|
||||
} else {
|
||||
@@ -103,7 +93,6 @@ class CreateUserDto {
|
||||
|
||||
return CreateUserDto(
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
password: mapValueOfType<String>(json, r'password')!,
|
||||
|
||||
14
mobile/openapi/lib/model/partner_response_dto.dart
generated
14
mobile/openapi/lib/model/partner_response_dto.dart
generated
@@ -17,7 +17,6 @@ class PartnerResponseDto {
|
||||
required this.createdAt,
|
||||
required this.deletedAt,
|
||||
required this.email,
|
||||
required this.externalPath,
|
||||
required this.id,
|
||||
this.inTimeline,
|
||||
required this.isAdmin,
|
||||
@@ -40,8 +39,6 @@ class PartnerResponseDto {
|
||||
|
||||
String email;
|
||||
|
||||
String? externalPath;
|
||||
|
||||
String id;
|
||||
|
||||
///
|
||||
@@ -84,7 +81,6 @@ class PartnerResponseDto {
|
||||
other.createdAt == createdAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.email == email &&
|
||||
other.externalPath == externalPath &&
|
||||
other.id == id &&
|
||||
other.inTimeline == inTimeline &&
|
||||
other.isAdmin == isAdmin &&
|
||||
@@ -105,7 +101,6 @@ class PartnerResponseDto {
|
||||
(createdAt.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(email.hashCode) +
|
||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(inTimeline == null ? 0 : inTimeline!.hashCode) +
|
||||
(isAdmin.hashCode) +
|
||||
@@ -120,7 +115,7 @@ class PartnerResponseDto {
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -132,11 +127,6 @@ class PartnerResponseDto {
|
||||
// json[r'deletedAt'] = null;
|
||||
}
|
||||
json[r'email'] = this.email;
|
||||
if (this.externalPath != null) {
|
||||
json[r'externalPath'] = this.externalPath;
|
||||
} else {
|
||||
// json[r'externalPath'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
if (this.inTimeline != null) {
|
||||
json[r'inTimeline'] = this.inTimeline;
|
||||
@@ -184,7 +174,6 @@ class PartnerResponseDto {
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
|
||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||
@@ -248,7 +237,6 @@ class PartnerResponseDto {
|
||||
'createdAt',
|
||||
'deletedAt',
|
||||
'email',
|
||||
'externalPath',
|
||||
'id',
|
||||
'isAdmin',
|
||||
'name',
|
||||
|
||||
19
mobile/openapi/lib/model/update_user_dto.dart
generated
19
mobile/openapi/lib/model/update_user_dto.dart
generated
@@ -15,7 +15,6 @@ class UpdateUserDto {
|
||||
UpdateUserDto({
|
||||
this.avatarColor,
|
||||
this.email,
|
||||
this.externalPath,
|
||||
required this.id,
|
||||
this.isAdmin,
|
||||
this.memoriesEnabled,
|
||||
@@ -42,14 +41,6 @@ class UpdateUserDto {
|
||||
///
|
||||
String? email;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? externalPath;
|
||||
|
||||
String id;
|
||||
|
||||
///
|
||||
@@ -106,7 +97,6 @@ class UpdateUserDto {
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.externalPath == externalPath &&
|
||||
other.id == id &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.memoriesEnabled == memoriesEnabled &&
|
||||
@@ -121,7 +111,6 @@ class UpdateUserDto {
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||
(email == null ? 0 : email!.hashCode) +
|
||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isAdmin == null ? 0 : isAdmin!.hashCode) +
|
||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||
@@ -132,7 +121,7 @@ class UpdateUserDto {
|
||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||
String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -145,11 +134,6 @@ class UpdateUserDto {
|
||||
json[r'email'] = this.email;
|
||||
} else {
|
||||
// json[r'email'] = null;
|
||||
}
|
||||
if (this.externalPath != null) {
|
||||
json[r'externalPath'] = this.externalPath;
|
||||
} else {
|
||||
// json[r'externalPath'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
if (this.isAdmin != null) {
|
||||
@@ -200,7 +184,6 @@ class UpdateUserDto {
|
||||
return UpdateUserDto(
|
||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||
email: mapValueOfType<String>(json, r'email'),
|
||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
|
||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||
|
||||
14
mobile/openapi/lib/model/user_response_dto.dart
generated
14
mobile/openapi/lib/model/user_response_dto.dart
generated
@@ -17,7 +17,6 @@ class UserResponseDto {
|
||||
required this.createdAt,
|
||||
required this.deletedAt,
|
||||
required this.email,
|
||||
required this.externalPath,
|
||||
required this.id,
|
||||
required this.isAdmin,
|
||||
this.memoriesEnabled,
|
||||
@@ -39,8 +38,6 @@ class UserResponseDto {
|
||||
|
||||
String email;
|
||||
|
||||
String? externalPath;
|
||||
|
||||
String id;
|
||||
|
||||
bool isAdmin;
|
||||
@@ -75,7 +72,6 @@ class UserResponseDto {
|
||||
other.createdAt == createdAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.email == email &&
|
||||
other.externalPath == externalPath &&
|
||||
other.id == id &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.memoriesEnabled == memoriesEnabled &&
|
||||
@@ -95,7 +91,6 @@ class UserResponseDto {
|
||||
(createdAt.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(email.hashCode) +
|
||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isAdmin.hashCode) +
|
||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||
@@ -109,7 +104,7 @@ class UserResponseDto {
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -121,11 +116,6 @@ class UserResponseDto {
|
||||
// json[r'deletedAt'] = null;
|
||||
}
|
||||
json[r'email'] = this.email;
|
||||
if (this.externalPath != null) {
|
||||
json[r'externalPath'] = this.externalPath;
|
||||
} else {
|
||||
// json[r'externalPath'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isAdmin'] = this.isAdmin;
|
||||
if (this.memoriesEnabled != null) {
|
||||
@@ -168,7 +158,6 @@ class UserResponseDto {
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||
@@ -231,7 +220,6 @@ class UserResponseDto {
|
||||
'createdAt',
|
||||
'deletedAt',
|
||||
'email',
|
||||
'externalPath',
|
||||
'id',
|
||||
'isAdmin',
|
||||
'name',
|
||||
|
||||
5
mobile/openapi/test/create_library_dto_test.dart
generated
5
mobile/openapi/test/create_library_dto_test.dart
generated
@@ -41,6 +41,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String ownerId
|
||||
test('to test the property `ownerId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// LibraryType type
|
||||
test('to test the property `type`', () async {
|
||||
// TODO
|
||||
|
||||
5
mobile/openapi/test/create_user_dto_test.dart
generated
5
mobile/openapi/test/create_user_dto_test.dart
generated
@@ -21,11 +21,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String externalPath
|
||||
test('to test the property `externalPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool memoriesEnabled
|
||||
test('to test the property `memoriesEnabled`', () async {
|
||||
// TODO
|
||||
|
||||
8
mobile/openapi/test/library_api_test.dart
generated
8
mobile/openapi/test/library_api_test.dart
generated
@@ -27,13 +27,13 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<LibraryResponseDto>> getLibraries() async
|
||||
test('test getLibraries', () async {
|
||||
//Future<List<LibraryResponseDto>> getAllLibraries({ LibraryType type }) async
|
||||
test('test getAllLibraries', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<LibraryResponseDto> getLibraryInfo(String id) async
|
||||
test('test getLibraryInfo', () async {
|
||||
//Future<LibraryResponseDto> getLibrary(String id) async
|
||||
test('test getLibrary', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
@@ -36,11 +36,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String externalPath
|
||||
test('to test the property `externalPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
|
||||
5
mobile/openapi/test/update_user_dto_test.dart
generated
5
mobile/openapi/test/update_user_dto_test.dart
generated
@@ -26,11 +26,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String externalPath
|
||||
test('to test the property `externalPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
|
||||
5
mobile/openapi/test/user_response_dto_test.dart
generated
5
mobile/openapi/test/user_response_dto_test.dart
generated
@@ -36,11 +36,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String externalPath
|
||||
test('to test the property `externalPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
|
||||
@@ -17,9 +17,7 @@ function dart {
|
||||
}
|
||||
|
||||
function typescript {
|
||||
rm -rf ./typescript-sdk/client
|
||||
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ./typescript-sdk/axios-client --additional-properties=useSingleRequestParameter=true,supportsES6=true
|
||||
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/fetch-client.ts
|
||||
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
||||
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
|
||||
}
|
||||
|
||||
|
||||
@@ -3299,8 +3299,17 @@
|
||||
},
|
||||
"/library": {
|
||||
"get": {
|
||||
"operationId": "getLibraries",
|
||||
"parameters": [],
|
||||
"operationId": "getAllLibraries",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LibraryType"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -3407,7 +3416,7 @@
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getLibraryInfo",
|
||||
"operationId": "getLibrary",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
@@ -7592,6 +7601,10 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/LibraryType"
|
||||
}
|
||||
@@ -7648,10 +7661,6 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"memoriesEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -8549,10 +8558,6 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8601,7 +8606,6 @@
|
||||
"createdAt",
|
||||
"deletedAt",
|
||||
"email",
|
||||
"externalPath",
|
||||
"id",
|
||||
"isAdmin",
|
||||
"name",
|
||||
@@ -10326,9 +10330,6 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
@@ -10455,10 +10456,6 @@
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"externalPath": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10504,7 +10501,6 @@
|
||||
"createdAt",
|
||||
"deletedAt",
|
||||
"email",
|
||||
"externalPath",
|
||||
"id",
|
||||
"isAdmin",
|
||||
"name",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
||||
@@ -1 +0,0 @@
|
||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
||||
@@ -1,23 +0,0 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
@@ -1,8 +0,0 @@
|
||||
.gitignore
|
||||
.npmignore
|
||||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
||||
@@ -1 +0,0 @@
|
||||
7.2.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.97.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from './configuration';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
|
||||
export const BASE_PATH = "/api".replace(/\/+$/, "");
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const COLLECTION_FORMATS = {
|
||||
csv: ",",
|
||||
ssv: " ",
|
||||
tsv: "\t",
|
||||
pipes: "|",
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RequestArgs
|
||||
*/
|
||||
export interface RequestArgs {
|
||||
url: string;
|
||||
options: RawAxiosRequestConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class BaseAPI
|
||||
*/
|
||||
export class BaseAPI {
|
||||
protected configuration: Configuration | undefined;
|
||||
|
||||
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
|
||||
if (configuration) {
|
||||
this.configuration = configuration;
|
||||
this.basePath = configuration.basePath ?? basePath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class RequiredError
|
||||
* @extends {Error}
|
||||
*/
|
||||
export class RequiredError extends Error {
|
||||
constructor(public field: string, msg?: string) {
|
||||
super(msg);
|
||||
this.name = "RequiredError"
|
||||
}
|
||||
}
|
||||
|
||||
interface ServerMap {
|
||||
[key: string]: {
|
||||
url: string,
|
||||
description: string,
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const operationServerMap: ServerMap = {
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.97.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from "./configuration";
|
||||
import type { RequestArgs } from "./base";
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { RequiredError } from "./base";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const DUMMY_BASE_URL = 'https://example.com'
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws {RequiredError}
|
||||
* @export
|
||||
*/
|
||||
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
|
||||
if (paramValue === null || paramValue === undefined) {
|
||||
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
|
||||
if (configuration && configuration.apiKey) {
|
||||
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
|
||||
? await configuration.apiKey(keyParamName)
|
||||
: await configuration.apiKey;
|
||||
object[keyParamName] = localVarApiKeyValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
|
||||
if (configuration && (configuration.username || configuration.password)) {
|
||||
object["auth"] = { username: configuration.username, password: configuration.password };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const accessToken = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken()
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken(name, scopes)
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
|
||||
}
|
||||
}
|
||||
|
||||
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
|
||||
if (parameter == null) return;
|
||||
if (typeof parameter === "object") {
|
||||
if (Array.isArray(parameter)) {
|
||||
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
|
||||
}
|
||||
else {
|
||||
Object.keys(parameter).forEach(currentKey =>
|
||||
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (urlSearchParams.has(key)) {
|
||||
urlSearchParams.append(key, parameter);
|
||||
}
|
||||
else {
|
||||
urlSearchParams.set(key, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
setFlattenedQueryParams(searchParams, objects);
|
||||
url.search = searchParams.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
|
||||
const nonString = typeof value !== 'string';
|
||||
const needsSerialization = nonString && configuration && configuration.isJsonMime
|
||||
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
|
||||
: nonString;
|
||||
return needsSerialization
|
||||
? JSON.stringify(value !== undefined ? value : {})
|
||||
: (value || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const toPathString = function (url: URL) {
|
||||
return url.pathname + url.search + url.hash
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
|
||||
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};
|
||||
return axios.request<T, R>(axiosRequestArgs);
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.97.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export interface ConfigurationParameters {
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
basePath?: string;
|
||||
serverIndex?: number;
|
||||
baseOptions?: any;
|
||||
formDataCtor?: new () => any;
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
/**
|
||||
* parameter for apiKey security
|
||||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* parameter for oauth2 security
|
||||
* @param name security name
|
||||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string;
|
||||
/**
|
||||
* override server index
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
serverIndex?: number;
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
baseOptions?: any;
|
||||
/**
|
||||
* The FormData constructor that will be used to create multipart form data
|
||||
* requests. You can inject this here so that execution environments that
|
||||
* do not support the FormData class can still run the generated client.
|
||||
*
|
||||
* @type {new () => FormData}
|
||||
*/
|
||||
formDataCtor?: new () => any;
|
||||
|
||||
constructor(param: ConfigurationParameters = {}) {
|
||||
this.apiKey = param.apiKey;
|
||||
this.username = param.username;
|
||||
this.password = param.password;
|
||||
this.accessToken = param.accessToken;
|
||||
this.basePath = param.basePath;
|
||||
this.serverIndex = param.serverIndex;
|
||||
this.baseOptions = param.baseOptions;
|
||||
this.formDataCtor = param.formDataCtor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given MIME is a JSON MIME.
|
||||
* JSON MIME examples:
|
||||
* application/json
|
||||
* application/json; charset=UTF8
|
||||
* APPLICATION/JSON
|
||||
* application/vnd.company+json
|
||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
|
||||
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||
#
|
||||
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
||||
|
||||
git_user_id=$1
|
||||
git_repo_id=$2
|
||||
release_note=$3
|
||||
git_host=$4
|
||||
|
||||
if [ "$git_host" = "" ]; then
|
||||
git_host="github.com"
|
||||
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
||||
fi
|
||||
|
||||
if [ "$git_user_id" = "" ]; then
|
||||
git_user_id="GIT_USER_ID"
|
||||
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||
fi
|
||||
|
||||
if [ "$git_repo_id" = "" ]; then
|
||||
git_repo_id="GIT_REPO_ID"
|
||||
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||
fi
|
||||
|
||||
if [ "$release_note" = "" ]; then
|
||||
release_note="Minor update"
|
||||
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||
fi
|
||||
|
||||
# Initialize the local directory as a Git repository
|
||||
git init
|
||||
|
||||
# Adds the files in the local repository and stages them for commit.
|
||||
git add .
|
||||
|
||||
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||
git commit -m "$release_note"
|
||||
|
||||
# Sets the new remote
|
||||
git_remote=$(git remote)
|
||||
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||
|
||||
if [ "$GIT_TOKEN" = "" ]; then
|
||||
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
||||
else
|
||||
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
git pull origin master
|
||||
|
||||
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
||||
git push origin master 2>&1 | grep -v 'To https'
|
||||
@@ -1,18 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.97.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export * from "./api";
|
||||
export * from "./configuration";
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './axios-client';
|
||||
export * as base from './axios-client/base';
|
||||
export * as configuration from './axios-client/configuration';
|
||||
export * as common from './axios-client/common';
|
||||
116
open-api/typescript-sdk/package-lock.json
generated
116
open-api/typescript-sdk/package-lock.json
generated
@@ -12,14 +12,6 @@
|
||||
"@oazapfts/runtime": "^1.0.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.6.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"axios": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@oazapfts/runtime": {
|
||||
@@ -37,114 +29,6 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
|
||||
@@ -3,33 +3,21 @@
|
||||
"version": "1.92.1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./build/fetch/index.js",
|
||||
"types": "./build/fetch/index.d.ts",
|
||||
"main": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
"./axios": {
|
||||
"types": "./build/axios/axios.d.ts",
|
||||
"default": "./build/axios/axios.js"
|
||||
},
|
||||
".": {
|
||||
"types": "./build/fetch/fetch.d.ts",
|
||||
"default": "./build/fetch/fetch.js"
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b ./tsconfig.axios.json ./tsconfig.fetch.json"
|
||||
"build": "tsc"
|
||||
},
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@oazapfts/runtime": "^1.0.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.6.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"axios": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ export type UserResponseDto = {
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
email: string;
|
||||
externalPath: string | null;
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
memoriesEnabled?: boolean;
|
||||
@@ -462,6 +461,7 @@ export type CreateLibraryDto = {
|
||||
isVisible?: boolean;
|
||||
isWatched?: boolean;
|
||||
name?: string;
|
||||
ownerId?: string;
|
||||
"type": LibraryType;
|
||||
};
|
||||
export type UpdateLibraryDto = {
|
||||
@@ -506,7 +506,6 @@ export type PartnerResponseDto = {
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
email: string;
|
||||
externalPath: string | null;
|
||||
id: string;
|
||||
inTimeline?: boolean;
|
||||
isAdmin: boolean;
|
||||
@@ -950,7 +949,6 @@ export type UpdateTagDto = {
|
||||
};
|
||||
export type CreateUserDto = {
|
||||
email: string;
|
||||
externalPath?: string | null;
|
||||
memoriesEnabled?: boolean;
|
||||
name: string;
|
||||
password: string;
|
||||
@@ -960,7 +958,6 @@ export type CreateUserDto = {
|
||||
export type UpdateUserDto = {
|
||||
avatarColor?: UserAvatarColor;
|
||||
email?: string;
|
||||
externalPath?: string;
|
||||
id: string;
|
||||
isAdmin?: boolean;
|
||||
memoriesEnabled?: boolean;
|
||||
@@ -1841,11 +1838,15 @@ export function sendJobCommand({ id, jobCommandDto }: {
|
||||
body: jobCommandDto
|
||||
})));
|
||||
}
|
||||
export function getLibraries(opts?: Oazapfts.RequestOpts) {
|
||||
export function getAllLibraries({ $type }: {
|
||||
$type?: LibraryType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: LibraryResponseDto[];
|
||||
}>("/library", {
|
||||
}>(`/library${QS.query(QS.explode({
|
||||
"type": $type
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
@@ -1869,7 +1870,7 @@ export function deleteLibrary({ id }: {
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getLibraryInfo({ id }: {
|
||||
export function getLibrary({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"include": ["axios.ts", "axios-client/**/*"],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "build/axios",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["esnext"]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"include": ["fetch.ts", "fetch-client.ts"],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "build/fetch",
|
||||
"outDir": "build",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["esnext", "dom"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -41,6 +41,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||
let app: INestApplication;
|
||||
let server: any;
|
||||
let assetRepository: IAssetRepository;
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
let userWithQuota: LoginResponseDto;
|
||||
@@ -72,7 +73,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||
await testApp.reset();
|
||||
|
||||
await api.authApi.adminSignUp(server);
|
||||
const admin = await api.authApi.adminLogin(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
|
||||
await Promise.all([
|
||||
api.userApi.create(server, admin.accessToken, userDto.user1),
|
||||
@@ -86,12 +87,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||
api.authApi.login(server, userDto.userWithQuota),
|
||||
]);
|
||||
|
||||
const [user1Libraries, user2Libraries] = await Promise.all([
|
||||
api.libraryApi.getAll(server, user1.accessToken),
|
||||
api.libraryApi.getAll(server, user2.accessToken),
|
||||
]);
|
||||
|
||||
libraries = [...user1Libraries, ...user2Libraries];
|
||||
libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -615,7 +611,7 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||
|
||||
it("should not upload to another user's library", async () => {
|
||||
const content = randomBytes(32);
|
||||
const [library] = await api.libraryApi.getAll(server, user2.accessToken);
|
||||
const [library] = await api.libraryApi.getAll(server, admin.accessToken);
|
||||
await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
|
||||
|
||||
const { body, status } = await request(server)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { testApp } from '../utils';
|
||||
describe(`${LibraryController.name} (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await testApp.create();
|
||||
@@ -25,6 +26,9 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
await testApp.reset();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
|
||||
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
||||
user = await api.authApi.login(server, userDto.user1);
|
||||
});
|
||||
|
||||
describe('GET /library', () => {
|
||||
@@ -39,18 +43,19 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
.get('/library')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body).toEqual([
|
||||
expect.objectContaining({
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.UPLOAD,
|
||||
name: 'Default Library',
|
||||
refreshedAt: null,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
]);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.UPLOAD,
|
||||
name: 'Default Library',
|
||||
refreshedAt: null,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +66,16 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require admin authentication', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({ type: LibraryType.EXTERNAL });
|
||||
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorStub.forbidden);
|
||||
});
|
||||
|
||||
it('should create an external library with defaults', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/library')
|
||||
@@ -184,29 +199,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have exclusion patterns'));
|
||||
});
|
||||
|
||||
it('should allow a non-admin to create a library', async () => {
|
||||
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
||||
const user1 = await api.authApi.login(server, userDto.user1);
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.post('/library')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ type: LibraryType.EXTERNAL });
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
type: LibraryType.EXTERNAL,
|
||||
name: 'New External Library',
|
||||
refreshedAt: null,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /library/:id', () => {
|
||||
@@ -249,7 +241,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should change the import paths', async () => {
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH);
|
||||
const { status, body } = await request(server)
|
||||
.put(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
@@ -327,6 +318,14 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require admin access', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.get(`/library/${uuidStub.notFound}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorStub.forbidden);
|
||||
});
|
||||
|
||||
it('should get library by id', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||
|
||||
@@ -347,27 +346,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not allow getting another user's library", async () => {
|
||||
await Promise.all([
|
||||
api.userApi.create(server, admin.accessToken, userDto.user1),
|
||||
api.userApi.create(server, admin.accessToken, userDto.user2),
|
||||
]);
|
||||
|
||||
const [user1, user2] = await Promise.all([
|
||||
api.authApi.login(server, userDto.user1),
|
||||
api.authApi.login(server, userDto.user2),
|
||||
]);
|
||||
|
||||
const library = await api.libraryApi.create(server, user1.accessToken, { type: LibraryType.EXTERNAL });
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.get(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest('Not found or no library.read access'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /library/:id', () => {
|
||||
@@ -390,7 +368,7 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(body).toEqual(errorStub.noDeleteUploadLibrary);
|
||||
});
|
||||
|
||||
it('should delete an empty library', async () => {
|
||||
it('should delete an external library', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||
|
||||
const { status, body } = await request(server)
|
||||
@@ -401,7 +379,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
||||
expect(libraries).toHaveLength(1);
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -455,74 +432,42 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL });
|
||||
});
|
||||
|
||||
it('should fail with no external path set', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post(`/library/${library.id}/validate`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
|
||||
.send({ importPaths: [] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest('User has no external path set'));
|
||||
it('should pass with no import paths', async () => {
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
|
||||
expect(response.importPaths).toEqual([]);
|
||||
});
|
||||
|
||||
describe('With external path set', () => {
|
||||
beforeEach(async () => {
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, IMMICH_TEST_ASSET_TEMP_PATH);
|
||||
it('should fail if path does not exist', async () => {
|
||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||
importPaths: [pathToTest],
|
||||
});
|
||||
|
||||
it('should pass with no import paths', async () => {
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, { importPaths: [] });
|
||||
expect(response.importPaths).toEqual([]);
|
||||
expect(response.importPaths?.length).toEqual(1);
|
||||
const pathResponse = response?.importPaths?.at(0);
|
||||
|
||||
expect(pathResponse).toEqual({
|
||||
importPath: pathToTest,
|
||||
isValid: false,
|
||||
message: `Path does not exist (ENOENT)`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if path is a file', async () => {
|
||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||
importPaths: [pathToTest],
|
||||
});
|
||||
|
||||
it('should not allow paths outside of the external path', async () => {
|
||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/../`;
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||
importPaths: [pathToTest],
|
||||
});
|
||||
expect(response.importPaths?.length).toEqual(1);
|
||||
const pathResponse = response?.importPaths?.at(0);
|
||||
expect(response.importPaths?.length).toEqual(1);
|
||||
const pathResponse = response?.importPaths?.at(0);
|
||||
|
||||
expect(pathResponse).toEqual({
|
||||
importPath: pathToTest,
|
||||
isValid: false,
|
||||
message: `Not contained in user's external path`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if path does not exist', async () => {
|
||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||
importPaths: [pathToTest],
|
||||
});
|
||||
|
||||
expect(response.importPaths?.length).toEqual(1);
|
||||
const pathResponse = response?.importPaths?.at(0);
|
||||
|
||||
expect(pathResponse).toEqual({
|
||||
importPath: pathToTest,
|
||||
isValid: false,
|
||||
message: `Path does not exist (ENOENT)`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if path is a file', async () => {
|
||||
const pathToTest = `${IMMICH_TEST_ASSET_TEMP_PATH}/does/not/exist`;
|
||||
|
||||
const response = await api.libraryApi.validate(server, admin.accessToken, library.id, {
|
||||
importPaths: [pathToTest],
|
||||
});
|
||||
|
||||
expect(response.importPaths?.length).toEqual(1);
|
||||
const pathResponse = response?.importPaths?.at(0);
|
||||
|
||||
expect(pathResponse).toEqual({
|
||||
importPath: pathToTest,
|
||||
isValid: false,
|
||||
message: `Path does not exist (ENOENT)`,
|
||||
});
|
||||
expect(pathResponse).toEqual({
|
||||
importPath: pathToTest,
|
||||
isValid: false,
|
||||
message: `Path does not exist (ENOENT)`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ import { InfraModule, InfraTestModule, dataSource } from '@app/infra';
|
||||
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { DateTime } from 'luxon';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
import { AppService } from '../../src/microservices/app.service';
|
||||
import { newJobRepositoryMock, newMetadataRepositoryMock } from '../../test';
|
||||
|
||||
@@ -26,7 +26,12 @@ export const userApi = {
|
||||
|
||||
return body as UserResponseDto;
|
||||
},
|
||||
setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => {
|
||||
return await userApi.update(server, accessToken, { id, externalPath });
|
||||
delete: async (server: any, accessToken: string, id: string) => {
|
||||
const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id, deletedAt: expect.any(String) });
|
||||
|
||||
return body as UserResponseDto;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
command: ['/usr/src/app/bin/immich-test', 'jobs']
|
||||
volumes:
|
||||
- /usr/src/app/node_modules
|
||||
- ../test/assets:/usr/src/app/test/assets:ro
|
||||
environment:
|
||||
- DB_HOSTNAME=database
|
||||
- DB_USERNAME=postgres
|
||||
|
||||
@@ -30,8 +30,6 @@ describe(`Library watcher (e2e)`, () => {
|
||||
await restoreTempFolder();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -205,8 +203,6 @@ describe(`Library watcher (e2e)`, () => {
|
||||
],
|
||||
});
|
||||
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
||||
|
||||
@@ -40,8 +40,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
@@ -79,8 +77,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
@@ -118,16 +114,12 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should scan external library with exclusion pattern', async () => {
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path');
|
||||
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
exclusionPatterns: ['**/el_corcal*'],
|
||||
});
|
||||
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
@@ -163,7 +155,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
@@ -190,39 +181,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should offline files outside of changed external path', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/some/other/path');
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
|
||||
expect(assets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
isOffline: true,
|
||||
originalFileName: 'el_torcal_rocks',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
isOffline: true,
|
||||
originalFileName: 'tanners_ridge',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should scan new files', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await fs.promises.cp(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`,
|
||||
@@ -258,7 +221,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await fs.promises.cp(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
@@ -305,7 +267,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await fs.promises.cp(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
@@ -345,7 +306,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await fs.promises.cp(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
@@ -387,72 +347,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('External path', () => {
|
||||
let library: LibraryResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not scan assets for user without external path', async () => {
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
|
||||
expect(assets).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not import assets outside of user's external path", async () => {
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path');
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(assets).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_PATH}/albums/nature/`])(
|
||||
'should scan external library with external path %s',
|
||||
async (externalPath: string) => {
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, externalPath);
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
|
||||
expect(assets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: AssetType.IMAGE,
|
||||
originalFileName: 'el_torcal_rocks',
|
||||
libraryId: library.id,
|
||||
resized: true,
|
||||
exifInfo: expect.objectContaining({
|
||||
exifImageWidth: 512,
|
||||
exifImageHeight: 341,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: AssetType.IMAGE,
|
||||
originalFileName: 'silver_fir',
|
||||
libraryId: library.id,
|
||||
resized: true,
|
||||
exifInfo: expect.objectContaining({
|
||||
exifImageWidth: 511,
|
||||
exifImageHeight: 323,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should not scan an upload library', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.UPLOAD,
|
||||
@@ -484,7 +378,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
@@ -506,12 +399,11 @@ describe(`${LibraryController.name} (e2e)`, () => {
|
||||
expect(assets).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not remvove online files', async () => {
|
||||
it('should not remove online files', async () => {
|
||||
const library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
||||
});
|
||||
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/');
|
||||
|
||||
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
||||
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
import { LibraryEntity, LibraryType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { ValidateUUID } from '../domain.util';
|
||||
import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Optional, ValidateUUID } from '../domain.util';
|
||||
|
||||
export class CreateLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
type!: LibraryType;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
ownerId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
isVisible?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ArrayUnique()
|
||||
@ArrayMaxSize(128)
|
||||
importPaths?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ArrayUnique()
|
||||
@ArrayMaxSize(128)
|
||||
exclusionPatterns?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
isWatched?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateLibraryDto {
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
isVisible?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ArrayUnique()
|
||||
@ArrayMaxSize(128)
|
||||
importPaths?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsNotEmpty({ each: true })
|
||||
@IsString({ each: true })
|
||||
@ArrayUnique()
|
||||
@@ -68,14 +71,14 @@ export class CrawlOptionsDto {
|
||||
}
|
||||
|
||||
export class ValidateLibraryDto {
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ArrayUnique()
|
||||
@ArrayMaxSize(128)
|
||||
importPaths?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
@IsNotEmpty({ each: true })
|
||||
@IsString({ each: true })
|
||||
@ArrayUnique()
|
||||
@@ -100,14 +103,21 @@ export class LibrarySearchDto {
|
||||
|
||||
export class ScanLibraryDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
refreshModifiedFiles?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
refreshAllFiles?: boolean = false;
|
||||
}
|
||||
|
||||
export class SearchLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
@Optional()
|
||||
type?: LibraryType;
|
||||
}
|
||||
|
||||
export class LibraryResponseDto {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
|
||||
@@ -156,24 +156,6 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
describe('handleQueueAssetRefresh', () => {
|
||||
it("should not queue assets outside of user's external path", async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
refreshModifiedFiles: false,
|
||||
refreshAllFiles: false,
|
||||
};
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
storageMock.crawl.mockResolvedValue(['/data/user2/photo.jpg']);
|
||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
||||
|
||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('should queue new assets', async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
@@ -184,8 +166,7 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
|
||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||
|
||||
@@ -212,8 +193,7 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
||||
userMock.get.mockResolvedValue(userStub.externalPath1);
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
|
||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||
|
||||
@@ -230,45 +210,6 @@ describe(LibraryService.name, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should mark assets outside of the user's external path as offline", async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
refreshModifiedFiles: false,
|
||||
refreshAllFiles: false,
|
||||
};
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']);
|
||||
assetMock.getByLibraryId.mockResolvedValue([assetStub.external]);
|
||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
||||
userMock.get.mockResolvedValue(userStub.externalPath2);
|
||||
|
||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||
|
||||
expect(assetMock.updateAll.mock.calls).toEqual([
|
||||
[
|
||||
[assetStub.external.id],
|
||||
{
|
||||
isOffline: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not scan libraries owned by user without external path', async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
refreshModifiedFiles: false,
|
||||
refreshAllFiles: false,
|
||||
};
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should not scan upload libraries', async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
@@ -303,7 +244,6 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
storageMock.crawl.mockResolvedValue([]);
|
||||
assetMock.getByLibraryId.mockResolvedValue([]);
|
||||
libraryMock.getOnlineAssetPaths.mockResolvedValue([]);
|
||||
userMock.get.mockResolvedValue(userStub.externalPathRoot);
|
||||
|
||||
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
||||
@@ -319,7 +259,7 @@ describe(LibraryService.name, () => {
|
||||
let mockUser: UserEntity;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUser = userStub.externalPath1;
|
||||
mockUser = userStub.admin;
|
||||
userMock.get.mockResolvedValue(mockUser);
|
||||
|
||||
storageMock.stat.mockResolvedValue({
|
||||
@@ -796,26 +736,6 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllForUser', () => {
|
||||
it('should return all libraries for user', async () => {
|
||||
libraryMock.getAllByUserId.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
||||
await expect(sut.getAllForUser(authStub.admin)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: libraryStub.uploadLibrary1.id,
|
||||
name: libraryStub.uploadLibrary1.name,
|
||||
ownerId: libraryStub.uploadLibrary1.ownerId,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should return library statistics', async () => {
|
||||
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
||||
@@ -1160,12 +1080,12 @@ describe(LibraryService.name, () => {
|
||||
storageMock.checkFileExists.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.external1, authStub.external1.user.id, { importPaths: ['/data/user1/foo'] }),
|
||||
sut.update(authStub.admin, authStub.admin.user.id, { importPaths: ['/data/user1/foo'] }),
|
||||
).resolves.toEqual(mapLibrary(libraryStub.externalLibraryWithImportPaths1));
|
||||
|
||||
expect(libraryMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: authStub.external1.user.id,
|
||||
id: authStub.admin.user.id,
|
||||
}),
|
||||
);
|
||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||
@@ -1600,26 +1520,6 @@ describe(LibraryService.name, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error when no external path is set', async () => {
|
||||
await expect(
|
||||
sut.validate(authStub.admin, libraryStub.externalLibrary1.id, { importPaths: ['/photos'] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should detect when path is outside external path', async () => {
|
||||
const result = await sut.validate(authStub.external1, libraryStub.externalLibraryWithImportPaths1.id, {
|
||||
importPaths: ['/data/user2'],
|
||||
});
|
||||
|
||||
expect(result.importPaths).toEqual([
|
||||
{
|
||||
importPath: '/data/user2',
|
||||
isValid: false,
|
||||
message: "Not contained in user's external path",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect when path does not exist', async () => {
|
||||
storageMock.stat.mockImplementation(() => {
|
||||
const error = { code: 'ENOENT' } as any;
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
LibraryResponseDto,
|
||||
LibraryStatsResponseDto,
|
||||
ScanLibraryDto,
|
||||
SearchLibraryDto,
|
||||
UpdateLibraryDto,
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryImportPathResponseDto,
|
||||
@@ -191,6 +192,7 @@ export class LibraryService extends EventEmitter {
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
||||
|
||||
return this.repository.getStatistics(id);
|
||||
}
|
||||
|
||||
@@ -198,17 +200,18 @@ export class LibraryService extends EventEmitter {
|
||||
return this.repository.getCountForUser(auth.user.id);
|
||||
}
|
||||
|
||||
async getAllForUser(auth: AuthDto): Promise<LibraryResponseDto[]> {
|
||||
const libraries = await this.repository.getAllByUserId(auth.user.id);
|
||||
return libraries.map((library) => mapLibrary(library));
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
|
||||
|
||||
const library = await this.findOrFail(id);
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
async getAll(auth: AuthDto, dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
||||
const libraries = await this.repository.getAll(false, dto.type);
|
||||
return libraries.map((library) => mapLibrary(library));
|
||||
}
|
||||
|
||||
async handleQueueCleanup(): Promise<boolean> {
|
||||
this.logger.debug('Cleaning up any pending library deletions');
|
||||
const pendingDeletion = await this.repository.getAllDeleted();
|
||||
@@ -243,8 +246,14 @@ export class LibraryService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
let ownerId = auth.user.id;
|
||||
|
||||
if (dto.ownerId) {
|
||||
ownerId = dto.ownerId;
|
||||
}
|
||||
|
||||
const library = await this.repository.create({
|
||||
ownerId: auth.user.id,
|
||||
ownerId,
|
||||
name: dto.name,
|
||||
type: dto.type,
|
||||
importPaths: dto.importPaths ?? [],
|
||||
@@ -309,24 +318,11 @@ export class LibraryService extends EventEmitter {
|
||||
public async validate(auth: AuthDto, id: string, dto: ValidateLibraryDto): Promise<ValidateLibraryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
||||
|
||||
if (!auth.user.externalPath) {
|
||||
throw new BadRequestException('User has no external path set');
|
||||
}
|
||||
|
||||
const response = new ValidateLibraryResponseDto();
|
||||
|
||||
if (dto.importPaths) {
|
||||
response.importPaths = await Promise.all(
|
||||
dto.importPaths.map(async (importPath) => {
|
||||
const normalizedPath = path.normalize(importPath);
|
||||
|
||||
if (!this.isInExternalPath(normalizedPath, auth.user.externalPath)) {
|
||||
const validation = new ValidateLibraryImportPathResponseDto();
|
||||
validation.importPath = importPath;
|
||||
validation.message = `Not contained in user's external path`;
|
||||
return validation;
|
||||
}
|
||||
|
||||
return await this.validateImportPath(importPath);
|
||||
}),
|
||||
);
|
||||
@@ -337,6 +333,7 @@ export class LibraryService extends EventEmitter {
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
||||
await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
|
||||
|
||||
const library = await this.repository.update({ id, ...dto });
|
||||
|
||||
if (dto.importPaths) {
|
||||
@@ -413,7 +410,7 @@ export class LibraryService extends EventEmitter {
|
||||
return true;
|
||||
} else {
|
||||
// File can't be accessed and does not already exist in db
|
||||
throw new BadRequestException("Can't access file", { cause: error });
|
||||
throw new BadRequestException('Cannot access file', { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,12 +597,6 @@ export class LibraryService extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.get(library.ownerId, {});
|
||||
if (!user?.externalPath) {
|
||||
this.logger.warn('User has no external path set, cannot refresh library');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Refreshing library: ${job.id}`);
|
||||
|
||||
const pathValidation = await Promise.all(
|
||||
@@ -627,11 +618,7 @@ export class LibraryService extends EventEmitter {
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
});
|
||||
|
||||
const crawledAssetPaths = rawPaths
|
||||
// Normalize file paths. This is important to prevent security issues like path traversal
|
||||
.map((filePath) => path.normalize(filePath))
|
||||
// Filter out paths that are not within the user's external path
|
||||
.filter((assetPath) => this.isInExternalPath(assetPath, user.externalPath)) as string[];
|
||||
const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath));
|
||||
|
||||
this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
|
||||
const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
|
||||
|
||||
@@ -18,7 +18,6 @@ const responseDto = {
|
||||
createdAt: new Date('2021-01-01'),
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
externalPath: null,
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
quotaSizeInBytes: null,
|
||||
@@ -37,7 +36,6 @@ const responseDto = {
|
||||
createdAt: new Date('2021-01-01'),
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
externalPath: null,
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
inTimeline: true,
|
||||
|
||||
@@ -5,7 +5,6 @@ export const ILibraryRepository = 'ILibraryRepository';
|
||||
|
||||
export interface ILibraryRepository {
|
||||
getCountForUser(ownerId: string): Promise<number>;
|
||||
getAllByUserId(userId: string, type?: LibraryType): Promise<LibraryEntity[]>;
|
||||
getAll(withDeleted?: boolean, type?: LibraryType): Promise<LibraryEntity[]>;
|
||||
getAllDeleted(): Promise<LibraryEntity[]>;
|
||||
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
|
||||
@@ -16,7 +15,5 @@ export interface ILibraryRepository {
|
||||
getUploadLibraryCount(ownerId: string): Promise<number>;
|
||||
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||
getStatistics(id: string): Promise<LibraryStatsResponseDto>;
|
||||
getOnlineAssetPaths(id: string): Promise<string[]>;
|
||||
getAssetIds(id: string, withDeleted?: boolean): Promise<string[]>;
|
||||
existsByName(name: string, withDeleted?: boolean): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Optional } from '../../domain.util';
|
||||
|
||||
export enum SearchSuggestionType {
|
||||
COUNTRY = 'country',
|
||||
@@ -16,18 +17,18 @@ export class SearchSuggestionRequestDto {
|
||||
type!: SearchSuggestionType;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
country?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
state?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
make?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Optional()
|
||||
model?: string;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ export class CreateUserDto {
|
||||
@Transform(toSanitized)
|
||||
storageLabel?: string | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsString()
|
||||
externalPath?: string | null;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
memoriesEnabled?: boolean;
|
||||
|
||||
@@ -25,10 +25,6 @@ export class UpdateUserDto {
|
||||
@Transform(toSanitized)
|
||||
storageLabel?: string;
|
||||
|
||||
@Optional()
|
||||
@IsString()
|
||||
externalPath?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsUUID('4')
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
|
||||
@@ -22,7 +22,6 @@ export class UserDto {
|
||||
|
||||
export class UserResponseDto extends UserDto {
|
||||
storageLabel!: string | null;
|
||||
externalPath!: string | null;
|
||||
shouldChangePassword!: boolean;
|
||||
isAdmin!: boolean;
|
||||
createdAt!: Date;
|
||||
@@ -50,7 +49,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
return {
|
||||
...mapSimpleUser(entity),
|
||||
storageLabel: entity.storageLabel,
|
||||
externalPath: entity.externalPath,
|
||||
shouldChangePassword: entity.shouldChangePassword,
|
||||
isAdmin: entity.isAdmin,
|
||||
createdAt: entity.createdAt,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LibraryType, UserEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import path from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
|
||||
import { UserResponseDto } from './response-dto';
|
||||
@@ -42,7 +41,6 @@ export class UserCore {
|
||||
// Users can never update the isAdmin property.
|
||||
delete dto.isAdmin;
|
||||
delete dto.storageLabel;
|
||||
delete dto.externalPath;
|
||||
} else if (dto.isAdmin && user.id !== id) {
|
||||
// Admin cannot create another admin.
|
||||
throw new BadRequestException('The server already has an admin');
|
||||
@@ -70,12 +68,6 @@ export class UserCore {
|
||||
dto.storageLabel = null;
|
||||
}
|
||||
|
||||
if (dto.externalPath === '') {
|
||||
dto.externalPath = null;
|
||||
} else if (dto.externalPath) {
|
||||
dto.externalPath = path.normalize(dto.externalPath);
|
||||
}
|
||||
|
||||
return this.userRepository.update(id, dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
LibraryStatsResponseDto,
|
||||
LibraryResponseDto as ResponseDto,
|
||||
ScanLibraryDto,
|
||||
SearchLibraryDto,
|
||||
UpdateLibraryDto as UpdateDto,
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryResponseDto,
|
||||
} from '@app/domain';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Auth, Authenticated } from '../app.guard';
|
||||
import { AdminRoute, Auth, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@@ -19,12 +20,13 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
@Controller('library')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
@AdminRoute()
|
||||
export class LibraryController {
|
||||
constructor(private service: LibraryService) {}
|
||||
|
||||
@Get()
|
||||
getLibraries(@Auth() auth: AuthDto): Promise<ResponseDto[]> {
|
||||
return this.service.getAllForUser(auth);
|
||||
getAllLibraries(@Auth() auth: AuthDto, @Query() dto: SearchLibraryDto): Promise<ResponseDto[]> {
|
||||
return this.service.getAll(auth, dto);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@@ -38,7 +40,7 @@ export class LibraryController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
||||
getLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ export class UserEntity {
|
||||
@Column({ type: 'varchar', unique: true, default: null })
|
||||
storageLabel!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', default: null })
|
||||
externalPath!: string | null;
|
||||
|
||||
@Column({ default: '', select: false })
|
||||
password?: string;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveExternalPath1708425975121 implements MigrationInterface {
|
||||
name = 'RemoveExternalPath1708425975121';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalPath"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "externalPath" character varying`);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -37,7 +36,6 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -97,7 +95,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -113,7 +110,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -155,7 +151,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -171,7 +166,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -285,7 +279,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -313,7 +306,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -358,7 +350,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -386,7 +377,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -468,7 +458,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
|
||||
@@ -496,7 +485,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
@@ -559,7 +547,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
|
||||
@@ -15,7 +15,6 @@ FROM
|
||||
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
|
||||
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
|
||||
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
|
||||
"APIKeyEntity__APIKeyEntity_user"."externalPath" AS "APIKeyEntity__APIKeyEntity_user_externalPath",
|
||||
"APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId",
|
||||
"APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath",
|
||||
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user