1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

refactor(web,server): use feature flags for oauth (#3928)

* refactor: oauth to use feature flags

* chore: open api

* chore: e2e test for authorize endpoint
This commit is contained in:
Jason Rasmussen 2023-09-01 07:08:42 -04:00 committed by GitHub
parent c7d53a5006
commit a26ed3d1a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 411 additions and 109 deletions

View File

@ -1861,6 +1861,19 @@ export const ModelType = {
export type ModelType = typeof ModelType[keyof typeof ModelType]; export type ModelType = typeof ModelType[keyof typeof ModelType];
/**
*
* @export
* @interface OAuthAuthorizeResponseDto
*/
export interface OAuthAuthorizeResponseDto {
/**
*
* @type {string}
* @memberof OAuthAuthorizeResponseDto
*/
'url': string;
}
/** /**
* *
* @export * @export
@ -8890,6 +8903,41 @@ export class JobApi extends BaseAPI {
*/ */
export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) { export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) {
return { return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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 * @param {OAuthCallbackDto} oAuthCallbackDto
@ -8926,9 +8974,10 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
}; };
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto * @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -9081,6 +9130,16 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
export const OAuthApiFp = function(configuration?: Configuration) { export const OAuthApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration)
return { return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async authorizeOAuth(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthAuthorizeResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeOAuth(oAuthConfigDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {OAuthCallbackDto} oAuthCallbackDto * @param {OAuthCallbackDto} oAuthCallbackDto
@ -9092,9 +9151,10 @@ export const OAuthApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto * @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthConfigResponseDto>> { async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthConfigResponseDto>> {
@ -9139,6 +9199,15 @@ export const OAuthApiFp = function(configuration?: Configuration) {
export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = OAuthApiFp(configuration) const localVarFp = OAuthApiFp(configuration)
return { return {
/**
*
* @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthAuthorizeResponseDto> {
return localVarFp.authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {OAuthApiCallbackRequest} requestParameters Request parameters. * @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)); return localVarFp.callback(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath));
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthConfigResponseDto> { generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthConfigResponseDto> {
@ -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. * Request parameters for callback operation in OAuthApi.
* @export * @export
@ -9234,6 +9318,17 @@ export interface OAuthApiLinkRequest {
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class OAuthApi 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. * @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 {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
* @memberof OAuthApi * @memberof OAuthApi
*/ */

View File

@ -73,6 +73,7 @@ doc/MemoryLaneResponseDto.md
doc/MergePersonDto.md doc/MergePersonDto.md
doc/ModelType.md doc/ModelType.md
doc/OAuthApi.md doc/OAuthApi.md
doc/OAuthAuthorizeResponseDto.md
doc/OAuthCallbackDto.md doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md doc/OAuthConfigResponseDto.md
@ -225,6 +226,7 @@ lib/model/map_marker_response_dto.dart
lib/model/memory_lane_response_dto.dart lib/model/memory_lane_response_dto.dart
lib/model/merge_person_dto.dart lib/model/merge_person_dto.dart
lib/model/model_type.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_callback_dto.dart
lib/model/o_auth_config_dto.dart lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_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/merge_person_dto_test.dart
test/model_type_test.dart test/model_type_test.dart
test/o_auth_api_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_callback_dto_test.dart
test/o_auth_config_dto_test.dart test/o_auth_config_dto_test.dart
test/o_auth_config_response_dto_test.dart test/o_auth_config_response_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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": { "/oauth/callback": {
"post": { "post": {
"operationId": "callback", "operationId": "callback",
@ -2510,6 +2541,8 @@
}, },
"/oauth/config": { "/oauth/config": {
"post": { "post": {
"deprecated": true,
"description": "@deprecated use feature flags and /oauth/authorize",
"operationId": "generateConfig", "operationId": "generateConfig",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@ -6202,6 +6235,17 @@
], ],
"type": "string" "type": "string"
}, },
"OAuthAuthorizeResponseDto": {
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
},
"OAuthCallbackDto": { "OAuthCallbackDto": {
"properties": { "properties": {
"url": { "url": {

View File

@ -1,5 +1,12 @@
import { SystemConfig, UserEntity } from '@app/infra/entities'; 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 cookieParser from 'cookie';
import { IncomingHttpHeaders } from 'http'; import { IncomingHttpHeaders } from 'http';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -27,6 +34,7 @@ import {
mapAdminSignupResponse, mapAdminSignupResponse,
mapLoginResponse, mapLoginResponse,
mapUserToken, mapUserToken,
OAuthAuthorizeResponseDto,
OAuthConfigResponseDto, OAuthConfigResponseDto,
} from './response-dto'; } from './response-dto';
import { IUserTokenRepository } from './user-token.repository'; import { IUserTokenRepository } from './user-token.repository';
@ -201,6 +209,22 @@ export class AuthService {
return { ...response, buttonText, url, autoLaunch }; return { ...response, buttonText, url, autoLaunch };
} }
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
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( async callback(
dto: OAuthCallbackDto, dto: OAuthCallbackDto,
loginDetails: LoginDetails, loginDetails: LoginDetails,
@ -280,8 +304,13 @@ export class AuthService {
const redirectUri = this.normalize(config, url.split('?')[0]); const redirectUri = this.normalize(config, url.split('?')[0]);
const client = await this.getOAuthClient(config); const client = await this.getOAuthClient(config);
const params = client.callbackParams(url); const params = client.callbackParams(url);
const tokens = await client.callback(redirectUri, params, { state: params.state }); try {
return client.userinfo<OAuthProfile>(tokens.access_token || ''); const tokens = await client.callback(redirectUri, params, { state: params.state });
return client.userinfo<OAuthProfile>(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) { private async getOAuthClient(config: SystemConfig) {

View File

@ -1,9 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
export class OAuthConfigDto { export class OAuthConfigDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@ApiProperty()
redirectUri!: string; redirectUri!: string;
} }

View File

@ -5,3 +5,7 @@ export class OAuthConfigResponseDto {
buttonText?: string; buttonText?: string;
autoLaunch?: boolean; autoLaunch?: boolean;
} }
export class OAuthAuthorizeResponseDto {
url!: string;
}

View File

@ -3,6 +3,7 @@ import {
AuthUserDto, AuthUserDto,
LoginDetails, LoginDetails,
LoginResponseDto, LoginResponseDto,
OAuthAuthorizeResponseDto,
OAuthCallbackDto, OAuthCallbackDto,
OAuthConfigDto, OAuthConfigDto,
OAuthConfigResponseDto, OAuthConfigResponseDto,
@ -31,12 +32,19 @@ export class OAuthController {
}; };
} }
/** @deprecated use feature flags and /oauth/authorize */
@PublicRoute() @PublicRoute()
@Post('config') @Post('config')
generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> { generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
return this.service.generateConfig(dto); return this.service.generateConfig(dto);
} }
@PublicRoute()
@Post('authorize')
authorizeOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute() @PublicRoute()
@Post('callback') @Post('callback')
async callback( async callback(

View File

@ -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);
});
});
});

View File

@ -1861,6 +1861,19 @@ export const ModelType = {
export type ModelType = typeof ModelType[keyof typeof ModelType]; export type ModelType = typeof ModelType[keyof typeof ModelType];
/**
*
* @export
* @interface OAuthAuthorizeResponseDto
*/
export interface OAuthAuthorizeResponseDto {
/**
*
* @type {string}
* @memberof OAuthAuthorizeResponseDto
*/
'url': string;
}
/** /**
* *
* @export * @export
@ -8890,6 +8903,41 @@ export class JobApi extends BaseAPI {
*/ */
export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) { export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) {
return { return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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 * @param {OAuthCallbackDto} oAuthCallbackDto
@ -8926,9 +8974,10 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
}; };
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto * @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -9081,6 +9130,16 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
export const OAuthApiFp = function(configuration?: Configuration) { export const OAuthApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration)
return { return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async authorizeOAuth(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthAuthorizeResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeOAuth(oAuthConfigDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {OAuthCallbackDto} oAuthCallbackDto * @param {OAuthCallbackDto} oAuthCallbackDto
@ -9092,9 +9151,10 @@ export const OAuthApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto * @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthConfigResponseDto>> { async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthConfigResponseDto>> {
@ -9139,6 +9199,15 @@ export const OAuthApiFp = function(configuration?: Configuration) {
export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = OAuthApiFp(configuration) const localVarFp = OAuthApiFp(configuration)
return { return {
/**
*
* @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthAuthorizeResponseDto> {
return localVarFp.authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {OAuthApiCallbackRequest} requestParameters Request parameters. * @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)); return localVarFp.callback(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath));
}, },
/** /**
* * @deprecated use feature flags and /oauth/authorize
* @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters. * @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthConfigResponseDto> { generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthConfigResponseDto> {
@ -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. * Request parameters for callback operation in OAuthApi.
* @export * @export
@ -9234,6 +9318,17 @@ export interface OAuthApiLinkRequest {
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class OAuthApi 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. * @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 {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
* @memberof OAuthApi * @memberof OAuthApi
*/ */

View File

@ -1,3 +1,4 @@
import { goto } from '$app/navigation';
import type { AxiosError, AxiosPromise } from 'axios'; import type { AxiosError, AxiosPromise } from 'axios';
import { import {
notificationController, notificationController,
@ -32,9 +33,17 @@ export const oauth = {
} }
return false; 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) => { getConfig: (location: Location) => {
const redirectUri = location.href.split('?')[0]; const redirectUri = location.href.split('?')[0];
console.log(`OAuth Redirect URI: ${redirectUri}`);
return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } }); return api.oauthApi.generateConfig({ oAuthConfigDto: { redirectUri } });
}, },
login: (location: Location) => { login: (location: Location) => {

View File

@ -2,8 +2,9 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { featureFlags } from '$lib/stores/feature-flags.store';
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; 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 { createEventDispatcher, onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
@ -11,14 +12,18 @@
let errorMessage: string; let errorMessage: string;
let email = ''; let email = '';
let password = ''; let password = '';
let oauthError: string; let oauthError = '';
export let authConfig: OAuthConfigResponseDto;
let loading = false; let loading = false;
let oauthLoading = true; let oauthLoading = true;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(async () => { onMount(async () => {
if (!$featureFlags.oauth) {
oauthLoading = false;
return;
}
if (oauth.isCallback(window.location)) { if (oauth.isCallback(window.location)) {
try { try {
await oauth.login(window.location); await oauth.login(window.location);
@ -26,25 +31,18 @@
return; return;
} catch (e) { } catch (e) {
console.error('Error [login-form] [oauth.callback]', e); console.error('Error [login-form] [oauth.callback]', e);
oauthError = 'Unable to complete OAuth login'; oauthError = (await getServerErrorMessage(e)) || 'Unable to complete OAuth login';
} finally {
oauthLoading = false; oauthLoading = false;
} }
} }
try { try {
const { data } = await oauth.getConfig(window.location); if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
authConfig = data;
const { enabled, url, autoLaunch } = authConfig;
if (enabled && url && autoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true }); await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true });
await goto(url); await oauth.authorize(window.location);
return; return;
} }
} catch (error) { } catch (error) {
authConfig.passwordLoginEnabled = true;
await handleError(error, 'Unable to connect!'); await handleError(error, 'Unable to connect!');
} }
@ -76,9 +74,15 @@
return; return;
} }
}; };
const handleOAuthLogin = async () => {
oauthLoading = true;
oauthError = '';
await oauth.authorize(window.location);
};
</script> </script>
{#if authConfig.passwordLoginEnabled} {#if !oauthLoading && $featureFlags.passwordLogin}
<form on:submit|preventDefault={login} class="mt-5 flex flex-col gap-5"> <form on:submit|preventDefault={login} class="mt-5 flex flex-col gap-5">
{#if errorMessage} {#if errorMessage}
<p class="text-red-400" transition:fade> <p class="text-red-400" transition:fade>
@ -113,7 +117,7 @@
</div> </div>
<div class="my-5 flex w-full"> <div class="my-5 flex w-full">
<Button type="submit" size="lg" fullwidth disabled={loading || oauthLoading}> <Button type="submit" size="lg" fullwidth disabled={loading}>
{#if loading} {#if loading}
<span class="h-6"> <span class="h-6">
<LoadingSpinner /> <LoadingSpinner />
@ -126,8 +130,8 @@
</form> </form>
{/if} {/if}
{#if authConfig.enabled} {#if $featureFlags.oauth}
{#if authConfig.passwordLoginEnabled} {#if $featureFlags.passwordLogin}
<div class="inline-flex w-full items-center justify-center"> <div class="inline-flex w-full items-center justify-center">
<hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" /> <hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
<span <span
@ -139,28 +143,27 @@
{/if} {/if}
<div class="my-5 flex flex-col gap-5"> <div class="my-5 flex flex-col gap-5">
{#if oauthError} {#if oauthError}
<p class="text-red-400" transition:fade>{oauthError}</p> <p class="text-center text-red-400" transition:fade>{oauthError}</p>
{/if} {/if}
<a href={authConfig.url} class="flex w-full"> <Button
<Button type="button"
type="button" disabled={loading || oauthLoading}
disabled={loading || oauthLoading} size="lg"
size="lg" fullwidth
fullwidth color={$featureFlags.passwordLogin ? 'secondary' : 'primary'}
color={authConfig.passwordLoginEnabled ? 'secondary' : 'primary'} on:click={handleOAuthLogin}
> >
{#if oauthLoading} {#if oauthLoading}
<span class="h-6"> <span class="h-6">
<LoadingSpinner /> <LoadingSpinner />
</span> </span>
{:else} {:else}
{authConfig.buttonText || 'Login with OAuth'} {$featureFlags.passwordLogin ? 'Login with OAuth' : 'Login'}
{/if} {/if}
</Button> </Button>
</a>
</div> </div>
{/if} {/if}
{#if !authConfig.enabled && !authConfig.passwordLoginEnabled} {#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
<p class="p-4 text-center dark:text-immich-dark-fg">Login has been disabled.</p> <p class="p-4 text-center dark:text-immich-dark-fg">Login has been disabled.</p>
{/if} {/if}

View File

@ -5,7 +5,7 @@
export let showMessage = $$slots.message; export let showMessage = $$slots.message;
</script> </script>
<section class="flex min-h-screen w-screen place-content-center place-items-center p-4"> <section class="min-w-screen flex min-h-screen place-content-center place-items-center p-4">
<div <div
class="flex w-full max-w-lg flex-col gap-4 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray" class="flex w-full max-w-lg flex-col gap-4 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
> >

View File

@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { oauth, OAuthConfigResponseDto, UserResponseDto } from '@api'; import { featureFlags } from '$lib/stores/feature-flags.store';
import { oauth, UserResponseDto } from '@api';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification'; import { notificationController, NotificationType } from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
let config: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: true };
let loading = true; let loading = true;
onMount(async () => { onMount(async () => {
@ -32,13 +32,6 @@
} }
} }
try {
const { data } = await oauth.getConfig(window.location);
config = data;
} catch (error) {
handleError(error, 'Unable to load OAuth config');
}
loading = false; loading = false;
}); });
@ -63,13 +56,11 @@
<div class="flex place-content-center place-items-center"> <div class="flex place-content-center place-items-center">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
{:else if config.enabled} {:else if $featureFlags.oauth}
{#if user.oauthId} {#if user.oauthId}
<Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button> <Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button>
{:else} {:else}
<a href={config.url}> <Button size="sm" on:click={() => oauth.authorize(window.location)}>Link to OAuth</Button>
<Button size="sm" on:click={() => handleUnlink()}>Link to OAuth</Button>
</a>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { featureFlags } from '$lib/stores/feature-flags.store';
import { APIKeyResponseDto, AuthDeviceResponseDto, oauth, UserResponseDto } from '@api'; import { APIKeyResponseDto, AuthDeviceResponseDto, oauth, UserResponseDto } from '@api';
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte'; import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
import ChangePasswordSettings from './change-password-settings.svelte'; import ChangePasswordSettings from './change-password-settings.svelte';
@ -9,7 +11,6 @@
import PartnerSettings from './partner-settings.svelte'; import PartnerSettings from './partner-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte'; import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte'; import UserProfileSettings from './user-profile-settings.svelte';
import { onMount } from 'svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
@ -17,18 +18,10 @@
export let devices: AuthDeviceResponseDto[] = []; export let devices: AuthDeviceResponseDto[] = [];
export let partners: UserResponseDto[] = []; export let partners: UserResponseDto[] = [];
let oauthEnabled = false;
let oauthOpen = false; let oauthOpen = false;
if (browser) {
onMount(async () => {
oauthOpen = oauth.isCallback(window.location); oauthOpen = oauth.isCallback(window.location);
try { }
const { data } = await oauth.getConfig(window.location);
oauthEnabled = data.enabled;
} catch {
// noop
}
});
</script> </script>
<SettingAccordion title="Account" subtitle="Manage your account"> <SettingAccordion title="Account" subtitle="Manage your account">
@ -47,7 +40,7 @@
<MemoriesSettings {user} /> <MemoriesSettings {user} />
</SettingAccordion> </SettingAccordion>
{#if oauthEnabled} {#if $featureFlags.loaded && $featureFlags.oauth}
<SettingAccordion <SettingAccordion
title="OAuth" title="OAuth"
subtitle="Manage your OAuth connection" subtitle="Manage your OAuth connection"

View File

@ -1,21 +1,22 @@
import { api, ServerFeaturesDto } from '@api'; import { api, ServerFeaturesDto } from '@api';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export type FeatureFlags = ServerFeaturesDto; export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
export const featureFlags = writable<FeatureFlags>({ export const featureFlags = writable<FeatureFlags>({
loaded: false,
clipEncode: true, clipEncode: true,
facialRecognition: true, facialRecognition: true,
sidecar: true, sidecar: true,
tagImage: true, tagImage: true,
search: true, search: true,
oauth: true, oauth: false,
oauthAutoLaunch: true, oauthAutoLaunch: false,
passwordLogin: true, passwordLogin: true,
configFile: false, configFile: false,
}); });
export const loadFeatureFlags = async () => { export const loadFeatureFlags = async () => {
const { data } = await api.serverInfoApi.getServerFeatures(); const { data } = await api.serverInfoApi.getServerFeatures();
featureFlags.update(() => data); featureFlags.update(() => ({ ...data, loaded: true }));
}; };

View File

@ -1,5 +1,4 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { OAuthConfigResponseDto } from '@api';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
@ -10,23 +9,7 @@ export const load = (async ({ locals: { api } }) => {
throw redirect(302, AppRoute.AUTH_REGISTER); 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 { return {
authConfig,
meta: { meta: {
title: 'Login', title: 'Login',
}, },

View File

@ -4,20 +4,22 @@
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { loginPageMessage } from '$lib/constants'; import { loginPageMessage } from '$lib/constants';
import { featureFlags } from '$lib/stores/feature-flags.store';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
</script> </script>
<FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}> {#if $featureFlags.loaded}
<p slot="message"> <FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
<!-- eslint-disable-next-line svelte/no-at-html-tags --> <p slot="message">
{@html loginPageMessage} <!-- eslint-disable-next-line svelte/no-at-html-tags -->
</p> {@html loginPageMessage}
</p>
<LoginForm <LoginForm
authConfig={data.authConfig} on:success={() => goto(AppRoute.PHOTOS, { invalidateAll: true })}
on:success={() => goto(AppRoute.PHOTOS, { invalidateAll: true })} on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)}
on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)} />
/> </FullscreenContainer>
</FullscreenContainer> {/if}