diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 8d15a19d7d..70cd8fa297 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -1861,6 +1861,19 @@ export const ModelType = { export type ModelType = typeof ModelType[keyof typeof ModelType]; +/** + * + * @export + * @interface OAuthAuthorizeResponseDto + */ +export interface OAuthAuthorizeResponseDto { + /** + * + * @type {string} + * @memberof OAuthAuthorizeResponseDto + */ + 'url': string; +} /** * * @export @@ -8890,6 +8903,41 @@ export class JobApi extends BaseAPI { */ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {OAuthConfigDto} oAuthConfigDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authorizeOAuth: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'oAuthConfigDto' is not null or undefined + assertParamExists('authorizeOAuth', 'oAuthConfigDto', oAuthConfigDto) + const localVarPath = `/oauth/authorize`; + // 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: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(oAuthConfigDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -8926,9 +8974,10 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthConfigDto} oAuthConfigDto * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise => { @@ -9081,6 +9130,16 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration export const OAuthApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration) return { + /** + * + * @param {OAuthConfigDto} oAuthConfigDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authorizeOAuth(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeOAuth(oAuthConfigDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -9092,9 +9151,10 @@ export const OAuthApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthConfigDto} oAuthConfigDto * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { @@ -9139,6 +9199,15 @@ export const OAuthApiFp = function(configuration?: Configuration) { export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = OAuthApiFp(configuration) return { + /** + * + * @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {OAuthApiCallbackRequest} requestParameters Request parameters. @@ -9149,9 +9218,10 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath return localVarFp.callback(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath)); }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise { @@ -9185,6 +9255,20 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath }; }; +/** + * Request parameters for authorizeOAuth operation in OAuthApi. + * @export + * @interface OAuthApiAuthorizeOAuthRequest + */ +export interface OAuthApiAuthorizeOAuthRequest { + /** + * + * @type {OAuthConfigDto} + * @memberof OAuthApiAuthorizeOAuth + */ + readonly oAuthConfigDto: OAuthConfigDto +} + /** * Request parameters for callback operation in OAuthApi. * @export @@ -9234,6 +9318,17 @@ export interface OAuthApiLinkRequest { * @extends {BaseAPI} */ export class OAuthApi extends BaseAPI { + /** + * + * @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof OAuthApi + */ + public authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig) { + return OAuthApiFp(this.configuration).authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {OAuthApiCallbackRequest} requestParameters Request parameters. @@ -9246,9 +9341,10 @@ export class OAuthApi extends BaseAPI { } /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof OAuthApi */ diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 517c33f249..5c6b65f46f 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -73,6 +73,7 @@ doc/MemoryLaneResponseDto.md doc/MergePersonDto.md doc/ModelType.md doc/OAuthApi.md +doc/OAuthAuthorizeResponseDto.md doc/OAuthCallbackDto.md doc/OAuthConfigDto.md doc/OAuthConfigResponseDto.md @@ -225,6 +226,7 @@ lib/model/map_marker_response_dto.dart lib/model/memory_lane_response_dto.dart lib/model/merge_person_dto.dart lib/model/model_type.dart +lib/model/o_auth_authorize_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart lib/model/o_auth_config_response_dto.dart @@ -352,6 +354,7 @@ test/memory_lane_response_dto_test.dart test/merge_person_dto_test.dart test/model_type_test.dart test/o_auth_api_test.dart +test/o_auth_authorize_response_dto_test.dart test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart test/o_auth_config_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 76503a4149..5972ea91cf 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/OAuthApi.md b/mobile/openapi/doc/OAuthApi.md index 519a96d9ee..5a5afc2a15 100644 Binary files a/mobile/openapi/doc/OAuthApi.md and b/mobile/openapi/doc/OAuthApi.md differ diff --git a/mobile/openapi/doc/OAuthAuthorizeResponseDto.md b/mobile/openapi/doc/OAuthAuthorizeResponseDto.md new file mode 100644 index 0000000000..fe32d2b85f Binary files /dev/null and b/mobile/openapi/doc/OAuthAuthorizeResponseDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 08ac25105b..b31f92e8c2 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/o_auth_api.dart b/mobile/openapi/lib/api/o_auth_api.dart index b8778596a7..0f2e72e2fa 100644 Binary files a/mobile/openapi/lib/api/o_auth_api.dart and b/mobile/openapi/lib/api/o_auth_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f900ea54b4..f660f07e95 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart new file mode 100644 index 0000000000..1a1c092873 Binary files /dev/null and b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart differ diff --git a/mobile/openapi/test/o_auth_api_test.dart b/mobile/openapi/test/o_auth_api_test.dart index bc8b5f3810..7a3ee0a9fb 100644 Binary files a/mobile/openapi/test/o_auth_api_test.dart and b/mobile/openapi/test/o_auth_api_test.dart differ diff --git a/mobile/openapi/test/o_auth_authorize_response_dto_test.dart b/mobile/openapi/test/o_auth_authorize_response_dto_test.dart new file mode 100644 index 0000000000..76b016642a Binary files /dev/null and b/mobile/openapi/test/o_auth_authorize_response_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 00765a38f2..4178f39eba 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -2477,6 +2477,37 @@ ] } }, + "/oauth/authorize": { + "post": { + "operationId": "authorizeOAuth", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthConfigDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeResponseDto" + } + } + }, + "description": "" + } + }, + "tags": [ + "OAuth" + ] + } + }, "/oauth/callback": { "post": { "operationId": "callback", @@ -2510,6 +2541,8 @@ }, "/oauth/config": { "post": { + "deprecated": true, + "description": "@deprecated use feature flags and /oauth/authorize", "operationId": "generateConfig", "parameters": [], "requestBody": { @@ -6202,6 +6235,17 @@ ], "type": "string" }, + "OAuthAuthorizeResponseDto": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + }, "OAuthCallbackDto": { "properties": { "url": { diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index cebb3d4626..bf7891ed0e 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -1,5 +1,12 @@ import { SystemConfig, UserEntity } from '@app/infra/entities'; -import { BadRequestException, Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + UnauthorizedException, +} from '@nestjs/common'; import cookieParser from 'cookie'; import { IncomingHttpHeaders } from 'http'; import { DateTime } from 'luxon'; @@ -27,6 +34,7 @@ import { mapAdminSignupResponse, mapLoginResponse, mapUserToken, + OAuthAuthorizeResponseDto, OAuthConfigResponseDto, } from './response-dto'; import { IUserTokenRepository } from './user-token.repository'; @@ -201,6 +209,22 @@ export class AuthService { return { ...response, buttonText, url, autoLaunch }; } + async authorize(dto: OAuthConfigDto): Promise { + const config = await this.configCore.getConfig(); + if (!config.oauth.enabled) { + throw new BadRequestException('OAuth is not enabled'); + } + + const client = await this.getOAuthClient(config); + const url = await client.authorizationUrl({ + redirect_uri: this.normalize(config, dto.redirectUri), + scope: config.oauth.scope, + state: generators.state(), + }); + + return { url }; + } + async callback( dto: OAuthCallbackDto, loginDetails: LoginDetails, @@ -280,8 +304,13 @@ export class AuthService { const redirectUri = this.normalize(config, url.split('?')[0]); const client = await this.getOAuthClient(config); const params = client.callbackParams(url); - const tokens = await client.callback(redirectUri, params, { state: params.state }); - return client.userinfo(tokens.access_token || ''); + try { + const tokens = await client.callback(redirectUri, params, { state: params.state }); + return client.userinfo(tokens.access_token || ''); + } catch (error: Error | any) { + this.logger.error(`Unable to complete OAuth login: ${error}`, error?.stack); + throw new InternalServerErrorException(`Unable to complete OAuth login: ${error}`, { cause: error }); + } } private async getOAuthClient(config: SystemConfig) { diff --git a/server/src/domain/auth/dto/oauth-config.dto.ts b/server/src/domain/auth/dto/oauth-config.dto.ts index a15a963bfa..a14fc19dc2 100644 --- a/server/src/domain/auth/dto/oauth-config.dto.ts +++ b/server/src/domain/auth/dto/oauth-config.dto.ts @@ -1,9 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class OAuthConfigDto { @IsNotEmpty() @IsString() - @ApiProperty() redirectUri!: string; } diff --git a/server/src/domain/auth/response-dto/oauth-config-response.dto.ts b/server/src/domain/auth/response-dto/oauth-config-response.dto.ts index c239405fc0..dadd66f59a 100644 --- a/server/src/domain/auth/response-dto/oauth-config-response.dto.ts +++ b/server/src/domain/auth/response-dto/oauth-config-response.dto.ts @@ -5,3 +5,7 @@ export class OAuthConfigResponseDto { buttonText?: string; autoLaunch?: boolean; } + +export class OAuthAuthorizeResponseDto { + url!: string; +} diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts index 24cc8afe45..eec8627199 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/immich/controllers/oauth.controller.ts @@ -3,6 +3,7 @@ import { AuthUserDto, LoginDetails, LoginResponseDto, + OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, OAuthConfigResponseDto, @@ -31,12 +32,19 @@ export class OAuthController { }; } + /** @deprecated use feature flags and /oauth/authorize */ @PublicRoute() @Post('config') generateConfig(@Body() dto: OAuthConfigDto): Promise { return this.service.generateConfig(dto); } + @PublicRoute() + @Post('authorize') + authorizeOAuth(@Body() dto: OAuthConfigDto): Promise { + return this.service.authorize(dto); + } + @PublicRoute() @Post('callback') async callback( diff --git a/server/test/e2e/oauth.e2e-spec.ts b/server/test/e2e/oauth.e2e-spec.ts new file mode 100644 index 0000000000..a9f0027819 --- /dev/null +++ b/server/test/e2e/oauth.e2e-spec.ts @@ -0,0 +1,42 @@ +import { AppModule, OAuthController } from '@app/immich'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { errorStub } from '../fixtures'; +import { api, db } from '../test-utils'; + +describe(`${OAuthController.name} (e2e)`, () => { + let app: INestApplication; + let server: any; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = await moduleFixture.createNestApplication().init(); + server = app.getHttpServer(); + }); + + beforeEach(async () => { + await db.reset(); + await api.adminSignUp(server); + }); + + afterAll(async () => { + await db.disconnect(); + await app.close(); + }); + + describe('POST /oauth/authorize', () => { + beforeEach(async () => { + await db.reset(); + }); + + it(`should throw an error if a redirect uri is not provided`, async () => { + const { status, body } = await request(server).post('/oauth/authorize').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest); + }); + }); +}); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 8d15a19d7d..70cd8fa297 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1861,6 +1861,19 @@ export const ModelType = { export type ModelType = typeof ModelType[keyof typeof ModelType]; +/** + * + * @export + * @interface OAuthAuthorizeResponseDto + */ +export interface OAuthAuthorizeResponseDto { + /** + * + * @type {string} + * @memberof OAuthAuthorizeResponseDto + */ + 'url': string; +} /** * * @export @@ -8890,6 +8903,41 @@ export class JobApi extends BaseAPI { */ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {OAuthConfigDto} oAuthConfigDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authorizeOAuth: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'oAuthConfigDto' is not null or undefined + assertParamExists('authorizeOAuth', 'oAuthConfigDto', oAuthConfigDto) + const localVarPath = `/oauth/authorize`; + // 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: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(oAuthConfigDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -8926,9 +8974,10 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthConfigDto} oAuthConfigDto * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise => { @@ -9081,6 +9130,16 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration export const OAuthApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration) return { + /** + * + * @param {OAuthConfigDto} oAuthConfigDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authorizeOAuth(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeOAuth(oAuthConfigDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {OAuthCallbackDto} oAuthCallbackDto @@ -9092,9 +9151,10 @@ export const OAuthApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthConfigDto} oAuthConfigDto * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { @@ -9139,6 +9199,15 @@ export const OAuthApiFp = function(configuration?: Configuration) { export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = OAuthApiFp(configuration) return { + /** + * + * @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {OAuthApiCallbackRequest} requestParameters Request parameters. @@ -9149,9 +9218,10 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath return localVarFp.callback(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath)); }, /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise { @@ -9185,6 +9255,20 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath }; }; +/** + * Request parameters for authorizeOAuth operation in OAuthApi. + * @export + * @interface OAuthApiAuthorizeOAuthRequest + */ +export interface OAuthApiAuthorizeOAuthRequest { + /** + * + * @type {OAuthConfigDto} + * @memberof OAuthApiAuthorizeOAuth + */ + readonly oAuthConfigDto: OAuthConfigDto +} + /** * Request parameters for callback operation in OAuthApi. * @export @@ -9234,6 +9318,17 @@ export interface OAuthApiLinkRequest { * @extends {BaseAPI} */ export class OAuthApi extends BaseAPI { + /** + * + * @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof OAuthApi + */ + public authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig) { + return OAuthApiFp(this.configuration).authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {OAuthApiCallbackRequest} requestParameters Request parameters. @@ -9246,9 +9341,10 @@ export class OAuthApi extends BaseAPI { } /** - * + * @deprecated use feature flags and /oauth/authorize * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof OAuthApi */ diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts index 26b6550666..f82596eef2 100644 --- a/web/src/api/utils.ts +++ b/web/src/api/utils.ts @@ -1,3 +1,4 @@ +import { goto } from '$app/navigation'; import type { AxiosError, AxiosPromise } from 'axios'; import { notificationController, @@ -32,9 +33,17 @@ export const oauth = { } return false; }, + authorize: async (location: Location) => { + try { + const redirectUri = location.href.split('?')[0]; + const { data } = await api.oauthApi.authorizeOAuth({ oAuthConfigDto: { redirectUri } }); + goto(data.url); + } catch (error) { + handleError(error, 'Unable to login with OAuth'); + } + }, getConfig: (location: Location) => { const redirectUri = location.href.split('?')[0]; - console.log(`OAuth Redirect URI: ${redirectUri}`); return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } }); }, login: (location: Location) => { diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index 8ededa6b7f..0dbc069f35 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -2,8 +2,9 @@ import { goto } from '$app/navigation'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { AppRoute } from '$lib/constants'; + import { featureFlags } from '$lib/stores/feature-flags.store'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; - import { OAuthConfigResponseDto, api, oauth } from '@api'; + import { api, oauth } from '@api'; import { createEventDispatcher, onMount } from 'svelte'; import { fade } from 'svelte/transition'; import Button from '../elements/buttons/button.svelte'; @@ -11,14 +12,18 @@ let errorMessage: string; let email = ''; let password = ''; - let oauthError: string; - export let authConfig: OAuthConfigResponseDto; + let oauthError = ''; let loading = false; let oauthLoading = true; const dispatch = createEventDispatcher(); onMount(async () => { + if (!$featureFlags.oauth) { + oauthLoading = false; + return; + } + if (oauth.isCallback(window.location)) { try { await oauth.login(window.location); @@ -26,25 +31,18 @@ return; } catch (e) { console.error('Error [login-form] [oauth.callback]', e); - oauthError = 'Unable to complete OAuth login'; - } finally { + oauthError = (await getServerErrorMessage(e)) || 'Unable to complete OAuth login'; oauthLoading = false; } } try { - const { data } = await oauth.getConfig(window.location); - authConfig = data; - - const { enabled, url, autoLaunch } = authConfig; - - if (enabled && url && autoLaunch && !oauth.isAutoLaunchDisabled(window.location)) { + if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(window.location)) { await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true }); - await goto(url); + await oauth.authorize(window.location); return; } } catch (error) { - authConfig.passwordLoginEnabled = true; await handleError(error, 'Unable to connect!'); } @@ -76,9 +74,15 @@ return; } }; + + const handleOAuthLogin = async () => { + oauthLoading = true; + oauthError = ''; + await oauth.authorize(window.location); + }; -{#if authConfig.passwordLoginEnabled} +{#if !oauthLoading && $featureFlags.passwordLogin}
{#if errorMessage}

@@ -113,7 +117,7 @@

- - +
{/if} -{#if !authConfig.enabled && !authConfig.passwordLoginEnabled} +{#if !$featureFlags.passwordLogin && !$featureFlags.oauth}

Login has been disabled.

{/if} diff --git a/web/src/lib/components/shared-components/fullscreen-container.svelte b/web/src/lib/components/shared-components/fullscreen-container.svelte index 75fd0bc4e7..13903ccd48 100644 --- a/web/src/lib/components/shared-components/fullscreen-container.svelte +++ b/web/src/lib/components/shared-components/fullscreen-container.svelte @@ -5,7 +5,7 @@ export let showMessage = $$slots.message; -
+
diff --git a/web/src/lib/components/user-settings-page/oauth-settings.svelte b/web/src/lib/components/user-settings-page/oauth-settings.svelte index 15c1a7e911..17e49d65f3 100644 --- a/web/src/lib/components/user-settings-page/oauth-settings.svelte +++ b/web/src/lib/components/user-settings-page/oauth-settings.svelte @@ -1,16 +1,16 @@ @@ -47,7 +40,7 @@ -{#if oauthEnabled} +{#if $featureFlags.loaded && $featureFlags.oauth} ({ + loaded: false, clipEncode: true, facialRecognition: true, sidecar: true, tagImage: true, search: true, - oauth: true, - oauthAutoLaunch: true, + oauth: false, + oauthAutoLaunch: false, passwordLogin: true, configFile: false, }); export const loadFeatureFlags = async () => { const { data } = await api.serverInfoApi.getServerFeatures(); - featureFlags.update(() => data); + featureFlags.update(() => ({ ...data, loaded: true })); }; diff --git a/web/src/routes/auth/login/+page.server.ts b/web/src/routes/auth/login/+page.server.ts index 5e10dcd55e..f325c7cd2e 100644 --- a/web/src/routes/auth/login/+page.server.ts +++ b/web/src/routes/auth/login/+page.server.ts @@ -1,5 +1,4 @@ import { AppRoute } from '$lib/constants'; -import type { OAuthConfigResponseDto } from '@api'; import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; @@ -10,23 +9,7 @@ export const load = (async ({ locals: { api } }) => { throw redirect(302, AppRoute.AUTH_REGISTER); } - let authConfig: OAuthConfigResponseDto = { - passwordLoginEnabled: true, - enabled: false, - }; - - try { - // TODO: Figure out how to get correct redirect URI server-side. - const { data } = await api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri: '/' } }); - data.url = undefined; - - authConfig = data; - } catch (err) { - console.error('[ERROR] login/+page.server.ts:', err); - } - return { - authConfig, meta: { title: 'Login', }, diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index e958864fb2..c3e4e8a418 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -4,20 +4,22 @@ import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import { AppRoute } from '$lib/constants'; import { loginPageMessage } from '$lib/constants'; + import { featureFlags } from '$lib/stores/feature-flags.store'; import type { PageData } from './$types'; export let data: PageData; - -

- - {@html loginPageMessage} -

+{#if $featureFlags.loaded} + +

+ + {@html loginPageMessage} +

- goto(AppRoute.PHOTOS, { invalidateAll: true })} - on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)} - /> -
+ goto(AppRoute.PHOTOS, { invalidateAll: true })} + on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)} + /> +
+{/if}