mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	feat(web,server): logout all devices (#2415)
* feat: logout all devices * chore: regenerate openapi * chore: add test * chore: logout vs log out
This commit is contained in:
		
							parent
							
								
									c956eee919
								
							
						
					
					
						commit
						a808b9403e
					
				
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@ -120,6 +120,7 @@ Class | Method | HTTP request | Description
 | 
			
		||||
*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* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
 | 
			
		||||
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | 
 | 
			
		||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} | 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								mobile/openapi/doc/AuthenticationApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										51
									
								
								mobile/openapi/doc/AuthenticationApi.md
									
									
									
										generated
									
									
									
								
							@ -15,6 +15,7 @@ Method | HTTP request | Description
 | 
			
		||||
[**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 | 
 | 
			
		||||
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -311,6 +312,56 @@ void (empty response body)
 | 
			
		||||
 | 
			
		||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
# **logoutAuthDevices**
 | 
			
		||||
> logoutAuthDevices()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Example
 | 
			
		||||
```dart
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
// TODO Configure API key authorization: cookie
 | 
			
		||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
			
		||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
			
		||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
			
		||||
// TODO Configure API key authorization: api_key
 | 
			
		||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
 | 
			
		||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
			
		||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
 | 
			
		||||
// TODO Configure HTTP Bearer authorization: bearer
 | 
			
		||||
// Case 1. Use String Token
 | 
			
		||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
 | 
			
		||||
// Case 2. Use Function which generate token.
 | 
			
		||||
// String yourTokenGeneratorFunction() { ... }
 | 
			
		||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('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)
 | 
			
		||||
 | 
			
		||||
# **validateAccessToken**
 | 
			
		||||
> ValidateAccessTokenResponseDto validateAccessToken()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								mobile/openapi/lib/api/authentication_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								mobile/openapi/lib/api/authentication_api.dart
									
									
									
										generated
									
									
									
								
							@ -282,6 +282,39 @@ class AuthenticationApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Performs an HTTP 'DELETE /auth/devices' operation and returns the [Response].
 | 
			
		||||
  Future<Response> logoutAuthDevicesWithHttpInfo() async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
    final path = r'/auth/devices';
 | 
			
		||||
 | 
			
		||||
    // ignore: prefer_final_locals
 | 
			
		||||
    Object? postBody;
 | 
			
		||||
 | 
			
		||||
    final queryParams = <QueryParam>[];
 | 
			
		||||
    final headerParams = <String, String>{};
 | 
			
		||||
    final formParams = <String, String>{};
 | 
			
		||||
 | 
			
		||||
    const contentTypes = <String>[];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return apiClient.invokeAPI(
 | 
			
		||||
      path,
 | 
			
		||||
      'DELETE',
 | 
			
		||||
      queryParams,
 | 
			
		||||
      postBody,
 | 
			
		||||
      headerParams,
 | 
			
		||||
      formParams,
 | 
			
		||||
      contentTypes.isEmpty ? null : contentTypes.first,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> logoutAuthDevices() async {
 | 
			
		||||
    final response = await logoutAuthDevicesWithHttpInfo();
 | 
			
		||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
			
		||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
 | 
			
		||||
  Future<Response> validateAccessTokenWithHttpInfo() async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								mobile/openapi/test/authentication_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/authentication_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -47,6 +47,11 @@ void main() {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //Future logoutAuthDevices() async
 | 
			
		||||
    test('test logoutAuthDevices', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //Future<ValidateAccessTokenResponseDto> validateAccessToken() async
 | 
			
		||||
    test('test validateAccessToken', () async {
 | 
			
		||||
      // TODO
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,12 @@ export class AuthController {
 | 
			
		||||
    return this.service.getDevices(authUser);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated()
 | 
			
		||||
  @Delete('devices')
 | 
			
		||||
  logoutAuthDevices(@GetAuthUser() authUser: AuthUserDto): Promise<void> {
 | 
			
		||||
    return this.service.logoutDevices(authUser);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated()
 | 
			
		||||
  @Delete('devices/:id')
 | 
			
		||||
  logoutAuthDevice(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
 | 
			
		||||
 | 
			
		||||
@ -393,6 +393,29 @@
 | 
			
		||||
            "api_key": []
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "delete": {
 | 
			
		||||
        "operationId": "logoutAuthDevices",
 | 
			
		||||
        "parameters": [],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "description": ""
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "Authentication"
 | 
			
		||||
        ],
 | 
			
		||||
        "security": [
 | 
			
		||||
          {
 | 
			
		||||
            "bearer": []
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "cookie": []
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "api_key": []
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/auth/devices/{id}": {
 | 
			
		||||
 | 
			
		||||
@ -357,6 +357,18 @@ describe('AuthService', () => {
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('logoutDevices', () => {
 | 
			
		||||
    it('should logout all devices', async () => {
 | 
			
		||||
      userTokenMock.getAll.mockResolvedValue([userTokenEntityStub.inactiveToken, userTokenEntityStub.userToken]);
 | 
			
		||||
 | 
			
		||||
      await sut.logoutDevices(authStub.user1);
 | 
			
		||||
 | 
			
		||||
      expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
 | 
			
		||||
      expect(userTokenMock.delete).toHaveBeenCalledWith(authStub.user1.id, 'not_active');
 | 
			
		||||
      expect(userTokenMock.delete).not.toHaveBeenCalledWith(authStub.user1.id, 'token-id');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('logoutDevice', () => {
 | 
			
		||||
    it('should logout the device', async () => {
 | 
			
		||||
      await sut.logoutDevice(authStub.user1, 'token-1');
 | 
			
		||||
 | 
			
		||||
@ -163,6 +163,16 @@ export class AuthService {
 | 
			
		||||
    await this.userTokenCore.delete(authUser.id, deviceId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async logoutDevices(authUser: AuthUserDto): Promise<void> {
 | 
			
		||||
    const devices = await this.userTokenCore.getAll(authUser.id);
 | 
			
		||||
    for (const device of devices) {
 | 
			
		||||
      if (device.id === authUser.accessTokenId) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      await this.userTokenCore.delete(authUser.id, device.id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getBearerToken(headers: IncomingHttpHeaders): string | null {
 | 
			
		||||
    const [type, token] = (headers.authorization || '').split(' ');
 | 
			
		||||
    if (type.toLowerCase() === 'bearer') {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										65
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										65
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -6299,6 +6299,44 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        logoutAuthDevices: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/auth/devices`;
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
@ -6414,6 +6452,15 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.logoutAuthDevice(id, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async logoutAuthDevices(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.logoutAuthDevices(options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@ -6485,6 +6532,14 @@ export const AuthenticationApiFactory = function (configuration?: Configuration,
 | 
			
		||||
        logoutAuthDevice(id: string, options?: any): AxiosPromise<void> {
 | 
			
		||||
            return localVarFp.logoutAuthDevice(id, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        logoutAuthDevices(options?: any): AxiosPromise<void> {
 | 
			
		||||
            return localVarFp.logoutAuthDevices(options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@ -6567,6 +6622,16 @@ export class AuthenticationApi extends BaseAPI {
 | 
			
		||||
        return AuthenticationApiFp(this.configuration).logoutAuthDevice(id, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof AuthenticationApi
 | 
			
		||||
     */
 | 
			
		||||
    public logoutAuthDevices(options?: AxiosRequestConfig) {
 | 
			
		||||
        return AuthenticationApiFp(this.configuration).logoutAuthDevices(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
	import { api, AuthDeviceResponseDto } from '@api';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { handleError } from '../../utils/handle-error';
 | 
			
		||||
	import Button from '../elements/buttons/button.svelte';
 | 
			
		||||
	import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		notificationController,
 | 
			
		||||
@ -11,6 +12,7 @@
 | 
			
		||||
 | 
			
		||||
	let devices: AuthDeviceResponseDto[] = [];
 | 
			
		||||
	let deleteDevice: AuthDeviceResponseDto | null = null;
 | 
			
		||||
	let deleteAll = false;
 | 
			
		||||
 | 
			
		||||
	const refresh = () => api.authenticationApi.getAuthDevices().then(({ data }) => (devices = data));
 | 
			
		||||
 | 
			
		||||
@ -36,6 +38,21 @@
 | 
			
		||||
			deleteDevice = null;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDeleteAll = async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			await api.authenticationApi.logoutAuthDevices();
 | 
			
		||||
			notificationController.show({
 | 
			
		||||
				message: `Logged out all devices`,
 | 
			
		||||
				type: NotificationType.Info
 | 
			
		||||
			});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			handleError(error, 'Unable to log out all devices');
 | 
			
		||||
		} finally {
 | 
			
		||||
			await refresh();
 | 
			
		||||
			deleteAll = false;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if deleteDevice}
 | 
			
		||||
@ -46,6 +63,14 @@
 | 
			
		||||
	/>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
{#if deleteAll}
 | 
			
		||||
	<ConfirmDialogue
 | 
			
		||||
		prompt="Are you sure you want to log out all devices?"
 | 
			
		||||
		on:confirm={() => handleDeleteAll()}
 | 
			
		||||
		on:cancel={() => (deleteAll = false)}
 | 
			
		||||
	/>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<section class="my-4">
 | 
			
		||||
	{#if currentDevice}
 | 
			
		||||
		<div class="mb-6">
 | 
			
		||||
@ -56,7 +81,7 @@
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
	{#if otherDevices.length > 0}
 | 
			
		||||
		<div>
 | 
			
		||||
		<div class="mb-6">
 | 
			
		||||
			<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
				OTHER DEVICES
 | 
			
		||||
			</h3>
 | 
			
		||||
@ -67,5 +92,11 @@
 | 
			
		||||
				{/if}
 | 
			
		||||
			{/each}
 | 
			
		||||
		</div>
 | 
			
		||||
		<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
			LOG OUT ALL DEVICES
 | 
			
		||||
		</h3>
 | 
			
		||||
		<div class="flex justify-end">
 | 
			
		||||
			<Button color="red" size="sm" on:click={() => (deleteAll = true)}>Log Out All Devices</Button>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user