diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index 28445f79d9ebd..4a6e1a773a813 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 0000000000000..0b632f78ba42e --- /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/utils.ts b/e2e/src/utils.ts index 617f2d62cc236..0047502023fda 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -140,7 +140,7 @@ export const utils = { 'asset_faces', 'activity', 'api_keys', - 'user_token', + 'sessions', 'users', 'system_metadata', 'system_config', diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index b296bbcb55b2a..2181476b3a1bf 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -41,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 @@ -142,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 @@ -219,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 @@ -267,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 @@ -357,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 @@ -448,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 @@ -549,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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 730307b9bf971..7fb4681f79296 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -117,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 | @@ -183,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 | @@ -258,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) @@ -348,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) diff --git a/mobile/openapi/doc/AuthenticationApi.md b/mobile/openapi/doc/AuthenticationApi.md index 9521568e9d303..02fb94a092a48 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 4433e33385aa8..9d1a11cbce6d5 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 0000000000000..d082a8cfed186 --- /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 e7320d5bb2d70..b484d38b688e7 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'; @@ -86,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'; @@ -176,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'; diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index d1f04d600ed8c..62f8be353a69e 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 0000000000000..bc0fed71e1352 --- /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 4bbae892858cb..0a0cd80088bbd 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -248,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': @@ -428,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': 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 f1425a221f9cc..6a44fc24bbae8 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 aa2f1879d55d0..dea20ec9b1d8d 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 c0cccf8d65c3c..d704b2e5eba3d 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 0000000000000..9fc6093c19416 --- /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/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 8f53f838b0cf0..bfe3ec32c9782 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2530,99 +2530,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", @@ -5184,6 +5091,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", @@ -7892,37 +7892,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": { @@ -10049,6 +10018,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 96b071f1f96d6..560295c94c5c6 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -346,14 +346,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; @@ -791,6 +783,14 @@ export type ServerVersionResponseDto = { minor: number; patch: number; }; +export type SessionResponseDto = { + createdAt: string; + current: boolean; + deviceOS: string; + deviceType: string; + id: string; + updatedAt: string; +}; export type SharedLinkResponseDto = { album?: AlbumResponseDto; allowDownload: boolean; @@ -1703,28 +1703,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) { @@ -2413,6 +2391,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/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 9b4e7a3bc754a..f4e7666207189 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 d136a52b047ce..5e109f1eb3414 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 0000000000000..552afcdf5aebe --- /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 f3f2270d02fa9..4651c010b9456 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 0000000000000..d96d7819adcab --- /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 761b4769301f2..59aa907199b1d 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -13,13 +13,13 @@ import { MemoryEntity } from 'src/entities/memory.entity'; import { MoveEntity } from 'src/entities/move.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { SystemConfigEntity } from 'src/entities/system-config.entity'; import { SystemMetadataEntity } from 'src/entities/system-metadata.entity'; import { TagEntity } from 'src/entities/tag.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserEntity } from 'src/entities/user.entity'; export const entities = [ @@ -44,6 +44,6 @@ export const entities = [ SystemMetadataEntity, TagEntity, UserEntity, - UserTokenEntity, + SessionEntity, LibraryEntity, ]; diff --git a/server/src/entities/user-token.entity.ts b/server/src/entities/session.entity.ts similarity index 92% rename from server/src/entities/user-token.entity.ts rename to server/src/entities/session.entity.ts index 3c2cf2cf6cb65..1cc9ad98572ab 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 0000000000000..3e2c9574a4a1a --- /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 0fcec39fdcb6a..0000000000000 --- 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 0000000000000..b1b35e8ae6d01 --- /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 0e1cab6d0ba69..3c6eca72701a7 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -173,13 +173,13 @@ WHERE -- AccessRepository.authDevice.checkOwnerAccess SELECT - "UserTokenEntity"."id" AS "UserTokenEntity_id" + "SessionEntity"."id" AS "SessionEntity_id" FROM - "user_token" "UserTokenEntity" + "sessions" "SessionEntity" WHERE ( - ("UserTokenEntity"."userId" = $1) - AND ("UserTokenEntity"."id" IN ($2)) + ("SessionEntity"."userId" = $1) + AND ("SessionEntity"."id" IN ($2)) ) -- AccessRepository.library.checkOwnerAccess diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql new file mode 100644 index 0000000000000..e712c8a160db4 --- /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 f09238e1379b0..0000000000000 --- 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 469de11be6916..a624e8bfdc079 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -9,8 +9,8 @@ import { LibraryEntity } from 'src/entities/library.entity'; import { MemoryEntity } from 'src/entities/memory.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { UserTokenEntity } from 'src/entities/user-token.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; @@ -286,7 +286,7 @@ class AssetAccess implements IAssetAccess { } class AuthDeviceAccess implements IAuthDeviceAccess { - constructor(private tokenRepository: Repository) {} + constructor(private sessionRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -295,7 +295,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess { return new Set(); } - return this.tokenRepository + return this.sessionRepository .find({ select: { id: true }, where: { @@ -457,12 +457,12 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(PersonEntity) personRepository: Repository, @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, - @InjectRepository(UserTokenEntity) tokenRepository: Repository, + @InjectRepository(SessionEntity) sessionRepository: Repository, ) { this.activity = new ActivityAccess(activityRepository, albumRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository); this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); - this.authDevice = new AuthDeviceAccess(tokenRepository); + this.authDevice = new AuthDeviceAccess(sessionRepository); this.library = new LibraryAccess(libraryRepository); this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index e6466ee6b5b60..6ab09ac746c9c 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -22,12 +22,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; -import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; @@ -53,12 +53,12 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { SearchRepository } from 'src/repositories/search.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository'; +import { SessionRepository } from 'src/repositories/session.repository'; import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemConfigRepository } from 'src/repositories/system-config.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { TagRepository } from 'src/repositories/tag.repository'; -import { UserTokenRepository } from 'src/repositories/user-token.repository'; import { UserRepository } from 'src/repositories/user.repository'; export const repositories = [ @@ -86,11 +86,11 @@ export const repositories = [ { provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISearchRepository, useClass: SearchRepository }, + { provide: ISessionRepository, useClass: SessionRepository }, { provide: IStorageRepository, useClass: StorageRepository }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, { provide: IMediaRepository, useClass: MediaRepository }, { provide: IUserRepository, useClass: UserRepository }, - { provide: IUserTokenRepository, useClass: UserTokenRepository }, ]; diff --git a/server/src/repositories/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 cbf3a3e3b0117..5e42039bc6bf2 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 d53f319661716..9d83d5261f049 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 7bebca59899d5..7e81d15ce5114 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 6c40f8420ae15..db3d6083e97ba 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/session.service.spec.ts b/server/src/services/session.service.spec.ts new file mode 100644 index 0000000000000..0b54564da690e --- /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 0000000000000..7ee454d7b4d2c --- /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/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 2e56d0001a874..a4753a02e7fbf 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 2f6fcc0cd575a..cdf499c8d1f02 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 0000000000000..1a034e79f047d --- /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 f34e65b7f3b62..0000000000000 --- 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/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index 64f17ad9e5ec5..8821ed970aa73 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -1,7 +1,7 @@