mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
refactor(server): api key auth (#3054)
This commit is contained in:
parent
f9671dfbf7
commit
399312ead3
@ -1,22 +0,0 @@
|
|||||||
import { APIKeyEntity } from '@app/infra/entities';
|
|
||||||
|
|
||||||
export class APIKeyCreateResponseDto {
|
|
||||||
secret!: string;
|
|
||||||
apiKey!: APIKeyResponseDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class APIKeyResponseDto {
|
|
||||||
id!: string;
|
|
||||||
name!: string;
|
|
||||||
createdAt!: Date;
|
|
||||||
updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKey(entity: APIKeyEntity): APIKeyResponseDto {
|
|
||||||
return {
|
|
||||||
id: entity.id,
|
|
||||||
name: entity.name,
|
|
||||||
createdAt: entity.createdAt,
|
|
||||||
updatedAt: entity.updatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { AuthUserDto } from '../auth';
|
|
||||||
import { ICryptoRepository } from '../crypto';
|
|
||||||
import { IKeyRepository } from './api-key.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class APIKeyCore {
|
|
||||||
constructor(private crypto: ICryptoRepository, private repository: IKeyRepository) {}
|
|
||||||
|
|
||||||
async validate(token: string): Promise<AuthUserDto | null> {
|
|
||||||
const hashedToken = this.crypto.hashSha256(token);
|
|
||||||
const keyEntity = await this.repository.getKey(hashedToken);
|
|
||||||
if (keyEntity?.user) {
|
|
||||||
const user = keyEntity.user;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
isPublicUser: false,
|
|
||||||
isAllowUpload: true,
|
|
||||||
externalPath: user.externalPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UnauthorizedException('Invalid API key');
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,3 +12,15 @@ export class APIKeyUpdateDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name!: string;
|
name!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class APIKeyCreateResponseDto {
|
||||||
|
secret!: string;
|
||||||
|
apiKey!: APIKeyResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIKeyResponseDto {
|
||||||
|
id!: string;
|
||||||
|
name!: string;
|
||||||
|
createdAt!: Date;
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
|
@ -56,6 +56,7 @@ describe(APIKeyService.name, () => {
|
|||||||
|
|
||||||
it('should update a key', async () => {
|
it('should update a key', async () => {
|
||||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
keyMock.getById.mockResolvedValue(keyStub.admin);
|
||||||
|
keyMock.update.mockResolvedValue(keyStub.admin);
|
||||||
|
|
||||||
await sut.update(authStub.admin, 'random-guid', { name: 'New Name' });
|
await sut.update(authStub.admin, 'random-guid', { name: 'New Name' });
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import { APIKeyEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { ICryptoRepository } from '../crypto';
|
import { ICryptoRepository } from '../crypto';
|
||||||
import { APIKeyCreateResponseDto, APIKeyResponseDto, mapKey } from './api-key-response.dto';
|
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
|
||||||
import { APIKeyCreateDto } from './api-key.dto';
|
|
||||||
import { IKeyRepository } from './api-key.repository';
|
import { IKeyRepository } from './api-key.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -20,7 +20,7 @@ export class APIKeyService {
|
|||||||
userId: authUser.id,
|
userId: authUser.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secret, apiKey: mapKey(entity) };
|
return { secret, apiKey: this.map(entity) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
|
async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
|
||||||
@ -29,9 +29,9 @@ export class APIKeyService {
|
|||||||
throw new BadRequestException('API Key not found');
|
throw new BadRequestException('API Key not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.repository.update(authUser.id, id, {
|
const key = await this.repository.update(authUser.id, id, { name: dto.name });
|
||||||
name: dto.name,
|
|
||||||
});
|
return this.map(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(authUser: AuthUserDto, id: string): Promise<void> {
|
async delete(authUser: AuthUserDto, id: string): Promise<void> {
|
||||||
@ -48,11 +48,20 @@ export class APIKeyService {
|
|||||||
if (!key) {
|
if (!key) {
|
||||||
throw new BadRequestException('API Key not found');
|
throw new BadRequestException('API Key not found');
|
||||||
}
|
}
|
||||||
return mapKey(key);
|
return this.map(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
|
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
|
||||||
const keys = await this.repository.getByUserId(authUser.id);
|
const keys = await this.repository.getByUserId(authUser.id);
|
||||||
return keys.map(mapKey);
|
return keys.map((key) => this.map(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private map(entity: APIKeyEntity): APIKeyResponseDto {
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
name: entity.name,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
updatedAt: entity.updatedAt,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export * from './api-key-response.dto';
|
|
||||||
export * from './api-key.dto';
|
export * from './api-key.dto';
|
||||||
export * from './api-key.repository';
|
export * from './api-key.repository';
|
||||||
export * from './api-key.service';
|
export * from './api-key.service';
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import cookieParser from 'cookie';
|
import cookieParser from 'cookie';
|
||||||
import { IncomingHttpHeaders } from 'http';
|
import { IncomingHttpHeaders } from 'http';
|
||||||
import { IKeyRepository } from '../api-key';
|
import { IKeyRepository } from '../api-key';
|
||||||
import { APIKeyCore } from '../api-key/api-key.core';
|
|
||||||
import { ICryptoRepository } from '../crypto/crypto.repository';
|
import { ICryptoRepository } from '../crypto/crypto.repository';
|
||||||
import { OAuthCore } from '../oauth/oauth.core';
|
import { OAuthCore } from '../oauth/oauth.core';
|
||||||
import { ISharedLinkRepository } from '../shared-link';
|
import { ISharedLinkRepository } from '../shared-link';
|
||||||
@ -35,17 +34,16 @@ export class AuthService {
|
|||||||
private authCore: AuthCore;
|
private authCore: AuthCore;
|
||||||
private oauthCore: OAuthCore;
|
private oauthCore: OAuthCore;
|
||||||
private userCore: UserCore;
|
private userCore: UserCore;
|
||||||
private keyCore: APIKeyCore;
|
|
||||||
|
|
||||||
private logger = new Logger(AuthService.name);
|
private logger = new Logger(AuthService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IUserRepository) userRepository: IUserRepository,
|
@Inject(IUserRepository) userRepository: IUserRepository,
|
||||||
@Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
|
@Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
|
||||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IKeyRepository) keyRepository: IKeyRepository,
|
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||||
@Inject(INITIAL_SYSTEM_CONFIG)
|
@Inject(INITIAL_SYSTEM_CONFIG)
|
||||||
initialConfig: SystemConfig,
|
initialConfig: SystemConfig,
|
||||||
) {
|
) {
|
||||||
@ -53,7 +51,6 @@ export class AuthService {
|
|||||||
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
|
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
|
||||||
this.oauthCore = new OAuthCore(configRepository, initialConfig);
|
this.oauthCore = new OAuthCore(configRepository, initialConfig);
|
||||||
this.userCore = new UserCore(userRepository, cryptoRepository);
|
this.userCore = new UserCore(userRepository, cryptoRepository);
|
||||||
this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
@ -153,7 +150,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
return this.keyCore.validate(apiKey);
|
return this.validateApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException('Authentication required');
|
throw new UnauthorizedException('Authentication required');
|
||||||
@ -192,7 +189,7 @@ export class AuthService {
|
|||||||
return cookies[IMMICH_ACCESS_COOKIE] || null;
|
return cookies[IMMICH_ACCESS_COOKIE] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateSharedLink(key: string | string[]): Promise<AuthUserDto | null> {
|
private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> {
|
||||||
key = Array.isArray(key) ? key[0] : key;
|
key = Array.isArray(key) ? key[0] : key;
|
||||||
|
|
||||||
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
||||||
@ -216,4 +213,23 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
throw new UnauthorizedException('Invalid share key');
|
throw new UnauthorizedException('Invalid share key');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateApiKey(key: string): Promise<AuthUserDto> {
|
||||||
|
const hashedKey = this.cryptoRepository.hashSha256(key);
|
||||||
|
const keyEntity = await this.keyRepository.getKey(hashedKey);
|
||||||
|
if (keyEntity?.user) {
|
||||||
|
const user = keyEntity.user;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
isPublicUser: false,
|
||||||
|
isAllowUpload: true,
|
||||||
|
externalPath: user.externalPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnauthorizedException('Invalid API key');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user