mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(server): api key auth (#3054)
This commit is contained in:
		@@ -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()
 | 
			
		||||
  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 () => {
 | 
			
		||||
      keyMock.getById.mockResolvedValue(keyStub.admin);
 | 
			
		||||
      keyMock.update.mockResolvedValue(keyStub.admin);
 | 
			
		||||
 | 
			
		||||
      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 { AuthUserDto } from '../auth';
 | 
			
		||||
import { ICryptoRepository } from '../crypto';
 | 
			
		||||
import { APIKeyCreateResponseDto, APIKeyResponseDto, mapKey } from './api-key-response.dto';
 | 
			
		||||
import { APIKeyCreateDto } from './api-key.dto';
 | 
			
		||||
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
 | 
			
		||||
import { IKeyRepository } from './api-key.repository';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@@ -20,7 +20,7 @@ export class APIKeyService {
 | 
			
		||||
      userId: authUser.id,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return { secret, apiKey: mapKey(entity) };
 | 
			
		||||
    return { secret, apiKey: this.map(entity) };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
 | 
			
		||||
@@ -29,9 +29,9 @@ export class APIKeyService {
 | 
			
		||||
      throw new BadRequestException('API Key not found');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.repository.update(authUser.id, id, {
 | 
			
		||||
      name: dto.name,
 | 
			
		||||
    });
 | 
			
		||||
    const key = await this.repository.update(authUser.id, id, { name: dto.name });
 | 
			
		||||
 | 
			
		||||
    return this.map(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(authUser: AuthUserDto, id: string): Promise<void> {
 | 
			
		||||
@@ -48,11 +48,20 @@ export class APIKeyService {
 | 
			
		||||
    if (!key) {
 | 
			
		||||
      throw new BadRequestException('API Key not found');
 | 
			
		||||
    }
 | 
			
		||||
    return mapKey(key);
 | 
			
		||||
    return this.map(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
 | 
			
		||||
    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.repository';
 | 
			
		||||
export * from './api-key.service';
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ import {
 | 
			
		||||
import cookieParser from 'cookie';
 | 
			
		||||
import { IncomingHttpHeaders } from 'http';
 | 
			
		||||
import { IKeyRepository } from '../api-key';
 | 
			
		||||
import { APIKeyCore } from '../api-key/api-key.core';
 | 
			
		||||
import { ICryptoRepository } from '../crypto/crypto.repository';
 | 
			
		||||
import { OAuthCore } from '../oauth/oauth.core';
 | 
			
		||||
import { ISharedLinkRepository } from '../shared-link';
 | 
			
		||||
@@ -35,17 +34,16 @@ export class AuthService {
 | 
			
		||||
  private authCore: AuthCore;
 | 
			
		||||
  private oauthCore: OAuthCore;
 | 
			
		||||
  private userCore: UserCore;
 | 
			
		||||
  private keyCore: APIKeyCore;
 | 
			
		||||
 | 
			
		||||
  private logger = new Logger(AuthService.name);
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
 | 
			
		||||
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
			
		||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
			
		||||
    @Inject(IUserRepository) userRepository: IUserRepository,
 | 
			
		||||
    @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
 | 
			
		||||
    @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
 | 
			
		||||
    @Inject(IKeyRepository) keyRepository: IKeyRepository,
 | 
			
		||||
    @Inject(IKeyRepository) private keyRepository: IKeyRepository,
 | 
			
		||||
    @Inject(INITIAL_SYSTEM_CONFIG)
 | 
			
		||||
    initialConfig: SystemConfig,
 | 
			
		||||
  ) {
 | 
			
		||||
@@ -53,7 +51,6 @@ export class AuthService {
 | 
			
		||||
    this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
 | 
			
		||||
    this.oauthCore = new OAuthCore(configRepository, initialConfig);
 | 
			
		||||
    this.userCore = new UserCore(userRepository, cryptoRepository);
 | 
			
		||||
    this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async login(
 | 
			
		||||
@@ -153,7 +150,7 @@ export class AuthService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (apiKey) {
 | 
			
		||||
      return this.keyCore.validate(apiKey);
 | 
			
		||||
      return this.validateApiKey(apiKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new UnauthorizedException('Authentication required');
 | 
			
		||||
@@ -192,7 +189,7 @@ export class AuthService {
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
 | 
			
		||||
@@ -216,4 +213,23 @@ export class AuthService {
 | 
			
		||||
    }
 | 
			
		||||
    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');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user