1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +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
* @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<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
@ -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<RequestArgs> => {
@ -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<OAuthAuthorizeResponseDto>> {
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<OAuthConfigResponseDto>> {
@ -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<OAuthAuthorizeResponseDto> {
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<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.
* @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
*/

View File

@ -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

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": {
"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": {

View File

@ -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<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(
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);
try {
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) {

View File

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

View File

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

View File

@ -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<OAuthConfigResponseDto> {
return this.service.generateConfig(dto);
}
@PublicRoute()
@Post('authorize')
authorizeOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute()
@Post('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
* @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<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
@ -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<RequestArgs> => {
@ -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<OAuthAuthorizeResponseDto>> {
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<OAuthConfigResponseDto>> {
@ -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<OAuthAuthorizeResponseDto> {
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<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.
* @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
*/

View File

@ -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) => {

View File

@ -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);
};
</script>
{#if authConfig.passwordLoginEnabled}
{#if !oauthLoading && $featureFlags.passwordLogin}
<form on:submit|preventDefault={login} class="mt-5 flex flex-col gap-5">
{#if errorMessage}
<p class="text-red-400" transition:fade>
@ -113,7 +117,7 @@
</div>
<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}
<span class="h-6">
<LoadingSpinner />
@ -126,8 +130,8 @@
</form>
{/if}
{#if authConfig.enabled}
{#if authConfig.passwordLoginEnabled}
{#if $featureFlags.oauth}
{#if $featureFlags.passwordLogin}
<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" />
<span
@ -139,28 +143,27 @@
{/if}
<div class="my-5 flex flex-col gap-5">
{#if oauthError}
<p class="text-red-400" transition:fade>{oauthError}</p>
<p class="text-center text-red-400" transition:fade>{oauthError}</p>
{/if}
<a href={authConfig.url} class="flex w-full">
<Button
type="button"
disabled={loading || oauthLoading}
size="lg"
fullwidth
color={authConfig.passwordLoginEnabled ? 'secondary' : 'primary'}
color={$featureFlags.passwordLogin ? 'secondary' : 'primary'}
on:click={handleOAuthLogin}
>
{#if oauthLoading}
<span class="h-6">
<LoadingSpinner />
</span>
{:else}
{authConfig.buttonText || 'Login with OAuth'}
{$featureFlags.passwordLogin ? 'Login with OAuth' : 'Login'}
{/if}
</Button>
</a>
</div>
{/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>
{/if}

View File

@ -5,7 +5,7 @@
export let showMessage = $$slots.message;
</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
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">
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 { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import Button from '../elements/buttons/button.svelte';
export let user: UserResponseDto;
let config: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: true };
let loading = true;
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;
});
@ -63,13 +56,11 @@
<div class="flex place-content-center place-items-center">
<LoadingSpinner />
</div>
{:else if config.enabled}
{:else if $featureFlags.oauth}
{#if user.oauthId}
<Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button>
{:else}
<a href={config.url}>
<Button size="sm" on:click={() => handleUnlink()}>Link to OAuth</Button>
</a>
<Button size="sm" on:click={() => oauth.authorize(window.location)}>Link to OAuth</Button>
{/if}
{/if}
</div>

View File

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

View File

@ -1,21 +1,22 @@
import { api, ServerFeaturesDto } from '@api';
import { writable } from 'svelte/store';
export type FeatureFlags = ServerFeaturesDto;
export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
export const featureFlags = writable<FeatureFlags>({
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 }));
};

View File

@ -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',
},

View File

@ -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;
</script>
<FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
{#if $featureFlags.loaded}
<FullscreenContainer title={data.meta.title} showMessage={!!loginPageMessage}>
<p slot="message">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html loginPageMessage}
</p>
<LoginForm
authConfig={data.authConfig}
on:success={() => goto(AppRoute.PHOTOS, { invalidateAll: true })}
on:first-login={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)}
/>
</FullscreenContainer>
</FullscreenContainer>
{/if}