diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 3da7cbe107..e522854059 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -37,15 +37,15 @@ jobs: - uses: actions/setup-java@v4 with: - distribution: "zulu" - java-version: "11.0.21+9" - cache: "gradle" + distribution: 'zulu' + java-version: '17' + cache: 'gradle' - name: Setup Flutter SDK uses: subosito/flutter-action@v2 with: - channel: "stable" - flutter-version: "3.19.3" + channel: 'stable' + flutter-version: '3.19.3' cache: true - name: Create the Keystore diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d74c961393..51b947418e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -208,7 +208,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.16.9' + flutter-version: '3.19.3' - name: Run tests working-directory: ./mobile run: flutter test -j 1 diff --git a/cli/Dockerfile b/cli/Dockerfile index 18f38fb4ab..17799b8506 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core +FROM node:20-alpine3.19@sha256:ec0c413b1d84f3f7f67ec986ba885930c57b5318d2eb3abc6960ee05d4f2eb28 as core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index 11154fec52..5075514973 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,15 +47,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 8b85b00284..1b56637baf 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -97,7 +97,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 + image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 database: container_name: immich_postgres diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 7f1f0f1a41..49b375b5f6 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -54,7 +54,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 + image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 restart: always database: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9516ea9ca7..731bd4c90a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -58,7 +58,7 @@ services: redis: container_name: immich_redis - image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 restart: always database: diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index b273f27712..6fcc47d6a4 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured Hostname - - `https://immich.example.com/auth/login`) - - `https://immich.example.com/user-settings`) + - `https://immich.example.com/auth/login` + - `https://immich.example.com/user-settings` ## Enable OAuth diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index a890d674bc..256f3619f1 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -120,7 +120,8 @@ The default configuration looks like this: "previewFormat": "jpeg", "previewSize": 1440, "quality": 80, - "colorspace": "p3" + "colorspace": "p3", + "extractEmbedded": false }, "newVersionCheck": { "enabled": true diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index b52fd4649a..6b733faa55 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -1,4 +1,4 @@ -Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level. +Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user). :::note new version On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further). diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index e10f22156f..85b201f98c 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -36,7 +36,7 @@ services: <<: *server-common redis: - image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 + image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6cd8dd90ec..4643b8b01f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,15 +81,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/e2e/package.json b/e2e/package.json index 9023de8162..34ef229a2a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "description": "", "main": "index.js", "type": "module", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 53c4c43468..d18dc2532c 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -572,6 +572,22 @@ describe('/asset', () => { } const tests = [ + { + input: 'formats/avif/8bit-sRGB.avif', + expected: { + type: AssetTypeEnum.Image, + originalFileName: '8bit-sRGB.avif', + resized: true, + exifInfo: { + description: '', + exifImageHeight: 1080, + exifImageWidth: 1617, + fileSizeInByte: 862_424, + latitude: null, + longitude: null, + }, + }, + }, { input: 'formats/jpg/el_torcal_rocks.jpg', expected: { @@ -596,6 +612,22 @@ describe('/asset', () => { }, }, }, + { + input: 'formats/jxl/8bit-sRGB.jxl', + expected: { + type: AssetTypeEnum.Image, + originalFileName: '8bit-sRGB.jxl', + resized: true, + exifInfo: { + description: '', + exifImageHeight: 1080, + exifImageWidth: 1440, + fileSizeInByte: 1_780_777, + latitude: null, + longitude: null, + }, + }, + }, { input: 'formats/heic/IMG_2682.heic', expected: { @@ -681,6 +713,80 @@ describe('/asset', () => { }, }, }, + { + input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2', + expected: { + type: AssetTypeEnum.Image, + originalFileName: '4_3.rw2', + resized: true, + fileCreatedAt: '2018-05-10T08:42:37.842Z', + exifInfo: { + make: 'Panasonic', + model: 'DMC-GH4', + exifImageHeight: 3456, + exifImageWidth: 4608, + exposureTime: '1/100', + fNumber: 3.2, + focalLength: 35, + iso: 400, + fileSizeInByte: 19_587_072, + dateTimeOriginal: '2018-05-10T08:42:37.842Z', + latitude: null, + longitude: null, + orientation: '1', + }, + }, + }, + { + input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw', + expected: { + type: AssetTypeEnum.Image, + originalFileName: '12bit-compressed-(3_2).arw', + resized: true, + fileCreatedAt: '2016-09-27T10:51:44.000Z', + exifInfo: { + make: 'SONY', + model: 'ILCE-6300', + exifImageHeight: 4024, + exifImageWidth: 6048, + exposureTime: '1/320', + fNumber: 8, + focalLength: 97, + iso: 100, + lensModel: 'E PZ 18-105mm F4 G OSS', + fileSizeInByte: 25_001_984, + dateTimeOriginal: '2016-09-27T10:51:44.000Z', + latitude: null, + longitude: null, + orientation: '1', + }, + }, + }, + { + input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw', + expected: { + type: AssetTypeEnum.Image, + originalFileName: '14bit-uncompressed-(3_2).arw', + resized: true, + fileCreatedAt: '2016-01-08T15:08:01.000Z', + exifInfo: { + make: 'SONY', + model: 'ILCE-7M2', + exifImageHeight: 4024, + exifImageWidth: 6048, + exposureTime: '1.3', + fNumber: 22, + focalLength: 25, + iso: 100, + lensModel: 'E 25mm F2', + fileSizeInByte: 49_512_448, + dateTimeOriginal: '2016-01-08T15:08:01.000Z', + latitude: null, + longitude: null, + orientation: '1', + }, + }, + }, ]; for (const { input, expected } of tests) { diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index 28445f79d9..9174128bb8 100644 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ b/e2e/src/api/specs/auth.e2e-spec.ts @@ -1,7 +1,7 @@ -import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk'; -import { loginDto, signupDto, uuidDto } from 'src/fixtures'; -import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; -import { app, asBearerAuth, utils } from 'src/utils'; +import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk'; +import { loginDto, signupDto } from 'src/fixtures'; +import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeEach, describe, expect, it } from 'vitest'; @@ -112,70 +112,29 @@ 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;'); - }); - }); - - describe('GET /auth/devices', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/auth/devices'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should get a list of authorized devices', async () => { - const { status, body } = await request(app) - .get('/auth/devices') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual([deviceDto.current]); - }); - }); - - describe('DELETE /auth/devices', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/auth/devices`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should logout all devices (except the current one)', async () => { - for (let i = 0; i < 5; i++) { - await login({ loginCredentialDto: loginDto.admin }); - } - - await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6); - - 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); - }); - - it('should throw an error for a non-existent device id', async () => { - const { status, body } = await request(app) - .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')); - }); - - it('should logout a device', async () => { - const [device] = await getAuthDevices({ - headers: asBearerAuth(admin.accessToken), - }); - const { status } = await request(app) - .delete(`/auth/devices/${device.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - - const response = await request(app) - .post('/auth/validateToken') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(response.body).toEqual(errorDto.invalidToken); - expect(response.status).toBe(401); + expect(cookies[0].split(';').map((item) => item.trim())).toEqual([ + `immich_access_token=${token}`, + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[1].split(';').map((item) => item.trim())).toEqual([ + 'immich_auth_type=password', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[2].split(';').map((item) => item.trim())).toEqual([ + 'immich_is_authenticated=true', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'SameSite=Lax', + ]); }); }); diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 5cfd6a8b98..690bfae744 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, utils } from 'src/utils'; @@ -162,19 +162,4 @@ describe('/server-info', () => { }); }); }); - - describe('POST /server-info/admin-onboarding', () => { - it('should set admin onboarding', async () => { - const config = await getServerConfig({}); - expect(config.isOnboarded).toBe(false); - - const { status } = await request(app) - .post('/server-info/admin-onboarding') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - - const newConfig = await getServerConfig({}); - expect(newConfig.isOnboarded).toBe(true); - }); - }); }); diff --git a/e2e/src/api/specs/session.e2e-spec.ts b/e2e/src/api/specs/session.e2e-spec.ts new file mode 100644 index 0000000000..0b632f78ba --- /dev/null +++ b/e2e/src/api/specs/session.e2e-spec.ts @@ -0,0 +1,75 @@ +import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk'; +import { loginDto, signupDto, uuidDto } from 'src/fixtures'; +import { deviceDto, errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeEach, describe, expect, it } from 'vitest'; + +describe('/sessions', () => { + let admin: LoginResponseDto; + + beforeEach(async () => { + await utils.resetDatabase(); + await signUpAdmin({ signUpDto: signupDto.admin }); + admin = await login({ loginCredentialDto: loginDto.admin }); + }); + + describe('GET /sessions', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/sessions'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get a list of authorized devices', async () => { + const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual([deviceDto.current]); + }); + }); + + describe('DELETE /sessions', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/sessions`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should logout all devices (except the current one)', async () => { + for (let i = 0; i < 5; i++) { + await login({ loginCredentialDto: loginDto.admin }); + } + + await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6); + + const { status } = await request(app).delete(`/sessions`).set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1); + }); + + it('should throw an error for a non-existent device id', async () => { + const { status, body } = await request(app) + .delete(`/sessions/${uuidDto.notFound}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access')); + }); + + it('should logout a device', async () => { + const [device] = await getSessions({ + headers: asBearerAuth(admin.accessToken), + }); + const { status } = await request(app) + .delete(`/sessions/${device.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + const response = await request(app) + .post('/auth/validateToken') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(response.body).toEqual(errorDto.invalidToken); + expect(response.status).toBe(401); + }); + }); +}); diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/api/specs/system-metadata.e2e-spec.ts new file mode 100644 index 0000000000..bd17bf2524 --- /dev/null +++ b/e2e/src/api/specs/system-metadata.e2e-spec.ts @@ -0,0 +1,76 @@ +import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { createUserDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/server-info', () => { + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + describe('POST /system-metadata/admin-onboarding', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should set admin onboarding', async () => { + const config = await getServerConfig({}); + expect(config.isOnboarded).toBe(false); + + const { status } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(204); + + const newConfig = await getServerConfig({}); + expect(newConfig.isOnboarded).toBe(true); + }); + }); + + describe('GET /system-metadata/reverse-geocoding-state', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should get the reverse geocoding state', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + lastUpdate: expect.any(String), + lastImportFileName: 'cities500.txt', + }); + }); + }); +}); diff --git a/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts b/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts new file mode 100644 index 0000000000..707093ab25 --- /dev/null +++ b/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts @@ -0,0 +1,19 @@ +import { immichAdmin, utils } from 'src/utils'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe(`immich-admin`, () => { + beforeAll(async () => { + await utils.resetDatabase(); + await utils.adminSetup(); + }); + + describe('list-users', () => { + it('should list the admin user', async () => { + const { stdout, stderr, exitCode } = await immichAdmin(['list-users']); + expect(exitCode).toBe(0); + expect(stderr).toBe(''); + expect(stdout).toContain("email: 'admin@immich.cloud'"); + expect(stdout).toContain("name: 'Immich Admin'"); + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index a46653eb11..96994c7f0a 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -24,8 +24,8 @@ import { getConfigDefaults, login, searchMetadata, - setAdminOnboarding, signUpAdmin, + updateAdminOnboarding, updateConfig, validate, } from '@immich/sdk'; @@ -43,7 +43,7 @@ import { loginDto, signupDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import request from 'supertest'; -type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; +type CommandResponse = { stdout: string; stderr: string; exitCode: number | null }; type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete'; type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number }; type AdminSetupOptions = { onboarding?: boolean }; @@ -59,13 +59,15 @@ export const testAssetDirInternal = '/data/assets'; export const tempDir = tmpdir(); export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); -export const immichCli = async (args: string[]) => { - let _resolve: (value: CliResponse) => void; - const deferred = new Promise((resolve) => (_resolve = resolve)); - const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; - const child = spawn('node', _args, { - stdio: 'pipe', - }); +export const immichCli = (args: string[]) => + executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]); +export const immichAdmin = (args: string[]) => + executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); + +const executeCommand = (command: string, args: string[]) => { + let _resolve: (value: CommandResponse) => void; + const deferred = new Promise((resolve) => (_resolve = resolve)); + const child = spawn(command, args, { stdio: 'pipe' }); let stdout = ''; let stderr = ''; @@ -138,7 +140,7 @@ export const utils = { 'asset_faces', 'activity', 'api_keys', - 'user_token', + 'sessions', 'users', 'system_metadata', 'system_config', @@ -262,7 +264,10 @@ export const utils = { await signUpAdmin({ signUpDto: signupDto.admin }); const response = await login({ loginCredentialDto: loginDto.admin }); if (options.onboarding) { - await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) }); + await updateAdminOnboarding( + { adminOnboardingUpdateDto: { isOnboarded: true } }, + { headers: asBearerAuth(response.accessToken) }, + ); } return response; }, diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 9b9670c042..6b1db353c5 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -10,7 +10,7 @@ try { export default defineConfig({ test: { - include: ['src/{api,cli}/specs/*.e2e-spec.ts'], + include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'], globalSetup, testTimeout: 15_000, poolOptions: { diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 6b98b8f521..ce3c8a180a 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1150,22 +1150,23 @@ test = ["objgraph", "psutil"] [[package]] name = "gunicorn" -version = "21.2.0" +version = "22.0.0" description = "WSGI HTTP Server for UNIX" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" files = [ - {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, - {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, + {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, + {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, ] [package.dependencies] packaging = "*" [package.extras] -eventlet = ["eventlet (>=0.24.1)"] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [[package]] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e5d8e06d43..76f51d964e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.101.0" +version = "1.102.3" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 96d2db23f5..85d6206e1f 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,14 +1,14 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id "kotlin-kapt" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -21,18 +21,12 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } - android { compileSdkVersion 34 @@ -50,7 +44,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "app.alextran.immich" minSdkVersion 26 targetSdkVersion 33 @@ -88,6 +81,13 @@ flutter { } dependencies { + def kotlin_version = '1.9.23' + def kotlin_coroutines_version = '1.8.0' + def work_version = '2.9.0' + def concurrent_version = '1.1.0' + def guava_version = '33.1.0-android' + def glide_version = '4.16.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.work:work-runtime-ktx:$work_version" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 660e1d55ba..b6b78c2cba 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L @@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) if (workInfoList != null) { for (workInfo in workInfoList) { - if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { val workRequest = buildWorkRequest(requireWifi, requireCharging) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") @@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct .setRequiresBatteryNotLow(true) .setRequiresCharging(requireCharging) .build(); - + val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) @@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } -private const val TAG = "BackupWorker" \ No newline at end of file +private const val TAG = "BackupWorker" diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 4dacde5a9d..5e374c9f64 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,21 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.20' - ext.kotlin_coroutines_version = '1.7.1' - ext.work_version = '2.7.1' - ext.concurrent_version = '1.1.0' - ext.guava_version = '33.0.0-android' - ext.glide_version = '4.14.2' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -34,3 +16,7 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +tasks.named('wrapper') { + distributionType = Wrapper.DistributionType.ALL +} \ No newline at end of file diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 65307abde8..94a9a7e0bd 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 131, - "android.injected.version.name" => "1.101.0", + "android.injected.version.code" => 136, + "android.injected.version.name" => "1.102.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index d39c4a373f..358fb9618c 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index 7787882b74..6357330c9e 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip -distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80 \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12 \ No newline at end of file diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 44e62bcf06..7ea6533b65 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.4.2" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false + id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false +} + +include ":app" diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 1a6ca76757..46155d0c53 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "no_assets_to_show" : "No assets to show", "notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index eaa4fc06ed..f9ce46c4d5 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Photos avec mouvement", "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", + "no_assets_to_show" : "Aucun élément à afficher", "notification_permission_dialog_cancel": "Annuler", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", "notification_permission_dialog_settings": "Paramètres", @@ -509,5 +510,7 @@ "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", - "viewer_unstack": "Désempiler" + "viewer_unstack": "Désempiler", + "haptic_feedback_title": "Retour haptique", + "haptic_feedback_switch": "Activer le retour haptique" } \ No newline at end of file diff --git a/mobile/fonts/SnowburstOne.ttf b/mobile/fonts/SnowburstOne.ttf deleted file mode 100644 index e29832085c..0000000000 Binary files a/mobile/fonts/SnowburstOne.ttf and /dev/null differ diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 1894e39798..9eb77da4d4 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 64b4ea5474..517c5b29ef 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.101.0 + 1.102.2 CFBundleSignature ???? CFBundleVersion - 147 + 150 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 6cf9173c1c..dd62415ff5 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.101.0" + version_number: "1.102.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 1d6f7ff460..c3a94c1c23 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart index 24ee7e693f..89f73c0c64 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart @@ -39,27 +39,24 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); buildTopRow() { - return Row( + return Stack( children: [ - InkWell( - onTap: () => context.pop(), - child: const Icon( - Icons.close, - size: 20, + Align( + alignment: Alignment.topLeft, + child: InkWell( + onTap: () => context.pop(), + child: const Icon( + Icons.close, + size: 20, + ), ), ), - Expanded( - child: Align( - alignment: Alignment.center, - child: Text( - 'IMMICH', - style: TextStyle( - fontFamily: 'SnowburstOne', - fontWeight: FontWeight.bold, - color: context.primaryColor, - fontSize: 16, - ), - ), + Center( + child: Image.asset( + context.isDarkTheme + ? 'assets/immich-text-dark.png' + : 'assets/immich-text-light.png', + height: 16, ), ), ], diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart index c9cc6c04a9..482f1efc4f 100644 --- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart +++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart @@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget { const Center(child: ImmichLoadingIndicator()); Widget buildEmptyIndicator() => - emptyIndicator ?? const Center(child: Text("No assets to show")); + emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index 64bc1ec081..47b550f9d0 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -25,7 +25,6 @@ class SplashScreenPage extends HookConsumerWidget { void performLoggingIn() async { bool isSuccess = false; bool deviceIsOffline = false; - if (accessToken != null && serverUrl != null) { try { // Resolve API server endpoint from user provided serverUrl @@ -51,11 +50,15 @@ class SplashScreenPage extends HookConsumerWidget { offlineLogin: deviceIsOffline, ); } catch (error, stackTrace) { + ref.read(authenticationProvider.notifier).logout(); + log.severe( 'Cannot set success login info', error, stackTrace, ); + + context.pushRoute(const LoginRoute()); } } @@ -73,11 +76,6 @@ class SplashScreenPage extends HookConsumerWidget { } context.replaceRoute(const TabControllerRoute()); } else { - log.severe( - 'Unable to login through offline or online methods - logging out completely', - ); - - ref.read(authenticationProvider.notifier).logout(); // User was unable to login through either offline or online methods context.replaceRoute(const LoginRoute()); } diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index b296bbcb55..64229329aa 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -13,6 +13,7 @@ doc/ActivityCreateDto.md doc/ActivityResponseDto.md doc/ActivityStatisticsResponseDto.md doc/AddUsersDto.md +doc/AdminOnboardingUpdateDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md @@ -41,7 +42,6 @@ doc/AssetTypeEnum.md doc/AudioCodec.md doc/AuditApi.md doc/AuditDeletesResponseDto.md -doc/AuthDeviceResponseDto.md doc/AuthenticationApi.md doc/BulkIdResponseDto.md doc/BulkIdsDto.md @@ -70,6 +70,7 @@ doc/FaceApi.md doc/FaceDto.md doc/FileChecksumDto.md doc/FileChecksumResponseDto.md +doc/FileReportApi.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md @@ -123,6 +124,7 @@ doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md doc/RecognitionConfig.md +doc/ReverseGeocodingStateResponseDto.md doc/ScanLibraryDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md @@ -142,6 +144,8 @@ doc/ServerPingResponse.md doc/ServerStatsResponseDto.md doc/ServerThemeDto.md doc/ServerVersionResponseDto.md +doc/SessionResponseDto.md +doc/SessionsApi.md doc/SharedLinkApi.md doc/SharedLinkCreateDto.md doc/SharedLinkEditDto.md @@ -172,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md +doc/SystemMetadataApi.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -211,6 +216,7 @@ lib/api/audit_api.dart lib/api/authentication_api.dart lib/api/download_api.dart lib/api/face_api.dart +lib/api/file_report_api.dart lib/api/job_api.dart lib/api/library_api.dart lib/api/memory_api.dart @@ -219,9 +225,11 @@ lib/api/partner_api.dart lib/api/person_api.dart lib/api/search_api.dart lib/api/server_info_api.dart +lib/api/sessions_api.dart lib/api/shared_link_api.dart lib/api/sync_api.dart lib/api/system_config_api.dart +lib/api/system_metadata_api.dart lib/api/tag_api.dart lib/api/timeline_api.dart lib/api/trash_api.dart @@ -238,6 +246,7 @@ lib/model/activity_create_dto.dart lib/model/activity_response_dto.dart lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart +lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/all_job_status_response_dto.dart @@ -267,7 +276,6 @@ lib/model/asset_stats_response_dto.dart lib/model/asset_type_enum.dart lib/model/audio_codec.dart lib/model/audit_deletes_response_dto.dart -lib/model/auth_device_response_dto.dart lib/model/bulk_id_response_dto.dart lib/model/bulk_ids_dto.dart lib/model/change_password_dto.dart @@ -340,6 +348,7 @@ lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart lib/model/recognition_config.dart +lib/model/reverse_geocoding_state_response_dto.dart lib/model/scan_library_dto.dart lib/model/search_album_response_dto.dart lib/model/search_asset_response_dto.dart @@ -357,6 +366,7 @@ lib/model/server_ping_response.dart lib/model/server_stats_response_dto.dart lib/model/server_theme_dto.dart lib/model/server_version_response_dto.dart +lib/model/session_response_dto.dart lib/model/shared_link_create_dto.dart lib/model/shared_link_edit_dto.dart lib/model/shared_link_response_dto.dart @@ -415,6 +425,7 @@ test/activity_create_dto_test.dart test/activity_response_dto_test.dart test/activity_statistics_response_dto_test.dart test/add_users_dto_test.dart +test/admin_onboarding_update_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart @@ -448,7 +459,6 @@ test/asset_type_enum_test.dart test/audio_codec_test.dart test/audit_api_test.dart test/audit_deletes_response_dto_test.dart -test/auth_device_response_dto_test.dart test/authentication_api_test.dart test/bulk_id_response_dto_test.dart test/bulk_ids_dto_test.dart @@ -477,6 +487,7 @@ test/face_api_test.dart test/face_dto_test.dart test/file_checksum_dto_test.dart test/file_checksum_response_dto_test.dart +test/file_report_api_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart @@ -530,6 +541,7 @@ test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart test/recognition_config_test.dart +test/reverse_geocoding_state_response_dto_test.dart test/scan_library_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart @@ -549,6 +561,8 @@ test/server_ping_response_test.dart test/server_stats_response_dto_test.dart test/server_theme_dto_test.dart test/server_version_response_dto_test.dart +test/session_response_dto_test.dart +test/sessions_api_test.dart test/shared_link_api_test.dart test/shared_link_create_dto_test.dart test/shared_link_edit_dto_test.dart @@ -579,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart +test/system_metadata_api_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 730307b9bf..5439d48208 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.101.0 +- API version: 1.102.3 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -112,16 +112,10 @@ Class | Method | HTTP request | Description *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset | *AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent | *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | -*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | *AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report | -*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | -*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | -*AuthenticationApi* | [**logoutAuthDevice**](doc//AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} | -*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | @@ -129,6 +123,9 @@ Class | Method | HTTP request | Description *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face | *FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | +*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /report/fix | +*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /report | +*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /report/checksum | *JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library | @@ -182,7 +179,9 @@ Class | Method | HTTP request | Description *ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | *ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | +*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | +*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | +*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | *SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | *SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link | *SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | @@ -198,6 +197,9 @@ Class | Method | HTTP request | Description *SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | +*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | *TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag | *TagApi* | [**deleteTag**](doc//TagApi.md#deletetag) | **DELETE** /tag/{id} | *TagApi* | [**getAllTags**](doc//TagApi.md#getalltags) | **GET** /tag | @@ -233,6 +235,7 @@ Class | Method | HTTP request | Description - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) + - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) @@ -258,7 +261,6 @@ Class | Method | HTTP request | Description - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) - [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md) - - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) @@ -331,6 +333,7 @@ Class | Method | HTTP request | Description - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [RecognitionConfig](doc//RecognitionConfig.md) + - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [ScanLibraryDto](doc//ScanLibraryDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) @@ -348,6 +351,7 @@ Class | Method | HTTP request | Description - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerThemeDto](doc//ServerThemeDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) + - [SessionResponseDto](doc//SessionResponseDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) diff --git a/mobile/openapi/doc/AdminOnboardingUpdateDto.md b/mobile/openapi/doc/AdminOnboardingUpdateDto.md new file mode 100644 index 0000000000..b250843019 --- /dev/null +++ b/mobile/openapi/doc/AdminOnboardingUpdateDto.md @@ -0,0 +1,15 @@ +# openapi.model.AdminOnboardingUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isOnboarded** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AuditApi.md b/mobile/openapi/doc/AuditApi.md index 8514cdec73..2c768c40d1 100644 --- a/mobile/openapi/doc/AuditApi.md +++ b/mobile/openapi/doc/AuditApi.md @@ -9,66 +9,9 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- -[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | [**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report | -[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | -# **fixAuditFiles** -> fixAuditFiles(fileReportFixDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | - -try { - api_instance.fixAuditFiles(fileReportFixDto); -} catch (e) { - print('Exception when calling AuditApi->fixAuditFiles: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[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) - # **getAuditDeletes** > AuditDeletesResponseDto getAuditDeletes(after, entityType, userId) @@ -128,109 +71,3 @@ Name | Type | Description | Notes [[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) -# **getAuditFiles** -> FileReportDto getAuditFiles() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); - -try { - final result = api_instance.getAuditFiles(); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getAuditFiles: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**FileReportDto**](FileReportDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[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) - -# **getFileChecksums** -> List getFileChecksums(fileChecksumDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | - -try { - final result = api_instance.getFileChecksums(fileChecksumDto); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getFileChecksums: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | - -### Return type - -[**List**](FileChecksumResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[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) - diff --git a/mobile/openapi/doc/AuthenticationApi.md b/mobile/openapi/doc/AuthenticationApi.md index 9521568e9d..02fb94a092 100644 --- a/mobile/openapi/doc/AuthenticationApi.md +++ b/mobile/openapi/doc/AuthenticationApi.md @@ -10,11 +10,8 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password | -[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices | [**login**](AuthenticationApi.md#login) | **POST** /auth/login | [**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout | -[**logoutAuthDevice**](AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} | -[**logoutAuthDevices**](AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices | [**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | [**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | @@ -74,57 +71,6 @@ Name | Type | Description | Notes [[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) -# **getAuthDevices** -> List getAuthDevices() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuthenticationApi(); - -try { - final result = api_instance.getAuthDevices(); - print(result); -} catch (e) { - print('Exception when calling AuthenticationApi->getAuthDevices: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**List**](AuthDeviceResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[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) - # **login** > LoginResponseDto login(loginCredentialDto) @@ -217,110 +163,6 @@ 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) -# **logoutAuthDevice** -> logoutAuthDevice(id) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuthenticationApi(); -final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | - -try { - api_instance.logoutAuthDevice(id); -} catch (e) { - print('Exception when calling AuthenticationApi->logoutAuthDevice: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **id** | **String**| | - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[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) - -# **logoutAuthDevices** -> logoutAuthDevices() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuthenticationApi(); - -try { - api_instance.logoutAuthDevices(); -} catch (e) { - print('Exception when calling AuthenticationApi->logoutAuthDevices: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[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) - # **signUpAdmin** > UserResponseDto signUpAdmin(signUpDto) diff --git a/mobile/openapi/doc/FileReportApi.md b/mobile/openapi/doc/FileReportApi.md new file mode 100644 index 0000000000..b722c86041 --- /dev/null +++ b/mobile/openapi/doc/FileReportApi.md @@ -0,0 +1,176 @@ +# openapi.api.FileReportApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**fixAuditFiles**](FileReportApi.md#fixauditfiles) | **POST** /report/fix | +[**getAuditFiles**](FileReportApi.md#getauditfiles) | **GET** /report | +[**getFileChecksums**](FileReportApi.md#getfilechecksums) | **POST** /report/checksum | + + +# **fixAuditFiles** +> fixAuditFiles(fileReportFixDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | + +try { + api_instance.fixAuditFiles(fileReportFixDto); +} catch (e) { + print('Exception when calling FileReportApi->fixAuditFiles: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[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) + +# **getAuditFiles** +> FileReportDto getAuditFiles() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); + +try { + final result = api_instance.getAuditFiles(); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getAuditFiles: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**FileReportDto**](FileReportDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[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) + +# **getFileChecksums** +> List getFileChecksums(fileChecksumDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | + +try { + final result = api_instance.getFileChecksums(fileChecksumDto); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getFileChecksums: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | + +### Return type + +[**List**](FileChecksumResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[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) + diff --git a/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md new file mode 100644 index 0000000000..87f8aa8ab7 --- /dev/null +++ b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md @@ -0,0 +1,16 @@ +# openapi.model.ReverseGeocodingStateResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**lastImportFileName** | **String** | | +**lastUpdate** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md index cb5cf0fd3e..e8121a8001 100644 --- a/mobile/openapi/doc/ServerInfoApi.md +++ b/mobile/openapi/doc/ServerInfoApi.md @@ -17,7 +17,6 @@ Method | HTTP request | Description [**getSupportedMediaTypes**](ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | [**getTheme**](ServerInfoApi.md#gettheme) | **GET** /server-info/theme | [**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -[**setAdminOnboarding**](ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | # **getServerConfig** @@ -344,53 +343,3 @@ No authorization required [[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) -# **setAdminOnboarding** -> setAdminOnboarding() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = ServerInfoApi(); - -try { - api_instance.setAdminOnboarding(); -} catch (e) { - print('Exception when calling ServerInfoApi->setAdminOnboarding: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[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) - diff --git a/mobile/openapi/doc/AuthDeviceResponseDto.md b/mobile/openapi/doc/SessionResponseDto.md similarity index 93% rename from mobile/openapi/doc/AuthDeviceResponseDto.md rename to mobile/openapi/doc/SessionResponseDto.md index 4433e33385..9d1a11cbce 100644 --- a/mobile/openapi/doc/AuthDeviceResponseDto.md +++ b/mobile/openapi/doc/SessionResponseDto.md @@ -1,4 +1,4 @@ -# openapi.model.AuthDeviceResponseDto +# openapi.model.SessionResponseDto ## Load the model package ```dart diff --git a/mobile/openapi/doc/SessionsApi.md b/mobile/openapi/doc/SessionsApi.md new file mode 100644 index 0000000000..d082a8cfed --- /dev/null +++ b/mobile/openapi/doc/SessionsApi.md @@ -0,0 +1,171 @@ +# openapi.api.SessionsApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**deleteAllSessions**](SessionsApi.md#deleteallsessions) | **DELETE** /sessions | +[**deleteSession**](SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | +[**getSessions**](SessionsApi.md#getsessions) | **GET** /sessions | + + +# **deleteAllSessions** +> deleteAllSessions() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SessionsApi(); + +try { + api_instance.deleteAllSessions(); +} catch (e) { + print('Exception when calling SessionsApi->deleteAllSessions: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[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) + +# **deleteSession** +> deleteSession(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SessionsApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + api_instance.deleteSession(id); +} catch (e) { + print('Exception when calling SessionsApi->deleteSession: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[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) + +# **getSessions** +> List getSessions() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SessionsApi(); + +try { + final result = api_instance.getSessions(); + print(result); +} catch (e) { + print('Exception when calling SessionsApi->getSessions: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**List**](SessionResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[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) + diff --git a/mobile/openapi/doc/SystemConfigImageDto.md b/mobile/openapi/doc/SystemConfigImageDto.md index 1b9bbe726d..81e88045d5 100644 --- a/mobile/openapi/doc/SystemConfigImageDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | +**extractEmbedded** | **bool** | | **previewFormat** | [**ImageFormat**](ImageFormat.md) | | **previewSize** | **int** | | **quality** | **int** | | diff --git a/mobile/openapi/doc/SystemMetadataApi.md b/mobile/openapi/doc/SystemMetadataApi.md new file mode 100644 index 0000000000..f8c2347afe --- /dev/null +++ b/mobile/openapi/doc/SystemMetadataApi.md @@ -0,0 +1,172 @@ +# openapi.api.SystemMetadataApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getAdminOnboarding**](SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +[**getReverseGeocodingState**](SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +[**updateAdminOnboarding**](SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | + + +# **getAdminOnboarding** +> AdminOnboardingUpdateDto getAdminOnboarding() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getAdminOnboarding(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getAdminOnboarding: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[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) + +# **getReverseGeocodingState** +> ReverseGeocodingStateResponseDto getReverseGeocodingState() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getReverseGeocodingState(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getReverseGeocodingState: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**ReverseGeocodingStateResponseDto**](ReverseGeocodingStateResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[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) + +# **updateAdminOnboarding** +> updateAdminOnboarding(adminOnboardingUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); +final adminOnboardingUpdateDto = AdminOnboardingUpdateDto(); // AdminOnboardingUpdateDto | + +try { + api_instance.updateAdminOnboarding(adminOnboardingUpdateDto); +} catch (e) { + print('Exception when calling SystemMetadataApi->updateAdminOnboarding: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **adminOnboardingUpdateDto** | [**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[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) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e7320d5bb2..44bd35a683 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -37,6 +37,7 @@ part 'api/audit_api.dart'; part 'api/authentication_api.dart'; part 'api/download_api.dart'; part 'api/face_api.dart'; +part 'api/file_report_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; part 'api/memory_api.dart'; @@ -45,9 +46,11 @@ part 'api/partner_api.dart'; part 'api/person_api.dart'; part 'api/search_api.dart'; part 'api/server_info_api.dart'; +part 'api/sessions_api.dart'; part 'api/shared_link_api.dart'; part 'api/sync_api.dart'; part 'api/system_config_api.dart'; +part 'api/system_metadata_api.dart'; part 'api/tag_api.dart'; part 'api/timeline_api.dart'; part 'api/trash_api.dart'; @@ -61,6 +64,7 @@ part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; +part 'model/admin_onboarding_update_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/all_job_status_response_dto.dart'; @@ -86,7 +90,6 @@ part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; part 'model/audit_deletes_response_dto.dart'; -part 'model/auth_device_response_dto.dart'; part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; @@ -159,6 +162,7 @@ part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/recognition_config.dart'; +part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/scan_library_dto.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_response_dto.dart'; @@ -176,6 +180,7 @@ part 'model/server_ping_response.dart'; part 'model/server_stats_response_dto.dart'; part 'model/server_theme_dto.dart'; part 'model/server_version_response_dto.dart'; +part 'model/session_response_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; diff --git a/mobile/openapi/lib/api/audit_api.dart b/mobile/openapi/lib/api/audit_api.dart index 871c8e1905..83dde34da7 100644 --- a/mobile/openapi/lib/api/audit_api.dart +++ b/mobile/openapi/lib/api/audit_api.dart @@ -16,45 +16,6 @@ class AuditApi { final ApiClient apiClient; - /// Performs an HTTP 'POST /audit/file-report/fix' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/fix'; - - // ignore: prefer_final_locals - Object? postBody = fileReportFixDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { - final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response]. /// Parameters: /// @@ -115,95 +76,4 @@ class AuditApi { } return null; } - - /// Performs an HTTP 'GET /audit/file-report' operation and returns the [Response]. - Future getAuditFilesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAuditFiles() async { - final response = await getAuditFilesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; - - } - return null; - } - - /// Performs an HTTP 'POST /audit/file-report/checksum' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/checksum'; - - // ignore: prefer_final_locals - Object? postBody = fileChecksumDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { - final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } } diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index d1f04d600e..62f8be353a 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -63,50 +63,6 @@ class AuthenticationApi { return null; } - /// Performs an HTTP 'GET /auth/devices' operation and returns the [Response]. - Future getAuthDevicesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/auth/devices'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future?> getAuthDevices() async { - final response = await getAuthDevicesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Performs an HTTP 'POST /auth/login' operation and returns the [Response]. /// Parameters: /// @@ -195,79 +151,6 @@ class AuthenticationApi { return null; } - /// Performs an HTTP 'DELETE /auth/devices/{id}' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - Future logoutAuthDeviceWithHttpInfo(String id,) async { - // ignore: prefer_const_declarations - final path = r'/auth/devices/{id}' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'DELETE', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - Future logoutAuthDevice(String id,) async { - final response = await logoutAuthDeviceWithHttpInfo(id,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - - /// Performs an HTTP 'DELETE /auth/devices' operation and returns the [Response]. - Future logoutAuthDevicesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/auth/devices'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'DELETE', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future logoutAuthDevices() async { - final response = await logoutAuthDevicesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/file_report_api.dart b/mobile/openapi/lib/api/file_report_api.dart new file mode 100644 index 0000000000..df307e12c7 --- /dev/null +++ b/mobile/openapi/lib/api/file_report_api.dart @@ -0,0 +1,148 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class FileReportApi { + FileReportApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /report/fix' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/fix'; + + // ignore: prefer_final_locals + Object? postBody = fileReportFixDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { + final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /report' operation and returns the [Response]. + Future getAuditFilesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/report'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAuditFiles() async { + final response = await getAuditFilesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; + + } + return null; + } + + /// Performs an HTTP 'POST /report/checksum' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/checksum'; + + // ignore: prefer_final_locals + Object? postBody = fileChecksumDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { + final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart index 77840acd19..b67045add1 100644 --- a/mobile/openapi/lib/api/server_info_api.dart +++ b/mobile/openapi/lib/api/server_info_api.dart @@ -343,37 +343,4 @@ class ServerInfoApi { } return null; } - - /// Performs an HTTP 'POST /server-info/admin-onboarding' operation and returns the [Response]. - Future setAdminOnboardingWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/admin-onboarding'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future setAdminOnboarding() async { - final response = await setAdminOnboardingWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } } diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart new file mode 100644 index 0000000000..bc0fed71e1 --- /dev/null +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -0,0 +1,135 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class SessionsApi { + SessionsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'DELETE /sessions' operation and returns the [Response]. + Future deleteAllSessionsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/sessions'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future deleteAllSessions() async { + final response = await deleteAllSessionsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /sessions/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteSessionWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/sessions/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteSession(String id,) async { + final response = await deleteSessionWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /sessions' operation and returns the [Response]. + Future getSessionsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/sessions'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> getSessions() async { + final response = await getSessionsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart new file mode 100644 index 0000000000..f3952fda8a --- /dev/null +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -0,0 +1,139 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class SystemMetadataApi { + SystemMetadataApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /system-metadata/admin-onboarding' operation and returns the [Response]. + Future getAdminOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAdminOnboarding() async { + final response = await getAdminOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AdminOnboardingUpdateDto',) as AdminOnboardingUpdateDto; + + } + return null; + } + + /// Performs an HTTP 'GET /system-metadata/reverse-geocoding-state' operation and returns the [Response]. + Future getReverseGeocodingStateWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/reverse-geocoding-state'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getReverseGeocodingState() async { + final response = await getReverseGeocodingStateWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ReverseGeocodingStateResponseDto',) as ReverseGeocodingStateResponseDto; + + } + return null; + } + + /// Performs an HTTP 'POST /system-metadata/admin-onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody = adminOnboardingUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 4bbae89285..a92f1df7a7 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -198,6 +198,8 @@ class ApiClient { return ActivityStatisticsResponseDto.fromJson(value); case 'AddUsersDto': return AddUsersDto.fromJson(value); + case 'AdminOnboardingUpdateDto': + return AdminOnboardingUpdateDto.fromJson(value); case 'AlbumCountResponseDto': return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': @@ -248,8 +250,6 @@ class ApiClient { return AudioCodecTypeTransformer().decode(value); case 'AuditDeletesResponseDto': return AuditDeletesResponseDto.fromJson(value); - case 'AuthDeviceResponseDto': - return AuthDeviceResponseDto.fromJson(value); case 'BulkIdResponseDto': return BulkIdResponseDto.fromJson(value); case 'BulkIdsDto': @@ -394,6 +394,8 @@ class ApiClient { return ReactionTypeTypeTransformer().decode(value); case 'RecognitionConfig': return RecognitionConfig.fromJson(value); + case 'ReverseGeocodingStateResponseDto': + return ReverseGeocodingStateResponseDto.fromJson(value); case 'ScanLibraryDto': return ScanLibraryDto.fromJson(value); case 'SearchAlbumResponseDto': @@ -428,6 +430,8 @@ class ApiClient { return ServerThemeDto.fromJson(value); case 'ServerVersionResponseDto': return ServerVersionResponseDto.fromJson(value); + case 'SessionResponseDto': + return SessionResponseDto.fromJson(value); case 'SharedLinkCreateDto': return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart new file mode 100644 index 0000000000..50c4ae090e --- /dev/null +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AdminOnboardingUpdateDto { + /// Returns a new [AdminOnboardingUpdateDto] instance. + AdminOnboardingUpdateDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is AdminOnboardingUpdateDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'AdminOnboardingUpdateDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [AdminOnboardingUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AdminOnboardingUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AdminOnboardingUpdateDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AdminOnboardingUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AdminOnboardingUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AdminOnboardingUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AdminOnboardingUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart new file mode 100644 index 0000000000..71e1d3ad99 --- /dev/null +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReverseGeocodingStateResponseDto { + /// Returns a new [ReverseGeocodingStateResponseDto] instance. + ReverseGeocodingStateResponseDto({ + required this.lastImportFileName, + required this.lastUpdate, + }); + + String? lastImportFileName; + + String? lastUpdate; + + @override + bool operator ==(Object other) => identical(this, other) || other is ReverseGeocodingStateResponseDto && + other.lastImportFileName == lastImportFileName && + other.lastUpdate == lastUpdate; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (lastImportFileName == null ? 0 : lastImportFileName!.hashCode) + + (lastUpdate == null ? 0 : lastUpdate!.hashCode); + + @override + String toString() => 'ReverseGeocodingStateResponseDto[lastImportFileName=$lastImportFileName, lastUpdate=$lastUpdate]'; + + Map toJson() { + final json = {}; + if (this.lastImportFileName != null) { + json[r'lastImportFileName'] = this.lastImportFileName; + } else { + // json[r'lastImportFileName'] = null; + } + if (this.lastUpdate != null) { + json[r'lastUpdate'] = this.lastUpdate; + } else { + // json[r'lastUpdate'] = null; + } + return json; + } + + /// Returns a new [ReverseGeocodingStateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReverseGeocodingStateResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return ReverseGeocodingStateResponseDto( + lastImportFileName: mapValueOfType(json, r'lastImportFileName'), + lastUpdate: mapValueOfType(json, r'lastUpdate'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReverseGeocodingStateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReverseGeocodingStateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReverseGeocodingStateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReverseGeocodingStateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'lastImportFileName', + 'lastUpdate', + }; +} + diff --git a/mobile/openapi/lib/model/auth_device_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart similarity index 70% rename from mobile/openapi/lib/model/auth_device_response_dto.dart rename to mobile/openapi/lib/model/session_response_dto.dart index f1425a221f..6a44fc24bb 100644 --- a/mobile/openapi/lib/model/auth_device_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AuthDeviceResponseDto { - /// Returns a new [AuthDeviceResponseDto] instance. - AuthDeviceResponseDto({ +class SessionResponseDto { + /// Returns a new [SessionResponseDto] instance. + SessionResponseDto({ required this.createdAt, required this.current, required this.deviceOS, @@ -34,7 +34,7 @@ class AuthDeviceResponseDto { String updatedAt; @override - bool operator ==(Object other) => identical(this, other) || other is AuthDeviceResponseDto && + bool operator ==(Object other) => identical(this, other) || other is SessionResponseDto && other.createdAt == createdAt && other.current == current && other.deviceOS == deviceOS && @@ -53,7 +53,7 @@ class AuthDeviceResponseDto { (updatedAt.hashCode); @override - String toString() => 'AuthDeviceResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; + String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -66,14 +66,14 @@ class AuthDeviceResponseDto { return json; } - /// Returns a new [AuthDeviceResponseDto] instance and imports its values from + /// Returns a new [SessionResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AuthDeviceResponseDto? fromJson(dynamic value) { + static SessionResponseDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return AuthDeviceResponseDto( + return SessionResponseDto( createdAt: mapValueOfType(json, r'createdAt')!, current: mapValueOfType(json, r'current')!, deviceOS: mapValueOfType(json, r'deviceOS')!, @@ -85,11 +85,11 @@ class AuthDeviceResponseDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AuthDeviceResponseDto.fromJson(row); + final value = SessionResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -98,12 +98,12 @@ class AuthDeviceResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AuthDeviceResponseDto.fromJson(entry.value); + final value = SessionResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -112,14 +112,14 @@ class AuthDeviceResponseDto { return map; } - // maps a json object with a list of AuthDeviceResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of SessionResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AuthDeviceResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = SessionResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 1c830861af..7072e11270 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -14,6 +14,7 @@ class SystemConfigImageDto { /// Returns a new [SystemConfigImageDto] instance. SystemConfigImageDto({ required this.colorspace, + required this.extractEmbedded, required this.previewFormat, required this.previewSize, required this.quality, @@ -23,6 +24,8 @@ class SystemConfigImageDto { Colorspace colorspace; + bool extractEmbedded; + ImageFormat previewFormat; int previewSize; @@ -36,6 +39,7 @@ class SystemConfigImageDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && + other.extractEmbedded == extractEmbedded && other.previewFormat == previewFormat && other.previewSize == previewSize && other.quality == quality && @@ -46,6 +50,7 @@ class SystemConfigImageDto { int get hashCode => // ignore: unnecessary_parenthesis (colorspace.hashCode) + + (extractEmbedded.hashCode) + (previewFormat.hashCode) + (previewSize.hashCode) + (quality.hashCode) + @@ -53,11 +58,12 @@ class SystemConfigImageDto { (thumbnailSize.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; + json[r'extractEmbedded'] = this.extractEmbedded; json[r'previewFormat'] = this.previewFormat; json[r'previewSize'] = this.previewSize; json[r'quality'] = this.quality; @@ -75,6 +81,7 @@ class SystemConfigImageDto { return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, + extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, previewSize: mapValueOfType(json, r'previewSize')!, quality: mapValueOfType(json, r'quality')!, @@ -128,6 +135,7 @@ class SystemConfigImageDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'colorspace', + 'extractEmbedded', 'previewFormat', 'previewSize', 'quality', diff --git a/mobile/openapi/test/admin_onboarding_update_dto_test.dart b/mobile/openapi/test/admin_onboarding_update_dto_test.dart new file mode 100644 index 0000000000..09cc73e977 --- /dev/null +++ b/mobile/openapi/test/admin_onboarding_update_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AdminOnboardingUpdateDto +void main() { + // final instance = AdminOnboardingUpdateDto(); + + group('test AdminOnboardingUpdateDto', () { + // bool isOnboarded + test('to test the property `isOnboarded`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/audit_api_test.dart b/mobile/openapi/test/audit_api_test.dart index 8161d4e4db..8114283a1a 100644 --- a/mobile/openapi/test/audit_api_test.dart +++ b/mobile/openapi/test/audit_api_test.dart @@ -17,25 +17,10 @@ void main() { // final instance = AuditApi(); group('tests for AuditApi', () { - //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async - test('test fixAuditFiles', () async { - // TODO - }); - //Future getAuditDeletes(DateTime after, EntityType entityType, { String userId }) async test('test getAuditDeletes', () async { // TODO }); - //Future getAuditFiles() async - test('test getAuditFiles', () async { - // TODO - }); - - //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async - test('test getFileChecksums', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/authentication_api_test.dart b/mobile/openapi/test/authentication_api_test.dart index aa2f1879d5..dea20ec9b1 100644 --- a/mobile/openapi/test/authentication_api_test.dart +++ b/mobile/openapi/test/authentication_api_test.dart @@ -22,11 +22,6 @@ void main() { // TODO }); - //Future> getAuthDevices() async - test('test getAuthDevices', () async { - // TODO - }); - //Future login(LoginCredentialDto loginCredentialDto) async test('test login', () async { // TODO @@ -37,16 +32,6 @@ void main() { // TODO }); - //Future logoutAuthDevice(String id) async - test('test logoutAuthDevice', () async { - // TODO - }); - - //Future logoutAuthDevices() async - test('test logoutAuthDevices', () async { - // TODO - }); - //Future signUpAdmin(SignUpDto signUpDto) async test('test signUpAdmin', () async { // TODO diff --git a/mobile/openapi/test/file_report_api_test.dart b/mobile/openapi/test/file_report_api_test.dart new file mode 100644 index 0000000000..255c787002 --- /dev/null +++ b/mobile/openapi/test/file_report_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for FileReportApi +void main() { + // final instance = FileReportApi(); + + group('tests for FileReportApi', () { + //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async + test('test fixAuditFiles', () async { + // TODO + }); + + //Future getAuditFiles() async + test('test getAuditFiles', () async { + // TODO + }); + + //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async + test('test getFileChecksums', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart new file mode 100644 index 0000000000..91fdfcfea4 --- /dev/null +++ b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ReverseGeocodingStateResponseDto +void main() { + // final instance = ReverseGeocodingStateResponseDto(); + + group('test ReverseGeocodingStateResponseDto', () { + // String lastImportFileName + test('to test the property `lastImportFileName`', () async { + // TODO + }); + + // String lastUpdate + test('to test the property `lastUpdate`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 68cd1c348b..dac465116e 100644 --- a/mobile/openapi/test/server_info_api_test.dart +++ b/mobile/openapi/test/server_info_api_test.dart @@ -57,10 +57,5 @@ void main() { // TODO }); - //Future setAdminOnboarding() async - test('test setAdminOnboarding', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/auth_device_response_dto_test.dart b/mobile/openapi/test/session_response_dto_test.dart similarity index 88% rename from mobile/openapi/test/auth_device_response_dto_test.dart rename to mobile/openapi/test/session_response_dto_test.dart index c0cccf8d65..d704b2e5eb 100644 --- a/mobile/openapi/test/auth_device_response_dto_test.dart +++ b/mobile/openapi/test/session_response_dto_test.dart @@ -11,11 +11,11 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for AuthDeviceResponseDto +// tests for SessionResponseDto void main() { - // final instance = AuthDeviceResponseDto(); + // final instance = SessionResponseDto(); - group('test AuthDeviceResponseDto', () { + group('test SessionResponseDto', () { // String createdAt test('to test the property `createdAt`', () async { // TODO diff --git a/mobile/openapi/test/sessions_api_test.dart b/mobile/openapi/test/sessions_api_test.dart new file mode 100644 index 0000000000..9fc6093c19 --- /dev/null +++ b/mobile/openapi/test/sessions_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for SessionsApi +void main() { + // final instance = SessionsApi(); + + group('tests for SessionsApi', () { + //Future deleteAllSessions() async + test('test deleteAllSessions', () async { + // TODO + }); + + //Future deleteSession(String id) async + test('test deleteSession', () async { + // TODO + }); + + //Future> getSessions() async + test('test getSessions', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/system_config_image_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart index aef907bbe6..b46340455b 100644 --- a/mobile/openapi/test/system_config_image_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // bool extractEmbedded + test('to test the property `extractEmbedded`', () async { + // TODO + }); + // ImageFormat previewFormat test('to test the property `previewFormat`', () async { // TODO diff --git a/mobile/openapi/test/system_metadata_api_test.dart b/mobile/openapi/test/system_metadata_api_test.dart new file mode 100644 index 0000000000..bc1ce6f6f3 --- /dev/null +++ b/mobile/openapi/test/system_metadata_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for SystemMetadataApi +void main() { + // final instance = SystemMetadataApi(); + + group('tests for SystemMetadataApi', () { + //Future getAdminOnboarding() async + test('test getAdminOnboarding', () async { + // TODO + }); + + //Future getReverseGeocodingState() async + test('test getReverseGeocodingState', () async { + // TODO + }); + + //Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto) async + test('test updateAdminOnboarding', () async { + // TODO + }); + + }); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6bf2b09026..54648fd20b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1804,5 +1804,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index b0dd2f6b61..8ae3a21318 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,10 +2,10 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.101.0+131 +version: 1.102.3+136 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: flutter: @@ -105,9 +105,6 @@ flutter: - assets/ - assets/i18n/ fonts: - - family: SnowburstOne - fonts: - - asset: fonts/SnowburstOne.ttf - family: Inconsolata fonts: - asset: fonts/Inconsolata-Regular.ttf diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 8f53f838b0..b5bf2c9f4d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2345,118 +2345,6 @@ ] } }, - "/audit/file-report": { - "get": { - "operationId": "getAuditFiles", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/checksum": { - "post": { - "operationId": "getFileChecksums", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileChecksumDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FileChecksumResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/fix": { - "post": { - "operationId": "fixAuditFiles", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportFixDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, "/auth/admin-sign-up": { "post": { "operationId": "signUpAdmin", @@ -2530,99 +2418,6 @@ ] } }, - "/auth/devices": { - "delete": { - "operationId": "logoutAuthDevices", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Authentication" - ] - }, - "get": { - "operationId": "getAuthDevices", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AuthDeviceResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Authentication" - ] - } - }, - "/auth/devices/{id}": { - "delete": { - "operationId": "logoutAuthDevice", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Authentication" - ] - } - }, "/auth/login": { "post": { "operationId": "login", @@ -4522,6 +4317,118 @@ ] } }, + "/report": { + "get": { + "operationId": "getAuditFiles", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/checksum": { + "post": { + "operationId": "getFileChecksums", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileChecksumDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/FileChecksumResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/fix": { + "post": { + "operationId": "fixAuditFiles", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportFixDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, "/search": { "get": { "deprecated": true, @@ -5001,31 +4908,6 @@ ] } }, - "/server-info/admin-onboarding": { - "post": { - "operationId": "setAdminOnboarding", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Server Info" - ] - } - }, "/server-info/config": { "get": { "operationId": "getServerConfig", @@ -5184,6 +5066,99 @@ ] } }, + "/sessions": { + "delete": { + "operationId": "deleteAllSessions", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] + }, + "get": { + "operationId": "getSessions", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SessionResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] + } + }, + "/sessions/{id}": { + "delete": { + "operationId": "deleteSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] + } + }, "/shared-link": { "get": { "operationId": "getAllSharedLinks", @@ -5885,6 +5860,103 @@ ] } }, + "/system-metadata/admin-onboarding": { + "get": { + "operationId": "getAdminOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + }, + "post": { + "operationId": "updateAdminOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, + "/system-metadata/reverse-geocoding-state": { + "get": { + "operationId": "getReverseGeocodingState", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, "/tag": { "get": { "operationId": "getAllTags", @@ -7006,7 +7078,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.101.0", + "version": "1.102.3", "contact": {} }, "tags": [], @@ -7180,6 +7252,17 @@ ], "type": "object" }, + "AdminOnboardingUpdateDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, "AlbumCountResponseDto": { "properties": { "notShared": { @@ -7892,37 +7975,6 @@ ], "type": "object" }, - "AuthDeviceResponseDto": { - "properties": { - "createdAt": { - "type": "string" - }, - "current": { - "type": "boolean" - }, - "deviceOS": { - "type": "string" - }, - "deviceType": { - "type": "string" - }, - "id": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - }, - "required": [ - "createdAt", - "current", - "deviceOS", - "deviceType", - "id", - "updatedAt" - ], - "type": "object" - }, "BulkIdResponseDto": { "properties": { "error": { @@ -9649,6 +9701,23 @@ ], "type": "object" }, + "ReverseGeocodingStateResponseDto": { + "properties": { + "lastImportFileName": { + "nullable": true, + "type": "string" + }, + "lastUpdate": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "lastImportFileName", + "lastUpdate" + ], + "type": "object" + }, "ScanLibraryDto": { "properties": { "refreshAllFiles": { @@ -10049,6 +10118,37 @@ ], "type": "object" }, + "SessionResponseDto": { + "properties": { + "createdAt": { + "type": "string" + }, + "current": { + "type": "boolean" + }, + "deviceOS": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "createdAt", + "current", + "deviceOS", + "deviceType", + "id", + "updatedAt" + ], + "type": "object" + }, "SharedLinkCreateDto": { "properties": { "albumId": { @@ -10531,6 +10631,9 @@ "colorspace": { "$ref": "#/components/schemas/Colorspace" }, + "extractEmbedded": { + "type": "boolean" + }, "previewFormat": { "$ref": "#/components/schemas/ImageFormat" }, @@ -10549,6 +10652,7 @@ }, "required": [ "colorspace", + "extractEmbedded", "previewFormat", "previewSize", "quality", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 8def6adffd..a6a751ccfa 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 887fece059..dd2360cede 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 96b071f1f9..41603bc0e8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.101.0 + * 1.102.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -316,27 +316,6 @@ export type AuditDeletesResponseDto = { ids: string[]; needsFullSync: boolean; }; -export type FileReportItemDto = { - checksum?: string; - entityId: string; - entityType: PathEntityType; - pathType: PathType; - pathValue: string; -}; -export type FileReportDto = { - extras: string[]; - orphans: FileReportItemDto[]; -}; -export type FileChecksumDto = { - filenames: string[]; -}; -export type FileChecksumResponseDto = { - checksum: string; - filename: string; -}; -export type FileReportFixDto = { - items: FileReportItemDto[]; -}; export type SignUpDto = { email: string; name: string; @@ -346,14 +325,6 @@ export type ChangePasswordDto = { newPassword: string; password: string; }; -export type AuthDeviceResponseDto = { - createdAt: string; - current: boolean; - deviceOS: string; - deviceType: string; - id: string; - updatedAt: string; -}; export type LoginCredentialDto = { email: string; password: string; @@ -607,6 +578,27 @@ export type AssetFaceUpdateDto = { export type PersonStatisticsResponseDto = { assets: number; }; +export type FileReportItemDto = { + checksum?: string; + entityId: string; + entityType: PathEntityType; + pathType: PathType; + pathValue: string; +}; +export type FileReportDto = { + extras: string[]; + orphans: FileReportItemDto[]; +}; +export type FileChecksumDto = { + filenames: string[]; +}; +export type FileChecksumResponseDto = { + checksum: string; + filename: string; +}; +export type FileReportFixDto = { + items: FileReportItemDto[]; +}; export type SearchFacetCountResponseDto = { count: number; value: string; @@ -791,6 +783,14 @@ export type ServerVersionResponseDto = { minor: number; patch: number; }; +export type SessionResponseDto = { + createdAt: string; + current: boolean; + deviceOS: string; + deviceType: string; + id: string; + updatedAt: string; +}; export type SharedLinkResponseDto = { album?: AlbumResponseDto; allowDownload: boolean; @@ -864,6 +864,7 @@ export type SystemConfigFFmpegDto = { }; export type SystemConfigImageDto = { colorspace: Colorspace; + extractEmbedded: boolean; previewFormat: ImageFormat; previewSize: number; quality: number; @@ -997,6 +998,13 @@ export type SystemConfigTemplateStorageOptionDto = { weekOptions: string[]; yearOptions: string[]; }; +export type AdminOnboardingUpdateDto = { + isOnboarded: boolean; +}; +export type ReverseGeocodingStateResponseDto = { + lastImportFileName: string | null; + lastUpdate: string | null; +}; export type CreateTagDto = { name: string; "type": TagTypeEnum; @@ -1650,35 +1658,6 @@ export function getAuditDeletes({ after, entityType, userId }: { ...opts })); } -export function getAuditFiles(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: FileReportDto; - }>("/audit/file-report", { - ...opts - })); -} -export function getFileChecksums({ fileChecksumDto }: { - fileChecksumDto: FileChecksumDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; - data: FileChecksumResponseDto[]; - }>("/audit/file-report/checksum", oazapfts.json({ - ...opts, - method: "POST", - body: fileChecksumDto - }))); -} -export function fixAuditFiles({ fileReportFixDto }: { - fileReportFixDto: FileReportFixDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/audit/file-report/fix", oazapfts.json({ - ...opts, - method: "POST", - body: fileReportFixDto - }))); -} export function signUpAdmin({ signUpDto }: { signUpDto: SignUpDto; }, opts?: Oazapfts.RequestOpts) { @@ -1703,28 +1682,6 @@ export function changePassword({ changePasswordDto }: { body: changePasswordDto }))); } -export function logoutAuthDevices(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/auth/devices", { - ...opts, - method: "DELETE" - })); -} -export function getAuthDevices(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AuthDeviceResponseDto[]; - }>("/auth/devices", { - ...opts - })); -} -export function logoutAuthDevice({ id }: { - id: string; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/auth/devices/${encodeURIComponent(id)}`, { - ...opts, - method: "DELETE" - })); -} export function login({ loginCredentialDto }: { loginCredentialDto: LoginCredentialDto; }, opts?: Oazapfts.RequestOpts) { @@ -2227,6 +2184,35 @@ export function getPersonThumbnail({ id }: { ...opts })); } +export function getAuditFiles(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: FileReportDto; + }>("/report", { + ...opts + })); +} +export function getFileChecksums({ fileChecksumDto }: { + fileChecksumDto: FileChecksumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: FileChecksumResponseDto[]; + }>("/report/checksum", oazapfts.json({ + ...opts, + method: "POST", + body: fileChecksumDto + }))); +} +export function fixAuditFiles({ fileReportFixDto }: { + fileReportFixDto: FileReportFixDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/report/fix", oazapfts.json({ + ...opts, + method: "POST", + body: fileReportFixDto + }))); +} export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { clip?: boolean; motion?: boolean; @@ -2351,12 +2337,6 @@ export function getServerInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", { - ...opts, - method: "POST" - })); -} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2413,6 +2393,28 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/sessions", { + ...opts, + method: "DELETE" + })); +} +export function getSessions(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SessionResponseDto[]; + }>("/sessions", { + ...opts + })); +} +export function deleteSession({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2596,6 +2598,31 @@ export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getAdminOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminOnboardingUpdateDto; + }>("/system-metadata/admin-onboarding", { + ...opts + })); +} +export function updateAdminOnboarding({ adminOnboardingUpdateDto }: { + adminOnboardingUpdateDto: AdminOnboardingUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({ + ...opts, + method: "POST", + body: adminOnboardingUpdateDto + }))); +} +export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ReverseGeocodingStateResponseDto; + }>("/system-metadata/reverse-geocoding-state", { + ...opts + })); +} export function getAllTags(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2947,20 +2974,6 @@ export enum EntityType { Asset = "ASSET", Album = "ALBUM" } -export enum PathEntityType { - Asset = "asset", - Person = "person", - User = "user" -} -export enum PathType { - Original = "original", - Preview = "preview", - Thumbnail = "thumbnail", - EncodedVideo = "encoded_video", - Sidecar = "sidecar", - Face = "face", - Profile = "profile" -} export enum JobName { ThumbnailGeneration = "thumbnailGeneration", MetadataExtraction = "metadataExtraction", @@ -2992,6 +3005,20 @@ export enum Type2 { export enum MemoryType { OnThisDay = "on_this_day" } +export enum PathEntityType { + Asset = "asset", + Person = "person", + User = "user" +} +export enum PathType { + Original = "original", + Preview = "preview", + Thumbnail = "thumbnail", + EncodedVideo = "encoded_video", + Sidecar = "sidecar", + Face = "face", + Profile = "profile" +} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/renovate.json b/renovate.json index afa68011d0..3e2c50d7f1 100644 --- a/renovate.json +++ b/renovate.json @@ -27,7 +27,8 @@ "matchFileNames": ["mobile/**"], "groupName": "mobile", "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" + "schedule": "on tuesday", + "addLabels": ["📱mobile"] }, { "groupName": "exiftool", diff --git a/server/Dockerfile b/server/Dockerfile index 8ed5344395..553084ed77 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e as web +FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6 as web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/package-lock.json b/server/package-lock.json index 286f1006b9..eb062059f3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index d5828822cd..274eddd304 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "description": "", "author": "", "private": true, diff --git a/server/src/commands/list-users.command.ts b/server/src/commands/list-users.command.ts index 32bcc35d95..ea3e745463 100644 --- a/server/src/commands/list-users.command.ts +++ b/server/src/commands/list-users.command.ts @@ -1,5 +1,4 @@ import { Command, CommandRunner } from 'nest-commander'; -import { UserEntity } from 'src/entities/user.entity'; import { UserService } from 'src/services/user.service'; @Command({ @@ -13,16 +12,7 @@ export class ListUsersCommand extends CommandRunner { async run(): Promise { try { - const users = await this.userService.getAll( - { - user: { - id: 'cli', - email: 'cli@immich.app', - isAdmin: true, - } as UserEntity, - }, - true, - ); + const users = await this.userService.listUsers(); console.dir(users); } catch (error) { console.error(error); diff --git a/server/src/constants.ts b/server/src/constants.ts index 1289701dd8..d9d4232396 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -26,12 +26,7 @@ export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); export const MOBILE_REDIRECT = 'app.immich:/'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; -export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; -export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; -export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; -export const IMMICH_API_KEY_NAME = 'api_key'; -export const IMMICH_API_KEY_HEADER = 'x-api-key'; -export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; + export enum AuthType { PASSWORD = 'password', OAUTH = 'oauth', diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts index 1487e78d47..8eea6a6e3e 100644 --- a/server/src/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -1,15 +1,8 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { - AuditDeletesDto, - AuditDeletesResponseDto, - FileChecksumDto, - FileChecksumResponseDto, - FileReportDto, - FileReportFixDto, -} from 'src/dtos/audit.dto'; +import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @@ -22,22 +15,4 @@ export class AuditController { getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise { return this.service.getDeletes(auth, dto); } - - @AdminRoute() - @Get('file-report') - getAuditFiles(): Promise { - return this.service.getFileReport(); - } - - @AdminRoute() - @Post('file-report/checksum') - getFileChecksums(@Body() dto: FileChecksumDto): Promise { - return this.service.getChecksums(dto); - } - - @AdminRoute() - @Post('file-report/fix') - fixAuditFiles(@Body() dto: FileReportFixDto): Promise { - return this.service.fixItems(dto.items); - } } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 9b4e7a3bc7..a4c7494f2b 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,11 +1,11 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants'; +import { AuthType } from 'src/constants'; import { - AuthDeviceResponseDto, AuthDto, ChangePasswordDto, + ImmichCookie, LoginCredentialDto, LoginResponseDto, LogoutResponseDto, @@ -15,7 +15,7 @@ import { import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; -import { UUIDParamDto } from 'src/validation'; +import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @ApiTags('Authentication') @Controller('auth') @@ -30,9 +30,15 @@ export class AuthController { @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.login(loginCredential, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.login(loginCredential, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @PublicRoute() @@ -41,23 +47,6 @@ export class AuthController { return this.service.adminSignUp(dto); } - @Get('devices') - getAuthDevices(@Auth() auth: AuthDto): Promise { - return this.service.getDevices(auth); - } - - @Delete('devices') - @HttpCode(HttpStatus.NO_CONTENT) - logoutAuthDevices(@Auth() auth: AuthDto): Promise { - return this.service.logoutDevices(auth); - } - - @Delete('devices/:id') - @HttpCode(HttpStatus.NO_CONTENT) - logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.logoutDevice(auth, id); - } - @Post('validateToken') @HttpCode(HttpStatus.OK) validateAccessToken(): ValidateAccessTokenResponseDto { @@ -72,15 +61,18 @@ export class AuthController { @Post('logout') @HttpCode(HttpStatus.OK) - logout( + async logout( @Req() request: Request, @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise { - res.clearCookie(IMMICH_ACCESS_COOKIE); - res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); - res.clearCookie(IMMICH_IS_AUTHENTICATED); + const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; - return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); + const body = await this.service.logout(auth, authType); + return respondWithoutCookie(res, body, [ + ImmichCookie.ACCESS_TOKEN, + ImmichCookie.AUTH_TYPE, + ImmichCookie.IS_AUTHENTICATED, + ]); } } diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts new file mode 100644 index 0000000000..6bdf726073 --- /dev/null +++ b/server/src/controllers/file-report.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; +import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { AuditService } from 'src/services/audit.service'; + +@ApiTags('File Report') +@Controller('report') +@Authenticated() +export class ReportController { + constructor(private service: AuditService) {} + + @AdminRoute() + @Get() + getAuditFiles(): Promise { + return this.service.getFileReport(); + } + + @AdminRoute() + @Post('/checksum') + getFileChecksums(@Body() dto: FileChecksumDto): Promise { + return this.service.getChecksums(dto); + } + + @AdminRoute() + @Post('/fix') + fixAuditFiles(@Body() dto: FileReportFixDto): Promise { + return this.service.fixItems(dto.items); + } +} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index d136a52b04..bd10c41a43 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -8,6 +8,7 @@ import { AuditController } from 'src/controllers/audit.controller'; import { AuthController } from 'src/controllers/auth.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { FaceController } from 'src/controllers/face.controller'; +import { ReportController } from 'src/controllers/file-report.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MemoryController } from 'src/controllers/memory.controller'; @@ -16,22 +17,24 @@ import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; import { SearchController } from 'src/controllers/search.controller'; import { ServerInfoController } from 'src/controllers/server-info.controller'; +import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; import { TagController } from 'src/controllers/tag.controller'; import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; import { UserController } from 'src/controllers/user.controller'; export const controllers = [ - ActivityController, - AssetsController, - AssetControllerV1, - AssetController, - AppController, - AlbumController, APIKeyController, + ActivityController, + AlbumController, + AppController, + AssetController, + AssetControllerV1, + AssetsController, AuditController, AuthController, DownloadController, @@ -41,14 +44,17 @@ export const controllers = [ MemoryController, OAuthController, PartnerController, + PersonController, + ReportController, SearchController, ServerInfoController, + SessionController, SharedLinkController, SyncController, SystemConfigController, + SystemMetadataController, TagController, TimelineController, TrashController, UserController, - PersonController, ]; diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index debbd4e676..d87fb11d88 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; +import { AuthType } from 'src/constants'; import { AuthDto, + ImmichCookie, LoginResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, @@ -11,6 +13,7 @@ import { import { UserResponseDto } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { respondWithCookie } from 'src/utils/response'; @ApiTags('OAuth') @Controller('oauth') @@ -41,9 +44,15 @@ export class OAuthController { @Body() dto: OAuthCallbackDto, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.callback(dto, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.callback(dto, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @Post('link') diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index e32b0d191c..35e5e17594 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ServerConfigDto, @@ -65,11 +65,4 @@ export class ServerInfoController { getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } - - @AdminRoute() - @Post('admin-onboarding') - @HttpCode(HttpStatus.NO_CONTENT) - setAdminOnboarding(): Promise { - return this.service.setAdminOnboarding(); - } } diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts new file mode 100644 index 0000000000..552afcdf5a --- /dev/null +++ b/server/src/controllers/session.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SessionResponseDto } from 'src/dtos/session.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { SessionService } from 'src/services/session.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Sessions') +@Controller('sessions') +@Authenticated() +export class SessionController { + constructor(private service: SessionService) {} + + @Get() + getSessions(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); + } + + @Delete() + @HttpCode(HttpStatus.NO_CONTENT) + deleteAllSessions(@Auth() auth: AuthDto): Promise { + return this.service.deleteAll(auth); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } +} diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index a7a8e3a1c6..58f2939b93 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,18 +1,19 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; -import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; +import { respondWithCookie } from 'src/utils/response'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') @@ -33,20 +34,17 @@ export class SharedLinkController { @Query() dto: SharedLinkPasswordDto, @Req() request: Request, @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; + const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; if (sharedLinkToken) { dto.token = sharedLinkToken; } - const response = await this.service.getMine(auth, dto); - if (response.token) { - res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24), - httpOnly: true, - sameSite: 'lax', - }); - } - return response; + const body = await this.service.getMine(auth, dto); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], + }); } @Get(':id') diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts new file mode 100644 index 0000000000..7f186fec03 --- /dev/null +++ b/server/src/controllers/system-metadata.controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; + +@ApiTags('System Metadata') +@Controller('system-metadata') +@Authenticated({ admin: true }) +export class SystemMetadataController { + constructor(private service: SystemMetadataService) {} + + @Get('admin-onboarding') + getAdminOnboarding(): Promise { + return this.service.getAdminOnboarding(); + } + + @Post('admin-onboarding') + @HttpCode(HttpStatus.NO_CONTENT) + updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { + return this.service.updateAdminOnboarding(dto); + } + + @Get('reverse-geocoding-state') + getReverseGeocodingState(): Promise { + return this.service.getReverseGeocodingState(); + } +} diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index f1c16e5698..4e5f4742a4 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; @@ -308,4 +309,8 @@ export class StorageCore { static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string { return join(this.getNestedFolder(folder, ownerId, filename), filename); } + + static getTempPathInDir(dir: string): string { + return join(dir, `${randomUUID()}.tmp`); + } } diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 9cbe3b8414..2520840173 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -120,6 +120,7 @@ export const defaults = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index f3f2270d02..5c1e01b818 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -2,16 +2,35 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { APIKeyEntity } from 'src/entities/api-key.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserEntity } from 'src/entities/user.entity'; +export enum ImmichCookie { + ACCESS_TOKEN = 'immich_access_token', + AUTH_TYPE = 'immich_auth_type', + IS_AUTHENTICATED = 'immich_is_authenticated', + SHARED_LINK_TOKEN = 'immich_shared_link_token', +} + +export enum ImmichHeader { + API_KEY = 'x-api-key', + USER_TOKEN = 'x-immich-user-token', + SESSION_TOKEN = 'x-immich-session-token', + SHARED_LINK_TOKEN = 'x-immich-share-key', +} + +export type CookieResponse = { + isSecure: boolean; + values: Array<{ key: ImmichCookie; value: string }>; +}; + export class AuthDto { user!: UserEntity; apiKey?: APIKeyEntity; sharedLink?: SharedLinkEntity; - userToken?: UserTokenEntity; + session?: SessionEntity; } export class LoginCredentialDto { @@ -39,7 +58,7 @@ export class LoginResponseDto { export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto { return { - accessToken: accessToken, + accessToken, userId: entity.id, userEmail: entity.email, name: entity.name, @@ -78,24 +97,6 @@ export class ValidateAccessTokenResponseDto { authStatus!: boolean; } -export class AuthDeviceResponseDto { - id!: string; - createdAt!: string; - updatedAt!: string; - current!: boolean; - deviceType!: string; - deviceOS!: string; -} - -export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({ - id: entity.id, - createdAt: entity.createdAt.toISOString(), - updatedAt: entity.updatedAt.toISOString(), - current: currentId === entity.id, - deviceOS: entity.deviceOS, - deviceType: entity.deviceType, -}); - export class OAuthCallbackDto { @IsNotEmpty() @IsString() diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts new file mode 100644 index 0000000000..d96d7819ad --- /dev/null +++ b/server/src/dtos/session.dto.ts @@ -0,0 +1,19 @@ +import { SessionEntity } from 'src/entities/session.entity'; + +export class SessionResponseDto { + id!: string; + createdAt!: string; + updatedAt!: string; + current!: boolean; + deviceType!: string; + deviceOS!: string; +} + +export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({ + id: entity.id, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + current: currentId === entity.id, + deviceOS: entity.deviceOS, + deviceType: entity.deviceType, +}); diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 9f80e8d6a3..d23eef4994 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -417,6 +417,9 @@ class SystemConfigImageDto { @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) colorspace!: Colorspace; + + @ValidateBoolean() + extractEmbedded!: boolean; } class SystemConfigTrashDto { diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts new file mode 100644 index 0000000000..1c04435341 --- /dev/null +++ b/server/src/dtos/system-metadata.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean } from 'class-validator'; + +export class AdminOnboardingUpdateDto { + @IsBoolean() + isOnboarded!: boolean; +} + +export class AdminOnboardingResponseDto { + isOnboarded!: boolean; +} + +export class ReverseGeocodingStateResponseDto { + lastUpdate!: string | null; + lastImportFileName!: string | null; +} diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 761b476930..59aa907199 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -13,13 +13,13 @@ import { MemoryEntity } from 'src/entities/memory.entity'; import { MoveEntity } from 'src/entities/move.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { SystemConfigEntity } from 'src/entities/system-config.entity'; import { SystemMetadataEntity } from 'src/entities/system-metadata.entity'; import { TagEntity } from 'src/entities/tag.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserEntity } from 'src/entities/user.entity'; export const entities = [ @@ -44,6 +44,6 @@ export const entities = [ SystemMetadataEntity, TagEntity, UserEntity, - UserTokenEntity, + SessionEntity, LibraryEntity, ]; diff --git a/server/src/entities/user-token.entity.ts b/server/src/entities/session.entity.ts similarity index 92% rename from server/src/entities/user-token.entity.ts rename to server/src/entities/session.entity.ts index 3c2cf2cf6c..1cc9ad9857 100644 --- a/server/src/entities/user-token.entity.ts +++ b/server/src/entities/session.entity.ts @@ -1,8 +1,8 @@ import { UserEntity } from 'src/entities/user.entity'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -@Entity('user_token') -export class UserTokenEntity { +@Entity('sessions') +export class SessionEntity { @PrimaryGeneratedColumn('uuid') id!: string; diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index a8a550fd6d..7126297ce3 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -114,6 +114,7 @@ export const SystemConfigKey = { IMAGE_PREVIEW_SIZE: 'image.previewSize', IMAGE_QUALITY: 'image.quality', IMAGE_COLORSPACE: 'image.colorspace', + IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded', TRASH_ENABLED: 'trash.enabled', TRASH_DAYS: 'trash.days', @@ -284,6 +285,7 @@ export interface SystemConfig { previewSize: number; quality: number; colorspace: Colorspace; + extractEmbedded: boolean; }; newVersionCheck: { enabled: boolean; diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 5e51e94a52..a82b38b6de 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -34,6 +34,11 @@ export interface VideoFormat { bitrate: number; } +export interface ImageDimensions { + width: number; + height: number; +} + export interface VideoInfo { format: VideoFormat; videoStreams: VideoStreamInfo[]; @@ -70,9 +75,11 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig { export interface IMediaRepository { // image + extract(input: string, output: string): Promise; resize(input: string | Buffer, output: string, options: ResizeOptions): Promise; crop(input: string, options: CropOptions): Promise; generateThumbhash(imagePath: string): Promise; + getImageDimensions(input: string): Promise; // video probe(input: string): Promise; diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts new file mode 100644 index 0000000000..62c2ecec7b --- /dev/null +++ b/server/src/interfaces/session.interface.ts @@ -0,0 +1,13 @@ +import { SessionEntity } from 'src/entities/session.entity'; + +export const ISessionRepository = 'ISessionRepository'; + +type E = SessionEntity; + +export interface ISessionRepository { + create>(dto: T): Promise; + update>(dto: T): Promise; + delete(id: string): Promise; + getByToken(token: string): Promise; + getByUserId(userId: string): Promise; +} diff --git a/server/src/interfaces/user-token.interface.ts b/server/src/interfaces/user-token.interface.ts deleted file mode 100644 index 0fcec39fdc..0000000000 --- a/server/src/interfaces/user-token.interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { UserTokenEntity } from 'src/entities/user-token.entity'; - -export const IUserTokenRepository = 'IUserTokenRepository'; - -export interface IUserTokenRepository { - create(dto: Partial): Promise; - save(dto: Partial): Promise; - delete(id: string): Promise; - getByToken(token: string): Promise; - getAll(userId: string): Promise; -} diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 8b3abe6693..1253e99bbb 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -10,7 +10,6 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; -import { IMMICH_API_KEY_NAME } from 'src/constants'; import { AuthDto } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; @@ -21,6 +20,7 @@ export enum Metadata { ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', PUBLIC_SECURITY = 'public_security', + API_KEY_SECURITY = 'api_key', } export interface AuthenticatedOptions { @@ -32,7 +32,7 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => { const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), - ApiSecurity(IMMICH_API_KEY_NAME), + ApiSecurity(Metadata.API_KEY_SECURITY), SetMetadata(Metadata.AUTH_ROUTE, true), ]; diff --git a/server/src/migrations/1713490844785-RenameSessionsTable.ts b/server/src/migrations/1713490844785-RenameSessionsTable.ts new file mode 100644 index 0000000000..b1b35e8ae6 --- /dev/null +++ b/server/src/migrations/1713490844785-RenameSessionsTable.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameSessionsTable1713490844785 implements MigrationInterface { + name = 'RenameSessionsTable1713490844785'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_token" RENAME TO "sessions"`); + await queryRunner.query(`ALTER TABLE "sessions" RENAME CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" to "FK_57de40bc620f456c7311aa3a1e6"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" RENAME CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" to "FK_d37db50eecdf9b8ce4eedd2f918"`); + await queryRunner.query(`ALTER TABLE "sessions" RENAME TO "user_token"`); + } +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index 0e1cab6d0b..3c6eca7270 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -173,13 +173,13 @@ WHERE -- AccessRepository.authDevice.checkOwnerAccess SELECT - "UserTokenEntity"."id" AS "UserTokenEntity_id" + "SessionEntity"."id" AS "SessionEntity_id" FROM - "user_token" "UserTokenEntity" + "sessions" "SessionEntity" WHERE ( - ("UserTokenEntity"."userId" = $1) - AND ("UserTokenEntity"."id" IN ($2)) + ("SessionEntity"."userId" = $1) + AND ("SessionEntity"."id" IN ($2)) ) -- AccessRepository.library.checkOwnerAccess diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql new file mode 100644 index 0000000000..e712c8a160 --- /dev/null +++ b/server/src/queries/session.repository.sql @@ -0,0 +1,48 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SessionRepository.getByToken +SELECT DISTINCT + "distinctAlias"."SessionEntity_id" AS "ids_SessionEntity_id" +FROM + ( + SELECT + "SessionEntity"."id" AS "SessionEntity_id", + "SessionEntity"."userId" AS "SessionEntity_userId", + "SessionEntity"."createdAt" AS "SessionEntity_createdAt", + "SessionEntity"."updatedAt" AS "SessionEntity_updatedAt", + "SessionEntity"."deviceType" AS "SessionEntity_deviceType", + "SessionEntity"."deviceOS" AS "SessionEntity_deviceOS", + "SessionEntity__SessionEntity_user"."id" AS "SessionEntity__SessionEntity_user_id", + "SessionEntity__SessionEntity_user"."name" AS "SessionEntity__SessionEntity_user_name", + "SessionEntity__SessionEntity_user"."avatarColor" AS "SessionEntity__SessionEntity_user_avatarColor", + "SessionEntity__SessionEntity_user"."isAdmin" AS "SessionEntity__SessionEntity_user_isAdmin", + "SessionEntity__SessionEntity_user"."email" AS "SessionEntity__SessionEntity_user_email", + "SessionEntity__SessionEntity_user"."storageLabel" AS "SessionEntity__SessionEntity_user_storageLabel", + "SessionEntity__SessionEntity_user"."oauthId" AS "SessionEntity__SessionEntity_user_oauthId", + "SessionEntity__SessionEntity_user"."profileImagePath" AS "SessionEntity__SessionEntity_user_profileImagePath", + "SessionEntity__SessionEntity_user"."shouldChangePassword" AS "SessionEntity__SessionEntity_user_shouldChangePassword", + "SessionEntity__SessionEntity_user"."createdAt" AS "SessionEntity__SessionEntity_user_createdAt", + "SessionEntity__SessionEntity_user"."deletedAt" AS "SessionEntity__SessionEntity_user_deletedAt", + "SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status", + "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", + "SessionEntity__SessionEntity_user"."memoriesEnabled" AS "SessionEntity__SessionEntity_user_memoriesEnabled", + "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", + "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes" + FROM + "sessions" "SessionEntity" + LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId" + AND ( + "SessionEntity__SessionEntity_user"."deletedAt" IS NULL + ) + WHERE + (("SessionEntity"."token" = $1)) + ) "distinctAlias" +ORDER BY + "SessionEntity_id" ASC +LIMIT + 1 + +-- SessionRepository.delete +DELETE FROM "sessions" +WHERE + "id" = $1 diff --git a/server/src/queries/user.token.repository.sql b/server/src/queries/user.token.repository.sql deleted file mode 100644 index f09238e137..0000000000 --- a/server/src/queries/user.token.repository.sql +++ /dev/null @@ -1,48 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- UserTokenRepository.getByToken -SELECT DISTINCT - "distinctAlias"."UserTokenEntity_id" AS "ids_UserTokenEntity_id" -FROM - ( - SELECT - "UserTokenEntity"."id" AS "UserTokenEntity_id", - "UserTokenEntity"."userId" AS "UserTokenEntity_userId", - "UserTokenEntity"."createdAt" AS "UserTokenEntity_createdAt", - "UserTokenEntity"."updatedAt" AS "UserTokenEntity_updatedAt", - "UserTokenEntity"."deviceType" AS "UserTokenEntity_deviceType", - "UserTokenEntity"."deviceOS" AS "UserTokenEntity_deviceOS", - "UserTokenEntity__UserTokenEntity_user"."id" AS "UserTokenEntity__UserTokenEntity_user_id", - "UserTokenEntity__UserTokenEntity_user"."name" AS "UserTokenEntity__UserTokenEntity_user_name", - "UserTokenEntity__UserTokenEntity_user"."avatarColor" AS "UserTokenEntity__UserTokenEntity_user_avatarColor", - "UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin", - "UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email", - "UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel", - "UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId", - "UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath", - "UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword", - "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt", - "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt", - "UserTokenEntity__UserTokenEntity_user"."status" AS "UserTokenEntity__UserTokenEntity_user_status", - "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt", - "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled", - "UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes", - "UserTokenEntity__UserTokenEntity_user"."quotaUsageInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaUsageInBytes" - FROM - "user_token" "UserTokenEntity" - LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId" - AND ( - "UserTokenEntity__UserTokenEntity_user"."deletedAt" IS NULL - ) - WHERE - (("UserTokenEntity"."token" = $1)) - ) "distinctAlias" -ORDER BY - "UserTokenEntity_id" ASC -LIMIT - 1 - --- UserTokenRepository.delete -DELETE FROM "user_token" -WHERE - "id" = $1 diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 469de11be6..a624e8bfdc 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -9,8 +9,8 @@ import { LibraryEntity } from 'src/entities/library.entity'; import { MemoryEntity } from 'src/entities/memory.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; @@ -286,7 +286,7 @@ class AssetAccess implements IAssetAccess { } class AuthDeviceAccess implements IAuthDeviceAccess { - constructor(private tokenRepository: Repository) {} + constructor(private sessionRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -295,7 +295,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess { return new Set(); } - return this.tokenRepository + return this.sessionRepository .find({ select: { id: true }, where: { @@ -457,12 +457,12 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(PersonEntity) personRepository: Repository, @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, - @InjectRepository(UserTokenEntity) tokenRepository: Repository, + @InjectRepository(SessionEntity) sessionRepository: Repository, ) { this.activity = new ActivityAccess(activityRepository, albumRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository); this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); - this.authDevice = new AuthDeviceAccess(tokenRepository); + this.authDevice = new AuthDeviceAccess(sessionRepository); this.library = new LibraryAccess(libraryRepository); this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index e6466ee6b5..6ab09ac746 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -22,12 +22,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; @@ -53,12 +53,12 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { SearchRepository } from 'src/repositories/search.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { SessionRepository } from 'src/repositories/session.repository'; import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemConfigRepository } from 'src/repositories/system-config.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { TagRepository } from 'src/repositories/tag.repository'; -import { UserTokenRepository } from 'src/repositories/user-token.repository'; import { UserRepository } from 'src/repositories/user.repository'; export const repositories = [ @@ -86,11 +86,11 @@ export const repositories = [ { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISearchRepository, useClass: SearchRepository }, + { provide: ISessionRepository, useClass: SessionRepository }, { provide: IStorageRepository, useClass: StorageRepository }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, { provide: IMediaRepository, useClass: MediaRepository }, { provide: IUserRepository, useClass: UserRepository }, - { provide: IUserTokenRepository, useClass: UserTokenRepository }, ]; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 3936ad7e42..434fb585f8 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { exiftool } from 'exiftool-vendored'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; @@ -9,6 +10,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { CropOptions, IMediaRepository, + ImageDimensions, ResizeOptions, TranscodeOptions, VideoInfo, @@ -26,6 +28,23 @@ export class MediaRepository implements IMediaRepository { constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { this.logger.setContext(MediaRepository.name); } + + async extract(input: string, output: string): Promise { + try { + await exiftool.extractJpgFromRaw(input, output); + } catch (error: any) { + this.logger.debug('Could not extract JPEG from image, trying preview', error.message); + try { + await exiftool.extractPreview(input, output); + } catch (error: any) { + this.logger.debug('Could not extract preview from image', error.message); + return false; + } + } + + return true; + } + crop(input: string | Buffer, options: CropOptions): Promise { return sharp(input, { failOn: 'none' }) .pipelineColorspace('rgb16') @@ -133,6 +152,11 @@ export class MediaRepository implements IMediaRepository { return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data)); } + async getImageDimensions(input: string): Promise { + const { width = 0, height = 0 } = await sharp(input).metadata(); + return { width, height }; + } + private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) { return ffmpeg(input, { niceness: 10 }) .inputOptions(options.inputOptions) @@ -140,9 +164,4 @@ export class MediaRepository implements IMediaRepository { .output(output) .on('error', (error, stdout, stderr) => this.logger.error(stderr || error)); } - - private chainPath(existing: string, path: string) { - const separator = existing.endsWith(':') ? '' : ':'; - return `${existing}${separator}${path}`; - } } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 8eeb0064ac..e7d37407d9 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository { this.logger.log('Initializing metadata repository'); const geodataDate = await readFile(geodataDatePath, 'utf8'); + // TODO move to metadata service init const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); - if (geocodingMetadata?.lastUpdate === geodataDate) { return; } diff --git a/server/src/repositories/user-token.repository.ts b/server/src/repositories/session.repository.ts similarity index 54% rename from server/src/repositories/user-token.repository.ts rename to server/src/repositories/session.repository.ts index cbf3a3e3b0..ed2da7a05f 100644 --- a/server/src/repositories/user-token.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,22 +1,22 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { SessionEntity } from 'src/entities/session.entity'; +import { ISessionRepository } from 'src/interfaces/session.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() @Injectable() -export class UserTokenRepository implements IUserTokenRepository { - constructor(@InjectRepository(UserTokenEntity) private repository: Repository) {} +export class SessionRepository implements ISessionRepository { + constructor(@InjectRepository(SessionEntity) private repository: Repository) {} @GenerateSql({ params: [DummyValue.STRING] }) - getByToken(token: string): Promise { + getByToken(token: string): Promise { return this.repository.findOne({ where: { token }, relations: { user: true } }); } - getAll(userId: string): Promise { + getByUserId(userId: string): Promise { return this.repository.find({ where: { userId, @@ -31,12 +31,12 @@ export class UserTokenRepository implements IUserTokenRepository { }); } - create(userToken: Partial): Promise { - return this.repository.save(userToken); + create>(dto: T): Promise { + return this.repository.save(dto); } - save(userToken: Partial): Promise { - return this.repository.save(userToken); + update>(dto: T): Promise { + return this.repository.save(dto); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index d53f319661..f00e10b13c 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -9,25 +9,25 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AuthService } from 'src/services/auth.service'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub, loginResponseStub } from 'test/fixtures/auth.stub'; +import { sessionStub } from 'test/fixtures/session.stub'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { userTokenStub } from 'test/fixtures/user-token.stub'; import { userStub } from 'test/fixtures/user.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; -import { newUserTokenRepositoryMock } from 'test/repositories/user-token.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { Mock, Mocked, vitest } from 'vitest'; @@ -65,7 +65,7 @@ describe('AuthService', () => { let libraryMock: Mocked; let loggerMock: Mocked; let configMock: Mocked; - let userTokenMock: Mocked; + let sessionMock: Mocked; let shareMock: Mocked; let keyMock: Mocked; @@ -98,7 +98,7 @@ describe('AuthService', () => { libraryMock = newLibraryRepositoryMock(); loggerMock = newLoggerRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - userTokenMock = newUserTokenRepositoryMock(); + sessionMock = newSessionRepositoryMock(); shareMock = newSharedLinkRepositoryMock(); keyMock = newKeyRepositoryMock(); @@ -109,7 +109,7 @@ describe('AuthService', () => { libraryMock, loggerMock, userMock, - userTokenMock, + sessionMock, shareMock, keyMock, ); @@ -139,24 +139,10 @@ describe('AuthService', () => { it('should successfully log the user in', async () => { userMock.getByEmail.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); + sessionMock.create.mockResolvedValue(sessionStub.valid); await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password); expect(userMock.getByEmail).toHaveBeenCalledTimes(1); }); - - it('should generate the cookie headers (insecure)', async () => { - userMock.getByEmail.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); - await expect( - sut.login(fixtures.login, { - clientIp: '127.0.0.1', - isSecure: false, - deviceOS: '', - deviceType: '', - }), - ).resolves.toEqual(loginResponseStub.user1insecure); - expect(userMock.getByEmail).toHaveBeenCalledTimes(1); - }); }); describe('changePassword', () => { @@ -231,14 +217,14 @@ describe('AuthService', () => { }); it('should delete the access token', async () => { - const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto; + const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto; await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); - expect(userTokenMock.delete).toHaveBeenCalledWith('token123'); + expect(sessionMock.delete).toHaveBeenCalledWith('token123'); }); it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { @@ -282,11 +268,11 @@ describe('AuthService', () => { it('should validate using authorization header', async () => { userMock.get.mockResolvedValue(userStub.user1); - userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); + sessionMock.getByToken.mockResolvedValue(sessionStub.valid); const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({ user: userStub.user1, - userToken: userTokenStub.userToken, + session: sessionStub.valid, }); }); }); @@ -336,37 +322,26 @@ describe('AuthService', () => { describe('validate - user token', () => { it('should throw if no token is found', async () => { - userTokenMock.getByToken.mockResolvedValue(null); + sessionMock.getByToken.mockResolvedValue(null); const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' }; await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); }); it('should return an auth dto', async () => { - userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); + sessionMock.getByToken.mockResolvedValue(sessionStub.valid); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.user1, - userToken: userTokenStub.userToken, + session: sessionStub.valid, }); }); it('should update when access time exceeds an hour', async () => { - userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken); - userTokenMock.save.mockResolvedValue(userTokenStub.userToken); + sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); + sessionMock.update.mockResolvedValue(sessionStub.valid); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ - user: userStub.user1, - userToken: userTokenStub.userToken, - }); - expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({ - id: 'not_active', - token: 'auth_token', - userId: 'user-id', - createdAt: new Date('2021-01-01'), - updatedAt: expect.any(Date), - deviceOS: 'Android', - deviceType: 'Mobile', - }); + await expect(sut.validate(headers, {})).resolves.toBeDefined(); + expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); @@ -386,55 +361,6 @@ describe('AuthService', () => { }); }); - describe('getDevices', () => { - it('should get the devices', async () => { - userTokenMock.getAll.mockResolvedValue([userTokenStub.userToken, userTokenStub.inactiveToken]); - await expect(sut.getDevices(authStub.user1)).resolves.toEqual([ - { - createdAt: '2021-01-01T00:00:00.000Z', - current: true, - deviceOS: '', - deviceType: '', - id: 'token-id', - updatedAt: expect.any(String), - }, - { - createdAt: '2021-01-01T00:00:00.000Z', - current: false, - deviceOS: 'Android', - deviceType: 'Mobile', - id: 'not_active', - updatedAt: expect.any(String), - }, - ]); - - expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); - }); - }); - - describe('logoutDevices', () => { - it('should logout all devices', async () => { - userTokenMock.getAll.mockResolvedValue([userTokenStub.inactiveToken, userTokenStub.userToken]); - - await sut.logoutDevices(authStub.user1); - - expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); - expect(userTokenMock.delete).toHaveBeenCalledWith('not_active'); - expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id'); - }); - }); - - describe('logoutDevice', () => { - it('should logout the device', async () => { - accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); - - await sut.logoutDevice(authStub.user1, 'token-1'); - - expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1'])); - expect(userTokenMock.delete).toHaveBeenCalledWith('token-1'); - }); - }); - describe('getMobileRedirect', () => { it('should pass along the query params', () => { expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456'); @@ -463,7 +389,7 @@ describe('AuthService', () => { configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister); userMock.getByEmail.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); + sessionMock.create.mockResolvedValue(sessionStub.valid); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( loginResponseStub.user1oauth, @@ -478,7 +404,7 @@ describe('AuthService', () => { userMock.getByEmail.mockResolvedValue(null); userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); + sessionMock.create.mockResolvedValue(sessionStub.valid); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( loginResponseStub.user1oauth, @@ -491,7 +417,7 @@ describe('AuthService', () => { it('should use the mobile redirect override', async () => { configMock.load.mockResolvedValue(systemConfigStub.override); userMock.getByOAuthId.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); + sessionMock.create.mockResolvedValue(sessionStub.valid); await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails); @@ -501,7 +427,7 @@ describe('AuthService', () => { it('should use the mobile redirect override for ios urls with multiple slashes', async () => { configMock.load.mockResolvedValue(systemConfigStub.override); userMock.getByOAuthId.mockResolvedValue(userStub.user1); - userTokenMock.create.mockResolvedValue(userTokenStub.userToken); + sessionMock.create.mockResolvedValue(sessionStub.valid); await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 7bebca5989..72fee12f45 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -10,31 +10,22 @@ import cookieParser from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; -import { - AuthType, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_AUTH_TYPE_COOKIE, - IMMICH_IS_AUTHENTICATED, - LOGIN_URL, - MOBILE_REDIRECT, -} from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants'; +import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserCore } from 'src/cores/user.core'; import { - AuthDeviceResponseDto, AuthDto, ChangePasswordDto, + ImmichCookie, + ImmichHeader, LoginCredentialDto, - LoginResponseDto, LogoutResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, SignUpDto, mapLoginResponse, - mapUserToken, } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { SystemConfig } from 'src/entities/system-config.entity'; @@ -44,9 +35,9 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { HumanReadableSize } from 'src/utils/bytes'; @@ -57,11 +48,6 @@ export interface LoginDetails { deviceOS: string; } -interface LoginResponse { - response: LoginResponseDto; - cookie: string[]; -} - interface OAuthProfile extends UserinfoResponse { email: string; } @@ -85,7 +71,7 @@ export class AuthService { @Inject(ILibraryRepository) libraryRepository: ILibraryRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IUserTokenRepository) private userTokenRepository: IUserTokenRepository, + @Inject(ISessionRepository) private sessionRepository: ISessionRepository, @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, @Inject(IKeyRepository) private keyRepository: IKeyRepository, ) { @@ -97,7 +83,7 @@ export class AuthService { custom.setHttpOptionsDefaults({ timeout: 30_000 }); } - async login(dto: LoginCredentialDto, details: LoginDetails): Promise { + async login(dto: LoginCredentialDto, details: LoginDetails) { const config = await this.configCore.getConfig(); if (!config.passwordLogin.enabled) { throw new UnauthorizedException('Password login has been disabled'); @@ -116,12 +102,12 @@ export class AuthService { throw new UnauthorizedException('Incorrect email or password'); } - return this.createLoginResponse(user, AuthType.PASSWORD, details); + return this.createLoginResponse(user, details); } async logout(auth: AuthDto, authType: AuthType): Promise { - if (auth.userToken) { - await this.userTokenRepository.delete(auth.userToken.id); + if (auth.session) { + await this.sessionRepository.delete(auth.session.id); } return { @@ -163,19 +149,20 @@ export class AuthService { } async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers['x-immich-share-key'] || params.key) as string; - const userToken = (headers['x-immich-user-token'] || - params.userToken || + const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string; + const session = (headers[ImmichHeader.USER_TOKEN] || + headers[ImmichHeader.SESSION_TOKEN] || + params.sessionKey || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; + const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string; if (shareKey) { return this.validateSharedLink(shareKey); } - if (userToken) { - return this.validateUserToken(userToken); + if (session) { + return this.validateSession(session); } if (apiKey) { @@ -185,26 +172,6 @@ export class AuthService { throw new UnauthorizedException('Authentication required'); } - async getDevices(auth: AuthDto): Promise { - const userTokens = await this.userTokenRepository.getAll(auth.user.id); - return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id)); - } - - async logoutDevice(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); - await this.userTokenRepository.delete(id); - } - - async logoutDevices(auth: AuthDto): Promise { - const devices = await this.userTokenRepository.getAll(auth.user.id); - for (const device of devices) { - if (device.id === auth.userToken?.id) { - continue; - } - await this.userTokenRepository.delete(device.id); - } - } - getMobileRedirect(url: string) { return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`; } @@ -225,10 +192,7 @@ export class AuthService { return { url }; } - async callback( - dto: OAuthCallbackDto, - loginDetails: LoginDetails, - ): Promise<{ response: LoginResponseDto; cookie: string[] }> { + async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) { const config = await this.configCore.getConfig(); const profile = await this.getOAuthProfile(config, dto.url); this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); @@ -277,7 +241,7 @@ export class AuthService { }); } - return this.createLoginResponse(user, AuthType.OAUTH, loginDetails); + return this.createLoginResponse(user, loginDetails); } async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { @@ -374,7 +338,7 @@ export class AuthService { private getCookieToken(headers: IncomingHttpHeaders): string | null { const cookies = cookieParser.parse(headers.cookie || ''); - return cookies[IMMICH_ACCESS_COOKIE] || null; + return cookies[ImmichCookie.ACCESS_TOKEN] || null; } async validateSharedLink(key: string | string[]): Promise { @@ -408,57 +372,36 @@ export class AuthService { return this.cryptoRepository.compareBcrypt(inputPassword, user.password); } - private async validateUserToken(tokenValue: string): Promise { + private async validateSession(tokenValue: string): Promise { const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - let userToken = await this.userTokenRepository.getByToken(hashedToken); + const session = await this.sessionRepository.getByToken(hashedToken); - if (userToken?.user) { + if (session?.user) { const now = DateTime.now(); - const updatedAt = DateTime.fromJSDate(userToken.updatedAt); + const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); if (diff.hours > 1) { - userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() }); + await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); } - return { user: userToken.user, userToken }; + return { user: session.user, session: session }; } throw new UnauthorizedException('Invalid user token'); } - private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) { + private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); - await this.userTokenRepository.create({ + await this.sessionRepository.create({ token, user, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, }); - const response = mapLoginResponse(user, key); - const cookie = this.getCookies(response, authType, loginDetails); - return { response, cookie }; - } - - private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) { - const maxAge = 400 * 24 * 3600; // 400 days - - let authTypeCookie = ''; - let accessTokenCookie = ''; - let isAuthenticatedCookie = ''; - - if (isSecure) { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } else { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } - return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie]; + return mapLoginResponse(user, key); } private getClaim(profile: OAuthProfile, options: ClaimOptions): T { diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 6c40f8420a..2305708caa 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -18,12 +18,14 @@ import { PartnerService } from 'src/services/partner.service'; import { PersonService } from 'src/services/person.service'; import { SearchService } from 'src/services/search.service'; import { ServerInfoService } from 'src/services/server-info.service'; +import { SessionService } from 'src/services/session.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { SmartInfoService } from 'src/services/smart-info.service'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; import { SystemConfigService } from 'src/services/system-config.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TimelineService } from 'src/services/timeline.service'; import { TrashService } from 'src/services/trash.service'; @@ -50,12 +52,14 @@ export const services = [ PersonService, SearchService, ServerInfoService, + SessionService, SharedLinkService, SmartInfoService, StorageService, StorageTemplateService, SyncService, SystemConfigService, + SystemMetadataService, TagService, TimelineService, TrashService, diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 1b1adcd573..6f02e72253 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -225,6 +225,15 @@ describe(MediaService.name, () => { expect(assetMock.update).not.toHaveBeenCalledWith(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.resize).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]); assetMock.getByIds.mockResolvedValue([assetStub.image]); @@ -353,6 +362,15 @@ describe(MediaService.name, () => { expect(assetMock.update).not.toHaveBeenCalledWith(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.resize).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it.each(Object.values(ImageFormat))( 'should generate a %s thumbnail for an image when specified', async (format) => { @@ -375,14 +393,12 @@ describe(MediaService.name, () => { }); it('should generate a P3 thumbnail for a wide gamut image', async () => { - assetMock.getByIds.mockResolvedValue([ - { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, - ]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.resize).toHaveBeenCalledWith( - '/original/path.jpg', + assetStub.imageDng.originalPath, 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', { format: ImageFormat.WEBP, @@ -397,7 +413,96 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateThumbhashThumbnail', () => { + it('should extract embedded image if enabled and available', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(mediaMock.resize.mock.calls).toEqual([ + [ + extractedPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image is too small', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize.mock.calls).toEqual([ + [ + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image not found', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + it('should resize original image if embedded image extraction is not enabled', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: false }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.extract).not.toHaveBeenCalled(); + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + describe('handleGenerateThumbhash', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); await sut.handleGenerateThumbhash({ id: assetStub.image.id }); @@ -410,6 +515,15 @@ describe(MediaService.name, () => { expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it('should generate a thumbhash', async () => { const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); assetMock.getByIds.mockResolvedValue([assetStub.image]); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 47fa31abcc..1795db86d0 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { dirname } from 'node:path'; import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; @@ -42,6 +43,7 @@ import { VAAPIConfig, VP9Config, } from 'src/utils/media'; +import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @Injectable() @@ -77,7 +79,7 @@ export class MediaService { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination) + ? this.assetRepository.getAll(pagination, { isVisible: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); }); @@ -178,6 +180,10 @@ export class MediaService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); await this.assetRepository.update({ id: asset.id, previewPath }); return JobStatus.SUCCESS; @@ -191,9 +197,21 @@ export class MediaService { switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - const imageOptions = { format, size, colorspace, quality: image.quality }; - await this.mediaRepository.resize(asset.originalPath, path, imageOptions); + const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); + const extractedPath = StorageCore.getTempPathInDir(dirname(path)); + const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); + + try { + const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize)); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + + await this.mediaRepository.resize(useExtracted ? extractedPath : asset.originalPath, path, imageOptions); + } finally { + if (didExtract) { + await this.storageRepository.unlink(extractedPath); + } + } break; } @@ -230,6 +248,10 @@ export class MediaService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); await this.assetRepository.update({ id: asset.id, thumbnailPath }); return JobStatus.SUCCESS; @@ -237,7 +259,15 @@ export class MediaService { async handleGenerateThumbhash({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); - if (!asset?.previewPath) { + if (!asset) { + return JobStatus.FAILED; + } + + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + + if (!asset.previewPath) { return JobStatus.FAILED; } @@ -511,7 +541,7 @@ export class MediaService { } } - parseBitrateToBps(bitrateString: string) { + private parseBitrateToBps(bitrateString: string) { const bitrateValue = Number.parseInt(bitrateString); if (Number.isNaN(bitrateValue)) { @@ -526,4 +556,11 @@ export class MediaService { return bitrateValue; } } + + private async shouldUseExtractedImage(extractedPath: string, targetSize: number) { + const { width, height } = await this.mediaRepository.getImageDimensions(extractedPath); + const extractedSize = Math.min(width, height); + + return extractedSize >= targetSize; + } } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 77b7e552cc..721f2586ee 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -292,7 +292,12 @@ export class PersonService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true }) + ? this.assetRepository.getAll(pagination, { + orderDirection: 'DESC', + withFaces: true, + withArchived: true, + isVisible: true, + }) : this.assetRepository.getWithout(pagination, WithoutProperty.FACES); }); @@ -322,6 +327,10 @@ export class PersonService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const faces = await this.machineLearningRepository.detectFaces( machineLearning.url, { imagePath: asset.previewPath }, @@ -424,7 +433,7 @@ export class PersonService { this.logger.debug(`Face ${id} has ${matches.length} matches`); - const isCore = matches.length >= machineLearning.facialRecognition.minFaces; + const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived; if (!isCore && !deferred) { this.logger.debug(`Deferring non-core face ${id} for later processing`); await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); diff --git a/server/src/services/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts index 836909b74f..115ab4b6a1 100644 --- a/server/src/services/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,5 +1,4 @@ import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => { }); }); - describe('setAdminOnboarding', () => { - it('should set admin onboarding to true', async () => { - await sut.setAdminOnboarding(); - expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - }); - }); - describe('getStats', () => { it('should total up usage by user', async () => { userMock.getUserStats.mockResolvedValue([ diff --git a/server/src/services/server-info.service.ts b/server/src/services/server-info.service.ts index bb092896bf..52bf8bd1d3 100644 --- a/server/src/services/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -51,7 +51,9 @@ export class ServerInfoService { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { - await this.setAdminOnboarding(); + await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: true, + }); } } @@ -105,10 +107,6 @@ export class ServerInfoService { }; } - setAdminOnboarding(): Promise { - return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - } - async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts new file mode 100644 index 0000000000..0b54564da6 --- /dev/null +++ b/server/src/services/session.service.spec.ts @@ -0,0 +1,77 @@ +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; +import { SessionService } from 'src/services/session.service'; +import { authStub } from 'test/fixtures/auth.stub'; +import { sessionStub } from 'test/fixtures/session.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; +import { Mocked } from 'vitest'; + +describe('SessionService', () => { + let sut: SessionService; + let accessMock: Mocked; + let loggerMock: Mocked; + let sessionMock: Mocked; + + beforeEach(() => { + accessMock = newAccessRepositoryMock(); + loggerMock = newLoggerRepositoryMock(); + sessionMock = newSessionRepositoryMock(); + + sut = new SessionService(accessMock, loggerMock, sessionMock); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('getAll', () => { + it('should get the devices', async () => { + sessionMock.getByUserId.mockResolvedValue([sessionStub.valid, sessionStub.inactive]); + await expect(sut.getAll(authStub.user1)).resolves.toEqual([ + { + createdAt: '2021-01-01T00:00:00.000Z', + current: true, + deviceOS: '', + deviceType: '', + id: 'token-id', + updatedAt: expect.any(String), + }, + { + createdAt: '2021-01-01T00:00:00.000Z', + current: false, + deviceOS: 'Android', + deviceType: 'Mobile', + id: 'not_active', + updatedAt: expect.any(String), + }, + ]); + + expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id); + }); + }); + + describe('logoutDevices', () => { + it('should logout all devices', async () => { + sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid]); + + await sut.deleteAll(authStub.user1); + + expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id); + expect(sessionMock.delete).toHaveBeenCalledWith('not_active'); + expect(sessionMock.delete).not.toHaveBeenCalledWith('token-id'); + }); + }); + + describe('logoutDevice', () => { + it('should logout the device', async () => { + accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); + + await sut.delete(authStub.user1, 'token-1'); + + expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1'])); + expect(sessionMock.delete).toHaveBeenCalledWith('token-1'); + }); + }); +}); diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts new file mode 100644 index 0000000000..7ee454d7b4 --- /dev/null +++ b/server/src/services/session.service.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; + +@Injectable() +export class SessionService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ISessionRepository) private sessionRepository: ISessionRepository, + ) { + this.logger.setContext(SessionService.name); + this.access = AccessCore.create(accessRepository); + } + + async getAll(auth: AuthDto): Promise { + const sessions = await this.sessionRepository.getByUserId(auth.user.id); + return sessions.map((session) => mapSession(session, auth.session?.id)); + } + + async delete(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); + await this.sessionRepository.delete(id); + } + + async deleteAll(auth: AuthDto): Promise { + const sessions = await this.sessionRepository.getByUserId(auth.user.id); + for (const session of sessions) { + if (session.id === auth.session?.id) { + continue; + } + await this.sessionRepository.delete(session.id); + } + } +} diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 4d85c00253..8dedcb5c5f 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,8 +1,7 @@ -import { AssetEntity } from 'src/entities/asset.entity'; import { SystemConfigKey } from 'src/entities/system-config.entity'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; -import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; @@ -19,11 +18,6 @@ import { newSearchRepositoryMock } from 'test/repositories/search.repository.moc import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; import { Mocked } from 'vitest'; -const asset = { - id: 'asset-1', - previewPath: 'path/to/resize.ext', -} as AssetEntity; - describe(SmartInfoService.name, () => { let sut: SmartInfoService; let assetMock: Mocked; @@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => { loggerMock = newLoggerRepositoryMock(); sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock); - assetMock.getByIds.mockResolvedValue([asset]); + assetMock.getByIds.mockResolvedValue([assetStub.image]); }); it('should work', () => { @@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => { it('should do nothing if machine learning is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await sut.handleEncodeClip({ id: '123' }); + expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); }); it('should skip assets without a resize path', async () => { - const asset = { previewPath: '' } as AssetEntity; - assetMock.getByIds.mockResolvedValue([asset]); + assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleEncodeClip({ id: asset.id }); + expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED); expect(searchMock.upsert).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); @@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => { it('should save the returned objects', async () => { machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); - await sut.handleEncodeClip({ id: asset.id }); + expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); expect(machineMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - { imagePath: 'path/to/resize.ext' }, + { imagePath: assetStub.image.previewPath }, { enabled: true, modelName: 'ViT-B-32__openai' }, ); - expect(searchMock.upsert).toHaveBeenCalledWith('asset-1', [0.01, 0.02, 0.03]); + expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); + }); + + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(machineMock.encodeImage).not.toHaveBeenCalled(); + expect(searchMock.upsert).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 9de5edbd88..929d15beca 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -60,7 +60,7 @@ export class SmartInfoService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination) + ? this.assetRepository.getAll(pagination, { isVisible: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH); }); @@ -84,6 +84,10 @@ export class SmartInfoService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + if (!asset.previewPath) { return JobStatus.FAILED; } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 49bf8d6544..5f55effcac 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -129,6 +129,7 @@ const updatedConfig = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts new file mode 100644 index 0000000000..9d11c1c72a --- /dev/null +++ b/server/src/services/system-metadata.service.spec.ts @@ -0,0 +1,31 @@ +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { Mocked } from 'vitest'; + +describe(SystemMetadataService.name, () => { + let sut: SystemMetadataService; + let metadataMock: Mocked; + + beforeEach(() => { + metadataMock = newSystemMetadataRepositoryMock(); + sut = new SystemMetadataService(metadataMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('updateAdminOnboarding', () => { + it('should update isOnboarded to true', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + }); + + it('should update isOnboarded to false', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + }); + }); +}); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts new file mode 100644 index 0000000000..e8fddfc13c --- /dev/null +++ b/server/src/services/system-metadata.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + AdminOnboardingResponseDto, + AdminOnboardingUpdateDto, + ReverseGeocodingStateResponseDto, +} from 'src/dtos/system-metadata.dto'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; + +@Injectable() +export class SystemMetadataService { + constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {} + + async getAdminOnboarding(): Promise { + const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); + return { isOnboarded: false, ...value }; + } + + async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { + await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: dto.isOnboarded, + }); + } + + async getReverseGeocodingState(): Promise { + const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + return { lastUpdate: null, lastImportFileName: null, ...value }; + } +} diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index cb9012d641..9d40a14e5a 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -37,6 +37,11 @@ export class UserService { this.configCore = SystemConfigCore.create(configRepository, this.logger); } + async listUsers(): Promise { + const users = await this.userRepository.getList({ withDeleted: true }); + return users.map((user) => mapUser(user)); + } + async getAll(auth: AuthDto, isAll: boolean): Promise { const users = await this.userRepository.getList({ withDeleted: !isAll }); return users.map((user) => mapUser(user)); diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index d0a6d4d740..ff38ded631 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -436,7 +436,7 @@ export class AV1Config extends BaseConfig { export class NVENCConfig extends BaseHWConfig { getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } getBaseInputOptions() { @@ -566,7 +566,7 @@ export class QSVConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1]; } // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index bce75e1e10..cbbf751bc5 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -106,12 +106,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.profile); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - for (const [extension, v] of Object.entries(mimeTypes.profile)) { it(`should lookup ${extension}`, () => { expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); @@ -128,12 +122,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.image); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - it('should contain only image mime types', () => { const values = Object.values(mimeTypes.image).flat(); expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/'))); @@ -157,7 +145,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.video); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -184,7 +171,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.sidecar); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -198,4 +184,20 @@ describe('mimeTypes', () => { }); } }); + + describe('raw', () => { + it('should contain only lowercase mime types', () => { + const keys = Object.keys(mimeTypes.raw); + expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase())); + + const values = Object.values(mimeTypes.raw).flat(); + expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); + }); + + for (const [extension, v] of Object.entries(mimeTypes.video)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); + }); + } + }); }); diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index a888e4f423..495efc9ebc 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,12 +1,10 @@ import { extname } from 'node:path'; import { AssetType } from 'src/entities/asset.entity'; -const image: Record = { +const raw: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.ari': ['image/ari', 'image/x-arriflex-ari'], '.arw': ['image/arw', 'image/x-sony-arw'], - '.avif': ['image/avif'], - '.bmp': ['image/bmp'], '.cap': ['image/cap', 'image/x-phaseone-cap'], '.cin': ['image/cin', 'image/x-phantom-cin'], '.cr2': ['image/cr2', 'image/x-canon-cr2'], @@ -16,16 +14,7 @@ const image: Record = { '.dng': ['image/dng', 'image/x-adobe-dng'], '.erf': ['image/erf', 'image/x-epson-erf'], '.fff': ['image/fff', 'image/x-hasselblad-fff'], - '.gif': ['image/gif'], - '.heic': ['image/heic'], - '.heif': ['image/heif'], - '.hif': ['image/hif'], '.iiq': ['image/iiq', 'image/x-phaseone-iiq'], - '.insp': ['image/jpeg'], - '.jpe': ['image/jpeg'], - '.jpeg': ['image/jpeg'], - '.jpg': ['image/jpeg'], - '.jxl': ['image/jxl'], '.k25': ['image/k25', 'image/x-kodak-k25'], '.kdc': ['image/kdc', 'image/x-kodak-kdc'], '.mrw': ['image/mrw', 'image/x-minolta-mrw'], @@ -33,7 +22,6 @@ const image: Record = { '.orf': ['image/orf', 'image/x-olympus-orf'], '.ori': ['image/ori', 'image/x-olympus-ori'], '.pef': ['image/pef', 'image/x-pentax-pef'], - '.png': ['image/png'], '.psd': ['image/psd', 'image/vnd.adobe.photoshop'], '.raf': ['image/raf', 'image/x-fuji-raf'], '.raw': ['image/raw', 'image/x-panasonic-raw'], @@ -42,11 +30,27 @@ const image: Record = { '.sr2': ['image/sr2', 'image/x-sony-sr2'], '.srf': ['image/srf', 'image/x-sony-srf'], '.srw': ['image/srw', 'image/x-samsung-srw'], + '.x3f': ['image/x3f', 'image/x-sigma-x3f'], +}; + +const image: Record = { + ...raw, + '.avif': ['image/avif'], + '.bmp': ['image/bmp'], + '.gif': ['image/gif'], + '.heic': ['image/heic'], + '.heif': ['image/heif'], + '.hif': ['image/hif'], + '.insp': ['image/jpeg'], + '.jpe': ['image/jpeg'], + '.jpeg': ['image/jpeg'], + '.jpg': ['image/jpeg'], + '.jxl': ['image/jxl'], + '.png': ['image/png'], '.svg': ['image/svg'], '.tif': ['image/tiff'], '.tiff': ['image/tiff'], '.webp': ['image/webp'], - '.x3f': ['image/x3f', 'image/x-sigma-x3f'], }; const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); @@ -77,22 +81,25 @@ const sidecar: Record = { '.xmp': ['application/xml', 'text/xml'], }; +const types = { ...image, ...video, ...sidecar }; + const isType = (filename: string, r: Record) => extname(filename).toLowerCase() in r; -const lookup = (filename: string) => - ({ ...image, ...video, ...sidecar })[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; +const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; export const mimeTypes = { image, profile, sidecar, video, + raw, isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + isRaw: (filename: string) => isType(filename, raw), lookup, assetType: (filename: string) => { const contentType = lookup(filename); diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index c11c936a1a..8262b6024b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -10,13 +10,8 @@ import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.inte import _ from 'lodash'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; -import { - CLIP_MODEL_INFO, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_API_KEY_NAME, - serverVersion, -} from 'src/constants'; +import { CLIP_MODEL_INFO, serverVersion } from 'src/constants'; +import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Metadata } from 'src/middleware/auth.guard'; @@ -143,14 +138,14 @@ export const useSwagger = (app: INestApplication, isDevelopment: boolean) => { scheme: 'Bearer', in: 'header', }) - .addCookieAuth(IMMICH_ACCESS_COOKIE) + .addCookieAuth(ImmichCookie.ACCESS_TOKEN) .addApiKey( { type: 'apiKey', in: 'header', - name: IMMICH_API_KEY_HEADER, + name: ImmichHeader.API_KEY, }, - IMMICH_API_KEY_NAME, + Metadata.API_KEY_SECURITY, ) .addServer('/api') .build(); diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts new file mode 100644 index 0000000000..f318ca3300 --- /dev/null +++ b/server/src/utils/response.ts @@ -0,0 +1,36 @@ +import { CookieOptions, Response } from 'express'; +import { Duration } from 'luxon'; +import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto'; + +export const respondWithCookie = (res: Response, body: T, { isSecure, values }: CookieResponse) => { + const defaults: CookieOptions = { + path: '/', + sameSite: 'lax', + httpOnly: true, + secure: isSecure, + maxAge: Duration.fromObject({ days: 400 }).toMillis(), + }; + + const cookieOptions: Record = { + [ImmichCookie.AUTH_TYPE]: defaults, + [ImmichCookie.ACCESS_TOKEN]: defaults, + // no httpOnly so that the client can know the auth state + [ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false }, + [ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, + }; + + for (const { key, value } of values) { + const options = cookieOptions[key]; + res.cookie(key, value, options); + } + + return body; +}; + +export const respondWithoutCookie = (res: Response, body: T, cookies: ImmichCookie[]) => { + for (const cookie of cookies) { + res.clearCookie(cookie); + } + + return body; +}; diff --git a/server/test/assets b/server/test/assets index 61131e84ec..625ec3a5e9 160000 --- a/server/test/assets +++ b/server/test/assets @@ -1 +1 @@ -Subproject commit 61131e84ec91d316265aebe375b3155308baaa89 +Subproject commit 625ec3a5e9aa9b087ad986e0c2e6a24edb4ea81e diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 7aa49866d0..ce2b070672 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -757,4 +757,45 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, }), + imageDng: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.dng', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + duration: null, + isVisible: true, + isExternal: false, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.uploadLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + } as ExifEntity, + }), }; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 2e56d0001a..96a0bc0141 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,6 +1,6 @@ import { AuthDto } from 'src/dtos/auth.dto'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserEntity } from 'src/entities/user.entity'; export const adminSignupStub = { @@ -35,9 +35,9 @@ export const authStub = { email: 'immich@test.com', isAdmin: false, } as UserEntity, - userToken: { + session: { id: 'token-id', - } as UserTokenEntity, + } as SessionEntity, }), user2: Object.freeze({ user: { @@ -45,9 +45,9 @@ export const authStub = { email: 'user2@immich.app', isAdmin: false, } as UserEntity, - userToken: { + session: { id: 'token-id', - } as UserTokenEntity, + } as SessionEntity, }), external1: Object.freeze({ user: { @@ -55,9 +55,9 @@ export const authStub = { email: 'immich@test.com', isAdmin: false, } as UserEntity, - userToken: { + session: { id: 'token-id', - } as UserTokenEntity, + } as SessionEntity, }), adminSharedLink: Object.freeze({ user: { @@ -129,51 +129,21 @@ export const loginResponseStub = { }, }, user1oauth: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, user1password: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], - }, - user1insecure: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, }; diff --git a/server/test/fixtures/user-token.stub.ts b/server/test/fixtures/session.stub.ts similarity index 72% rename from server/test/fixtures/user-token.stub.ts rename to server/test/fixtures/session.stub.ts index 2f6fcc0cd5..cdf499c8d1 100644 --- a/server/test/fixtures/user-token.stub.ts +++ b/server/test/fixtures/session.stub.ts @@ -1,8 +1,8 @@ -import { UserTokenEntity } from 'src/entities/user-token.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { userStub } from 'test/fixtures/user.stub'; -export const userTokenStub = { - userToken: Object.freeze({ +export const sessionStub = { + valid: Object.freeze({ id: 'token-id', token: 'auth_token', userId: userStub.user1.id, @@ -12,7 +12,7 @@ export const userTokenStub = { deviceType: '', deviceOS: '', }), - inactiveToken: Object.freeze({ + inactive: Object.freeze({ id: 'not_active', token: 'auth_token', userId: userStub.user1.id, diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 2eea47b6ac..da3e05fe81 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -4,9 +4,11 @@ import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { return { generateThumbhash: vitest.fn(), + extract: vitest.fn().mockResolvedValue(false), resize: vitest.fn(), crop: vitest.fn(), probe: vitest.fn(), transcode: vitest.fn(), + getImageDimensions: vitest.fn(), }; }; diff --git a/server/test/repositories/session.repository.mock.ts b/server/test/repositories/session.repository.mock.ts new file mode 100644 index 0000000000..d510eb53f7 --- /dev/null +++ b/server/test/repositories/session.repository.mock.ts @@ -0,0 +1,12 @@ +import { ISessionRepository } from 'src/interfaces/session.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newSessionRepositoryMock = (): Mocked => { + return { + create: vitest.fn() as any, + update: vitest.fn() as any, + delete: vitest.fn(), + getByToken: vitest.fn(), + getByUserId: vitest.fn(), + }; +}; diff --git a/server/test/repositories/user-token.repository.mock.ts b/server/test/repositories/user-token.repository.mock.ts deleted file mode 100644 index f34e65b7f3..0000000000 --- a/server/test/repositories/user-token.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; -import { Mocked, vitest } from 'vitest'; - -export const newUserTokenRepositoryMock = (): Mocked => { - return { - create: vitest.fn(), - save: vitest.fn(), - delete: vitest.fn(), - getByToken: vitest.fn(), - getAll: vitest.fn(), - }; -}; diff --git a/web/Dockerfile b/web/Dockerfile index 8659c64277..a25ac2bfac 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e +FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6 RUN apk add --no-cache tini USER node diff --git a/web/package-lock.json b/web/package-lock.json index b5e3a6c2f9..89b02cc4cd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,17 +1,19 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", @@ -63,7 +65,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" @@ -1590,6 +1592,22 @@ "three": "^0.161.0" } }, + "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz", + "integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, + "node_modules/@photo-sphere-viewer/video-plugin": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz", + "integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", diff --git a/web/package.json b/web/package.json index 8418af55aa..34c2ee83a3 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -61,6 +61,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index 5b984e2305..2a1853f904 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -101,6 +101,16 @@ isEdited={config.image.colorspace !== savedConfig.image.colorspace} {disabled} /> + + (config.image.extractEmbedded = !config.image.extractEmbedded)} + isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} + {disabled} + />
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index abcb248f1c..6772ff5db0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -20,6 +20,7 @@ mdiFolderDownloadOutline, mdiHeart, mdiHeartOutline, + mdiHistory, mdiImageAlbum, mdiImageMinusOutline, mdiImageOutline, @@ -52,6 +53,7 @@ type MenuItemEvent = | 'addToAlbum' + | 'restoreAsset' | 'addToSharedAlbum' | 'asProfileImage' | 'setAsAlbumCover' @@ -70,6 +72,7 @@ delete: void; toggleArchive: void; addToAlbum: void; + restoreAsset: void; addToSharedAlbum: void; asProfileImage: void; setAsAlbumCover: void; @@ -208,12 +211,16 @@ {#if showDownloadButton} onMenuClick('download')} text="Download" /> {/if} - onMenuClick('addToAlbum')} text="Add to album" /> - onMenuClick('addToSharedAlbum')} - text="Add to shared album" - /> + {#if asset.isTrashed} + onMenuClick('restoreAsset')} text="Restore" /> + {:else} + onMenuClick('addToAlbum')} text="Add to album" /> + onMenuClick('addToSharedAlbum')} + text="Add to shared album" + /> + {/if} {#if isOwner} {#if hasStackChildren} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4fa7d72a2f..28899a7525 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -12,7 +12,7 @@ import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { user } from '$lib/stores/user.store'; import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; - import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils'; + import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { shortcuts } from '$lib/utils/shortcut'; import { SlideshowHistory } from '$lib/utils/slideshow-history'; @@ -27,8 +27,8 @@ getActivityStatistics, getAllAlbums, runAssetJobs, + restoreAssets, updateAsset, - updateAssets, updateAlbumInfo, type ActivityResponseDto, type AlbumResponseDto, @@ -50,7 +50,7 @@ import PanoramaViewer from './panorama-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; - import VideoViewer from './video-viewer.svelte'; + import VideoViewer from './video-wrapper-viewer.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; @@ -404,6 +404,22 @@ await handleGetAllAlbums(); }; + const handleRestoreAsset = async () => { + try { + await restoreAssets({ bulkIdsDto: { ids: [asset.id] } }); + asset.isTrashed = false; + + dispatch('action', { type: AssetAction.RESTORE, asset }); + + notificationController.show({ + type: NotificationType.Info, + message: `Restored asset`, + }); + } catch (error) { + handleError(error, 'Error restoring asset'); + } + }; + const toggleArchive = async () => { try { const data = await updateAsset({ @@ -481,20 +497,15 @@ }; const handleUnstack = async () => { - try { - const ids = $stackAssetsStore.map(({ id }) => id); - await updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } }); - for (const child of $stackAssetsStore) { - child.stackParentId = null; - child.stackCount = 0; - child.stack = []; - dispatch('action', { type: AssetAction.ADD, asset: child }); + const unstackedAssets = await unstackAssets($stackAssetsStore); + if (unstackedAssets) { + for (const asset of unstackedAssets) { + dispatch('action', { + type: AssetAction.ADD, + asset, + }); } - dispatch('close'); - notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 }); - } catch (error) { - handleError(error, `Unable to unstack`); } }; @@ -562,6 +573,7 @@ on:delete={() => trashOrDelete()} on:favorite={toggleFavorite} on:addToAlbum={() => openAlbumPicker(false)} + on:restoreAsset={() => handleRestoreAsset()} on:addToSharedAlbum={() => openAlbumPicker(true)} on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} @@ -610,6 +622,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -630,6 +643,7 @@ {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} (shouldPlayMotionPhoto = false)} /> @@ -643,6 +657,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte index 66d8f63099..592053e5b8 100644 --- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -1,22 +1,39 @@
- {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])} + {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])} - {:then [data, module]} - + {:then [data, module, adapter, plugins, navbar]} + {:catch} Failed to load asset {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 796622e7fe..0c0e707693 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,17 +1,32 @@ + +{#if projectionType === ProjectionType.EQUIRECTANGULAR} + +{:else} + +{/if} diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte index fb301b65fe..9d8c8854be 100644 --- a/web/src/lib/components/elements/search-bar.svelte +++ b/web/src/lib/components/elements/search-bar.svelte @@ -28,9 +28,7 @@
- { const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner); - const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id); + const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id); const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null; @@ -229,7 +229,7 @@
{#if searchName == ''} {#each allPeople as person (person.id)} - {#if person.id !== peopleWithFaces[editedPersonIndex].person?.id} + {#if person.id !== editedPerson.id}
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 3cb705a1f3..489cfeb170 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -28,14 +28,14 @@ export let assetType: AssetTypeEnum; // keep track of the changes - let numberOfPersonToCreate: string[] = []; - let numberOfAssetFaceGenerated: string[] = []; + let peopleToCreate: string[] = []; + let assetFaceGenerated: string[] = []; // faces let peopleWithFaces: AssetFaceResponseDto[] = []; - let selectedPersonToReassign: (PersonResponseDto | null)[]; - let selectedPersonToCreate: (string | null)[]; - let editedPersonIndex: number; + let selectedPersonToReassign: Record = {}; + let selectedPersonToCreate: Record = {}; + let editedPerson: PersonResponseDto; // loading spinners let isShowLoadingDone = false; @@ -49,6 +49,8 @@ let loaderLoadingDoneTimeout: ReturnType; let automaticRefreshTimeout: ReturnType; + const thumbnailWidth = '90px'; + const dispatch = createEventDispatcher<{ close: void; refresh: void; @@ -60,8 +62,6 @@ const { people } = await getAllPeople({ withHidden: true }); allPeople = people; peopleWithFaces = await getFaces({ id: assetId }); - selectedPersonToCreate = Array.from({ length: peopleWithFaces.length }); - selectedPersonToReassign = Array.from({ length: peopleWithFaces.length }); } catch (error) { handleError(error, "Can't get faces"); } finally { @@ -71,12 +71,12 @@ } const onPersonThumbnail = (personId: string) => { - numberOfAssetFaceGenerated.push(personId); + assetFaceGenerated.push(personId); if ( - isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) && + isEqual(assetFaceGenerated, peopleToCreate) && loaderLoadingDoneTimeout && automaticRefreshTimeout && - selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length + Object.keys(selectedPersonToCreate).length === peopleToCreate.length ) { clearTimeout(loaderLoadingDoneTimeout); clearTimeout(automaticRefreshTimeout); @@ -97,36 +97,41 @@ dispatch('close'); }; - const handleReset = (index: number) => { - if (selectedPersonToReassign[index]) { - selectedPersonToReassign[index] = null; + const handleReset = (id: string) => { + if (selectedPersonToReassign[id]) { + delete selectedPersonToReassign[id]; + + // trigger reactivity + selectedPersonToReassign = selectedPersonToReassign; } - if (selectedPersonToCreate[index]) { - selectedPersonToCreate[index] = null; + if (selectedPersonToCreate[id]) { + delete selectedPersonToCreate[id]; + + // trigger reactivity + selectedPersonToCreate = selectedPersonToCreate; } }; const handleEditFaces = async () => { loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner); - const numberOfChanges = - selectedPersonToCreate.filter((person) => person !== null).length + - selectedPersonToReassign.filter((person) => person !== null).length; + const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length; + if (numberOfChanges > 0) { try { - for (const [index, peopleWithFace] of peopleWithFaces.entries()) { - const personId = selectedPersonToReassign[index]?.id; + for (const personWithFace of peopleWithFaces) { + const personId = selectedPersonToReassign[personWithFace.id]?.id; if (personId) { await reassignFacesById({ id: personId, - faceDto: { id: peopleWithFace.id }, + faceDto: { id: personWithFace.id }, }); - } else if (selectedPersonToCreate[index]) { + } else if (selectedPersonToCreate[personWithFace.id]) { const data = await createPerson({ personCreateDto: {} }); - numberOfPersonToCreate.push(data.id); + peopleToCreate.push(data.id); await reassignFacesById({ id: data.id, - faceDto: { id: peopleWithFace.id }, + faceDto: { id: personWithFace.id }, }); } } @@ -141,7 +146,7 @@ } isShowLoadingDone = false; - if (numberOfPersonToCreate.length === 0) { + if (peopleToCreate.length === 0) { clearTimeout(loaderLoadingDoneTimeout); dispatch('refresh'); } else { @@ -150,23 +155,26 @@ }; const handleCreatePerson = (newFeaturePhoto: string | null) => { - const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id); + const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id); if (newFeaturePhoto && personToUpdate) { - selectedPersonToCreate[peopleWithFaces.indexOf(personToUpdate)] = newFeaturePhoto; + selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto; } showSeletecFaces = false; }; const handleReassignFace = (person: PersonResponseDto | null) => { - if (person) { - selectedPersonToReassign[editedPersonIndex] = person; + const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id); + if (person && personToUpdate) { + selectedPersonToReassign[personToUpdate.id] = person; showSeletecFaces = false; } }; - const handlePersonPicker = (index: number) => { - editedPersonIndex = index; - showSeletecFaces = true; + const handlePersonPicker = (person: PersonResponseDto | null) => { + if (person) { + editedPerson = person; + showSeletecFaces = true; + } }; @@ -217,35 +225,48 @@ on:mouseleave={() => ($boundingBoxesArray = [])} >
-
- {#if !selectedPersonToCreate[index]} + + {#if !selectedPersonToCreate[face.id]}

- {#if selectedPersonToReassign[index]?.id} - {selectedPersonToReassign[index]?.name} + {#if selectedPersonToReassign[face.id]?.id} + {selectedPersonToReassign[face.id]?.name} {:else} {face.person?.name} {/if} @@ -253,8 +274,8 @@ {/if}

- {#if selectedPersonToCreate[index] || selectedPersonToReassign[index]} - {:else} -