Просмотр исходного кода

refactor(server): api key auth (#3054)

Jason Rasmussen 2 лет назад
Родитель
Сommit
399312ead3

+ 0 - 22
server/src/domain/api-key/api-key-response.dto.ts

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

+ 0 - 28
server/src/domain/api-key/api-key.core.ts

@@ -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 - 0
server/src/domain/api-key/api-key.dto.ts

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

+ 1 - 0
server/src/domain/api-key/api-key.service.spec.ts

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

+ 17 - 8
server/src/domain/api-key/api-key.service.ts

@@ -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 } from './api-key.dto';
+import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } 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, {
-      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> {
   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,
+    };
   }
   }
 }
 }

+ 0 - 1
server/src/domain/api-key/index.ts

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

+ 23 - 7
server/src/domain/auth/auth.service.ts

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