mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server): mobile oauth with custom scheme redirect uri (#1204)
* feat(server): support providers without support for custom schemas * chore: unit tests * chore: test mobile override * chore: add details to the docs
This commit is contained in:
		
							
								
								
									
										70
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										70
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
 * Immich
 | 
			
		||||
 * Immich API
 | 
			
		||||
 *
 | 
			
		||||
 * The version of the OpenAPI document: 1.39.0
 | 
			
		||||
 * The version of the OpenAPI document: 1.40.0
 | 
			
		||||
 * 
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | 
			
		||||
@@ -1567,6 +1567,18 @@ export interface SystemConfigOAuthDto {
 | 
			
		||||
     * @memberof SystemConfigOAuthDto
 | 
			
		||||
     */
 | 
			
		||||
    'autoRegister': boolean;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     * @memberof SystemConfigOAuthDto
 | 
			
		||||
     */
 | 
			
		||||
    'mobileOverrideEnabled': boolean;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof SystemConfigOAuthDto
 | 
			
		||||
     */
 | 
			
		||||
    'mobileRedirectUri': string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
@@ -5111,6 +5123,35 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        mobileRedirect: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/oauth/mobile-redirect`;
 | 
			
		||||
            // 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: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            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.
 | 
			
		||||
@@ -5180,6 +5221,15 @@ export const OAuthApiFp = function(configuration?: Configuration) {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.link(oAuthCallbackDto, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async mobileRedirect(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.mobileRedirect(options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@@ -5226,6 +5276,14 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath
 | 
			
		||||
        link(oAuthCallbackDto: OAuthCallbackDto, options?: any): AxiosPromise<UserResponseDto> {
 | 
			
		||||
            return localVarFp.link(oAuthCallbackDto, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        mobileRedirect(options?: any): AxiosPromise<void> {
 | 
			
		||||
            return localVarFp.mobileRedirect(options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
@@ -5277,6 +5335,16 @@ export class OAuthApi extends BaseAPI {
 | 
			
		||||
        return OAuthApiFp(this.configuration).link(oAuthCallbackDto, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof OAuthApi
 | 
			
		||||
     */
 | 
			
		||||
    public mobileRedirect(options?: AxiosRequestConfig) {
 | 
			
		||||
        return OAuthApiFp(this.configuration).mobileRedirect(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/base.ts
									
									
									
										generated
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
 * Immich
 | 
			
		||||
 * Immich API
 | 
			
		||||
 *
 | 
			
		||||
 * The version of the OpenAPI document: 1.39.0
 | 
			
		||||
 * The version of the OpenAPI document: 1.40.0
 | 
			
		||||
 * 
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/common.ts
									
									
									
										generated
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
 * Immich
 | 
			
		||||
 * Immich API
 | 
			
		||||
 *
 | 
			
		||||
 * The version of the OpenAPI document: 1.39.0
 | 
			
		||||
 * The version of the OpenAPI document: 1.40.0
 | 
			
		||||
 * 
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/configuration.ts
									
									
									
										generated
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
 * Immich
 | 
			
		||||
 * Immich API
 | 
			
		||||
 *
 | 
			
		||||
 * The version of the OpenAPI document: 1.39.0
 | 
			
		||||
 * The version of the OpenAPI document: 1.40.0
 | 
			
		||||
 * 
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/src/api/open-api/index.ts
									
									
									
										generated
									
									
									
								
							@@ -4,7 +4,7 @@
 | 
			
		||||
 * Immich
 | 
			
		||||
 * Immich API
 | 
			
		||||
 *
 | 
			
		||||
 * The version of the OpenAPI document: 1.39.0
 | 
			
		||||
 * The version of the OpenAPI document: 1.40.0
 | 
			
		||||
 * 
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 | 
			
		||||
 
 | 
			
		||||
@@ -3,18 +3,27 @@
 | 
			
		||||
		notificationController,
 | 
			
		||||
		NotificationType
 | 
			
		||||
	} from '$lib/components/shared-components/notification/notification';
 | 
			
		||||
	import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
	import { api, SystemConfigOAuthDto } from '@api';
 | 
			
		||||
	import _ from 'lodash';
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
	import SettingButtonsRow from '../setting-buttons-row.svelte';
 | 
			
		||||
	import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
 | 
			
		||||
	import SettingSwitch from '../setting-switch.svelte';
 | 
			
		||||
	import _ from 'lodash';
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
 | 
			
		||||
	export let oauthConfig: SystemConfigOAuthDto;
 | 
			
		||||
 | 
			
		||||
	let savedConfig: SystemConfigOAuthDto;
 | 
			
		||||
	let defaultConfig: SystemConfigOAuthDto;
 | 
			
		||||
 | 
			
		||||
	const handleToggleOverride = () => {
 | 
			
		||||
		// click runs before bind
 | 
			
		||||
		const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
 | 
			
		||||
		if (!previouslyEnabled && !oauthConfig.mobileRedirectUri) {
 | 
			
		||||
			oauthConfig.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	async function getConfigs() {
 | 
			
		||||
		[savedConfig, defaultConfig] = await Promise.all([
 | 
			
		||||
			api.systemConfigApi.getConfig().then((res) => res.data.oauth),
 | 
			
		||||
@@ -38,6 +47,10 @@
 | 
			
		||||
		try {
 | 
			
		||||
			const { data: currentConfig } = await api.systemConfigApi.getConfig();
 | 
			
		||||
 | 
			
		||||
			if (!oauthConfig.mobileOverrideEnabled) {
 | 
			
		||||
				oauthConfig.mobileRedirectUri = '';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const result = await api.systemConfigApi.updateConfig({
 | 
			
		||||
				...currentConfig,
 | 
			
		||||
				oauth: oauthConfig
 | 
			
		||||
@@ -50,12 +63,8 @@
 | 
			
		||||
				message: 'OAuth settings saved',
 | 
			
		||||
				type: NotificationType.Info
 | 
			
		||||
			});
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.error('Error [oauth-settings] [saveSetting]', e);
 | 
			
		||||
			notificationController.show({
 | 
			
		||||
				message: 'Unable to save settings',
 | 
			
		||||
				type: NotificationType.Error
 | 
			
		||||
			});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			handleError(error, 'Unable to save OAuth settings');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -74,76 +83,95 @@
 | 
			
		||||
<div class="mt-2">
 | 
			
		||||
	{#await getConfigs() then}
 | 
			
		||||
		<div in:fade={{ duration: 500 }}>
 | 
			
		||||
			<form autocomplete="off" on:submit|preventDefault>
 | 
			
		||||
				<div class="mt-4">
 | 
			
		||||
					<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
 | 
			
		||||
				</div>
 | 
			
		||||
			<form autocomplete="off" on:submit|preventDefault class="flex flex-col mx-4 gap-4 py-4">
 | 
			
		||||
				<p class="text-sm dark:text-immich-dark-fg">
 | 
			
		||||
					For more details about this feature, refer to the <a
 | 
			
		||||
						href="http://immich.app/docs/features/oauth#mobile-redirect-uri"
 | 
			
		||||
						class="underline"
 | 
			
		||||
						target="_blank"
 | 
			
		||||
						rel="noreferrer">docs</a
 | 
			
		||||
					>.
 | 
			
		||||
				</p>
 | 
			
		||||
 | 
			
		||||
				<hr class="m-4" />
 | 
			
		||||
				<div class="flex flex-col gap-4 ml-4">
 | 
			
		||||
				<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
 | 
			
		||||
				<hr />
 | 
			
		||||
				<SettingInputField
 | 
			
		||||
					inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
					label="ISSUER URL"
 | 
			
		||||
					bind:value={oauthConfig.issuerUrl}
 | 
			
		||||
					required={true}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingInputField
 | 
			
		||||
					inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
					label="CLIENT ID"
 | 
			
		||||
					bind:value={oauthConfig.clientId}
 | 
			
		||||
					required={true}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingInputField
 | 
			
		||||
					inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
					label="CLIENT SECRET"
 | 
			
		||||
					bind:value={oauthConfig.clientSecret}
 | 
			
		||||
					required={true}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingInputField
 | 
			
		||||
					inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
					label="SCOPE"
 | 
			
		||||
					bind:value={oauthConfig.scope}
 | 
			
		||||
					required={true}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					isEdited={!(oauthConfig.scope == savedConfig.scope)}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingInputField
 | 
			
		||||
					inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
					label="BUTTON TEXT"
 | 
			
		||||
					bind:value={oauthConfig.buttonText}
 | 
			
		||||
					required={false}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingSwitch
 | 
			
		||||
					title="AUTO REGISTER"
 | 
			
		||||
					subtitle="Automatically register new users after signing in with OAuth"
 | 
			
		||||
					bind:checked={oauthConfig.autoRegister}
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<SettingSwitch
 | 
			
		||||
					title="MOBILE REDIRECT URI OVERRIDE"
 | 
			
		||||
					subtitle="Enable when `app.immich:/` is an invalid redirect URI."
 | 
			
		||||
					disabled={!oauthConfig.enabled}
 | 
			
		||||
					on:click={() => handleToggleOverride()}
 | 
			
		||||
					bind:checked={oauthConfig.mobileOverrideEnabled}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				{#if oauthConfig.mobileOverrideEnabled}
 | 
			
		||||
					<SettingInputField
 | 
			
		||||
						inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
						label="ISSUER URL"
 | 
			
		||||
						bind:value={oauthConfig.issuerUrl}
 | 
			
		||||
						label="MOBILE REDIRECT URI"
 | 
			
		||||
						bind:value={oauthConfig.mobileRedirectUri}
 | 
			
		||||
						required={true}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
						isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
 | 
			
		||||
						isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
 | 
			
		||||
					/>
 | 
			
		||||
				{/if}
 | 
			
		||||
 | 
			
		||||
					<SettingInputField
 | 
			
		||||
						inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
						label="CLIENT ID"
 | 
			
		||||
						bind:value={oauthConfig.clientId}
 | 
			
		||||
						required={true}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
						isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
 | 
			
		||||
					/>
 | 
			
		||||
 | 
			
		||||
					<SettingInputField
 | 
			
		||||
						inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
						label="CLIENT SECRET"
 | 
			
		||||
						bind:value={oauthConfig.clientSecret}
 | 
			
		||||
						required={true}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
						isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
 | 
			
		||||
					/>
 | 
			
		||||
 | 
			
		||||
					<SettingInputField
 | 
			
		||||
						inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
						label="SCOPE"
 | 
			
		||||
						bind:value={oauthConfig.scope}
 | 
			
		||||
						required={true}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
						isEdited={!(oauthConfig.scope == savedConfig.scope)}
 | 
			
		||||
					/>
 | 
			
		||||
 | 
			
		||||
					<SettingInputField
 | 
			
		||||
						inputType={SettingInputFieldType.TEXT}
 | 
			
		||||
						label="BUTTON TEXT"
 | 
			
		||||
						bind:value={oauthConfig.buttonText}
 | 
			
		||||
						required={false}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
						isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="mt-4">
 | 
			
		||||
					<SettingSwitch
 | 
			
		||||
						title="AUTO REGISTER"
 | 
			
		||||
						subtitle="Automatically register new users after signing in with OAuth"
 | 
			
		||||
						bind:checked={oauthConfig.autoRegister}
 | 
			
		||||
						disabled={!oauthConfig.enabled}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="ml-4">
 | 
			
		||||
					<SettingButtonsRow
 | 
			
		||||
						on:reset={reset}
 | 
			
		||||
						on:save={saveSetting}
 | 
			
		||||
						on:reset-to-default={resetToDefault}
 | 
			
		||||
						showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<SettingButtonsRow
 | 
			
		||||
					on:reset={reset}
 | 
			
		||||
					on:save={saveSetting}
 | 
			
		||||
					on:reset-to-default={resetToDefault}
 | 
			
		||||
					showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
 | 
			
		||||
				/>
 | 
			
		||||
			</form>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/await}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
	export let disabled = false;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex justify-between mx-4 place-items-center">
 | 
			
		||||
<div class="flex justify-between place-items-center">
 | 
			
		||||
	<div>
 | 
			
		||||
		<h2 class="immich-form-label text-sm">
 | 
			
		||||
			{title.toUpperCase()}
 | 
			
		||||
@@ -19,6 +19,7 @@
 | 
			
		||||
			class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
 | 
			
		||||
			type="checkbox"
 | 
			
		||||
			bind:checked
 | 
			
		||||
			on:click
 | 
			
		||||
			{disabled}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user