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/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/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/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index 28445f79d9..4a6e1a773a 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'; @@ -118,67 +118,6 @@ describe('/auth/*', () => { }); }); - 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); - }); - }); - describe('POST /auth/validateToken', () => { it('should reject an invalid token', async () => { const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123'); 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/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 27bc514bde..14ffbea745 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -44,7 +44,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 }; @@ -60,13 +60,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 = ''; @@ -139,7 +141,7 @@ export const utils = { 'asset_faces', 'activity', 'api_keys', - 'user_token', + 'sessions', 'users', 'system_metadata', 'system_config', 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/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/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/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 83380c5e77..2181476b3a 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -16,8 +16,6 @@ doc/AddUsersDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md -doc/AlbumUserResponseDto.md -doc/AlbumUserRole.md doc/AllJobStatusResponseDto.md doc/AssetApi.md doc/AssetBulkDeleteDto.md @@ -43,7 +41,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 @@ -144,6 +141,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 @@ -186,7 +185,6 @@ doc/TranscodeHWAccel.md doc/TranscodePolicy.md doc/TrashApi.md doc/UpdateAlbumDto.md -doc/UpdateAlbumUserDto.md doc/UpdateAssetDto.md doc/UpdateLibraryDto.md doc/UpdatePartnerDto.md @@ -222,6 +220,7 @@ 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 @@ -243,8 +242,6 @@ lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart -lib/model/album_user_response_dto.dart -lib/model/album_user_role.dart lib/model/all_job_status_response_dto.dart lib/model/api_key_create_dto.dart lib/model/api_key_create_response_dto.dart @@ -272,7 +269,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 @@ -362,6 +358,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 @@ -398,7 +395,6 @@ lib/model/tone_mapping.dart lib/model/transcode_hw_accel.dart lib/model/transcode_policy.dart lib/model/update_album_dto.dart -lib/model/update_album_user_dto.dart lib/model/update_asset_dto.dart lib/model/update_library_dto.dart lib/model/update_partner_dto.dart @@ -424,8 +420,6 @@ test/add_users_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart -test/album_user_response_dto_test.dart -test/album_user_role_test.dart test/all_job_status_response_dto_test.dart test/api_key_api_test.dart test/api_key_create_dto_test.dart @@ -456,7 +450,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 @@ -557,6 +550,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 @@ -599,7 +594,6 @@ test/transcode_hw_accel_test.dart test/transcode_policy_test.dart test/trash_api_test.dart test/update_album_dto_test.dart -test/update_album_user_dto_test.dart test/update_asset_dto_test.dart test/update_library_dto_test.dart test/update_partner_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index da96fc2c08..7fb4681f79 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -91,7 +91,6 @@ Class | Method | HTTP request | Description *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | -*AlbumApi* | [**updateAlbumUser**](doc//AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/permission/{userId} | *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | @@ -118,11 +117,8 @@ Class | Method | HTTP request | Description *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 | @@ -184,6 +180,9 @@ Class | Method | HTTP request | Description *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 | @@ -236,8 +235,6 @@ Class | Method | HTTP request | Description - [AddUsersDto](doc//AddUsersDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) - - [AlbumUserRole](doc//AlbumUserRole.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) @@ -261,7 +258,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) @@ -351,6 +347,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) @@ -387,7 +384,6 @@ Class | Method | HTTP request | Description - [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodePolicy](doc//TranscodePolicy.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md) - - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.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/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/lib/api.dart b/mobile/openapi/lib/api.dart index fe7282a27a..b484d38b68 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -45,6 +45,7 @@ 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'; @@ -63,8 +64,6 @@ part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; -part 'model/album_user_response_dto.dart'; -part 'model/album_user_role.dart'; part 'model/all_job_status_response_dto.dart'; part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_update_dto.dart'; @@ -88,7 +87,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'; @@ -178,6 +176,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'; @@ -214,7 +213,6 @@ part 'model/tone_mapping.dart'; part 'model/transcode_hw_accel.dart'; part 'model/transcode_policy.dart'; part 'model/update_album_dto.dart'; -part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; part 'model/update_partner_dto.dart'; 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/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_client.dart b/mobile/openapi/lib/api_client.dart index 57349e2383..0a0cd80088 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -202,10 +202,6 @@ class ApiClient { return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': return AlbumResponseDto.fromJson(value); - case 'AlbumUserResponseDto': - return AlbumUserResponseDto.fromJson(value); - case 'AlbumUserRole': - return AlbumUserRoleTypeTransformer().decode(value); case 'AllJobStatusResponseDto': return AllJobStatusResponseDto.fromJson(value); case 'AssetBulkDeleteDto': @@ -252,8 +248,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': @@ -432,6 +426,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': @@ -504,8 +500,6 @@ class ApiClient { return TranscodePolicyTypeTransformer().decode(value); case 'UpdateAlbumDto': return UpdateAlbumDto.fromJson(value); - case 'UpdateAlbumUserDto': - return UpdateAlbumUserDto.fromJson(value); case 'UpdateAssetDto': return UpdateAssetDto.fromJson(value); case 'UpdateLibraryDto': 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/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/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/pubspec.yaml b/mobile/pubspec.yaml index b0dd2f6b61..8dcc892a06 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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 b1b3fb1a8f..5feda4d092 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2583,99 +2583,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", @@ -5237,6 +5144,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", @@ -7976,37 +7976,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": { @@ -10133,6 +10102,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": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 01e5d3556c..fa31801133 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -355,14 +355,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; @@ -800,6 +792,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; @@ -1723,28 +1723,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) { @@ -2433,6 +2411,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; 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/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/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 9b4e7a3bc7..f4e7666207 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,9 +1,8 @@ -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 { - AuthDeviceResponseDto, AuthDto, ChangePasswordDto, LoginCredentialDto, @@ -15,7 +14,6 @@ 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'; @ApiTags('Authentication') @Controller('auth') @@ -41,23 +39,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 { diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index d136a52b04..5e109f1eb3 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -16,6 +16,7 @@ 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'; @@ -43,6 +44,7 @@ export const controllers = [ PartnerController, SearchController, ServerInfoController, + SessionController, SharedLinkController, SyncController, SystemConfigController, 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/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index f3f2270d02..4651c010b9 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -2,8 +2,8 @@ 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 class AuthDto { @@ -11,7 +11,7 @@ export class AuthDto { apiKey?: APIKeyEntity; sharedLink?: SharedLinkEntity; - userToken?: UserTokenEntity; + session?: SessionEntity; } export class LoginCredentialDto { @@ -78,24 +78,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/entities/index.ts b/server/src/entities/index.ts index 0a44cc8000..0862dd48a2 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -14,13 +14,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 = [ @@ -46,6 +46,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/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts new file mode 100644 index 0000000000..3e2c9574a4 --- /dev/null +++ b/server/src/interfaces/session.interface.ts @@ -0,0 +1,11 @@ +import { SessionEntity } from 'src/entities/session.entity'; + +export const ISessionRepository = 'ISessionRepository'; + +export interface ISessionRepository { + create(dto: Partial): Promise; + update(dto: Partial): 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/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 4c0b786057..f6eed63bb1 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -185,13 +185,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 6e1fda8376..c33ab1608b 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -10,8 +10,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'; @@ -293,7 +293,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 }) @@ -302,7 +302,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess { return new Set(); } - return this.tokenRepository + return this.sessionRepository .find({ select: { id: true }, where: { @@ -464,12 +464,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 32b7b338fb..712e925cf2 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -23,12 +23,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'; @@ -55,12 +55,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 = [ @@ -89,11 +89,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/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..5e42039bc6 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(session: Partial): Promise { + return this.repository.save(session); } - save(userToken: Partial): Promise { - return this.repository.save(userToken); + update(session: Partial): Promise { + return this.repository.save(session); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index d53f319661..9d83d5261f 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,14 +139,14 @@ 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); + sessionMock.create.mockResolvedValue(sessionStub.valid); await expect( sut.login(fixtures.login, { clientIp: '127.0.0.1', @@ -231,14 +231,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 +282,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 +336,29 @@ 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', + session: sessionStub.valid, }); + expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); @@ -386,55 +378,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 +406,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 +421,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 +434,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 +444,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..7e81d15ce5 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -19,11 +19,10 @@ import { LOGIN_URL, MOBILE_REDIRECT, } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +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, LoginCredentialDto, @@ -34,7 +33,6 @@ import { 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 +42,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'; @@ -85,7 +83,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, ) { @@ -120,8 +118,8 @@ export class AuthService { } 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 { @@ -164,8 +162,9 @@ 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 session = (headers['x-immich-user-token'] || + headers['x-immich-session-token'] || + params.sessionKey || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; @@ -174,8 +173,8 @@ export class AuthService { return this.validateSharedLink(shareKey); } - if (userToken) { - return this.validateUserToken(userToken); + if (session) { + return this.validateSession(session); } if (apiKey) { @@ -185,26 +184,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] || ''}`; } @@ -408,19 +387,19 @@ 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); + let 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() }); + session = 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'); @@ -430,7 +409,7 @@ export class AuthService { 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, diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 6c40f8420a..db3d6083e9 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -18,6 +18,7 @@ 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'; @@ -50,6 +51,7 @@ export const services = [ PersonService, SearchService, ServerInfoService, + SessionService, SharedLinkService, SmartInfoService, StorageService, diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 1b1adcd573..c6301c7c33 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) => { @@ -410,6 +428,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..ca72b6cbdd 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -77,7 +77,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 +178,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; @@ -230,6 +234,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 +245,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; } 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/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/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/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 2e56d0001a..a4753a02e7 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: { 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/session.repository.mock.ts b/server/test/repositories/session.repository.mock.ts new file mode 100644 index 0000000000..1a034e79f0 --- /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(), + update: vitest.fn(), + 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/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4fa7d72a2f..46c95636d0 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'; @@ -28,7 +28,6 @@ getAllAlbums, runAssetJobs, updateAsset, - updateAssets, updateAlbumInfo, type ActivityResponseDto, type AlbumResponseDto, @@ -481,20 +480,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`); } }; diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 14bbb6b1fa..6285a63006 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -14,6 +14,9 @@ import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { getAltText } from '$lib/utils/thumbnail-util'; + import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; + + const { slideshowState, slideshowLook } = slideshowStore; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] | null = null; @@ -158,7 +161,9 @@ transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} src={assetData} alt={getAltText(asset)} - class="h-full w-full object-contain" + class="h-full w-full {$slideshowState === SlideshowState.None + ? 'object-contain' + : slideshowLookCssMapping[$slideshowLook]}" draggable="false" /> {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox} diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index ce7212f950..7f48a9eae8 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -21,7 +21,7 @@ export let peopleWithFaces: AssetFaceResponseDto[]; export let allPeople: PersonResponseDto[]; - export let editedPersonIndex: number; + export let editedPerson: PersonResponseDto; export let assetType: AssetTypeEnum; export let assetId: string; @@ -106,7 +106,7 @@ const handleCreatePerson = async () => { 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}
{:else} -