mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-02-07 00:45:51 +00: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:
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
const tokens = await client.callback(redirectUri, params, { state: params.state });
|
||||
return client.userinfo<OAuthProfile>(tokens.access_token || '');
|
||||
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) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class OAuthConfigDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
redirectUri!: string;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ export class OAuthConfigResponseDto {
|
||||
buttonText?: string;
|
||||
autoLaunch?: boolean;
|
||||
}
|
||||
|
||||
export class OAuthAuthorizeResponseDto {
|
||||
url!: string;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
42
server/test/e2e/oauth.e2e-spec.ts
Normal file
42
server/test/e2e/oauth.e2e-spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user