mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(server): auth guard (#1472)
* refactor: auth guard * chore: move auth guard to middleware * chore: tests * chore: remove unused code * fix: migration to uuid without dataloss * chore: e2e tests * chore: removed unused guards
This commit is contained in:
		| @@ -19,7 +19,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco | |||||||
|   async handleConnection(client: Socket) { |   async handleConnection(client: Socket) { | ||||||
|     try { |     try { | ||||||
|       this.logger.log(`New websocket connection: ${client.id}`); |       this.logger.log(`New websocket connection: ${client.id}`); | ||||||
|       const user = await this.authService.validate(client.request.headers); |       const user = await this.authService.validate(client.request.headers, {}); | ||||||
|       if (user) { |       if (user) { | ||||||
|         client.join(user.id); |         client.join(user.id); | ||||||
|       } else { |       } else { | ||||||
|   | |||||||
| @@ -21,9 +21,8 @@ import { | |||||||
|   SystemConfigController, |   SystemConfigController, | ||||||
|   UserController, |   UserController, | ||||||
| } from './controllers'; | } from './controllers'; | ||||||
| import { PublicShareStrategy } from './modules/immich-auth/strategies/public-share.strategy'; | import { APP_GUARD } from '@nestjs/core'; | ||||||
| import { APIKeyStrategy } from './modules/immich-auth/strategies/api-key.strategy'; | import { AuthGuard } from './middlewares/auth.guard'; | ||||||
| import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.strategy'; |  | ||||||
|  |  | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [ |   imports: [ | ||||||
| @@ -61,7 +60,7 @@ import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.str | |||||||
|     SystemConfigController, |     SystemConfigController, | ||||||
|     UserController, |     UserController, | ||||||
|   ], |   ], | ||||||
|   providers: [UserAuthStrategy, APIKeyStrategy, PublicShareStrategy], |   providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard], | ||||||
| }) | }) | ||||||
| export class AppModule implements NestModule { | export class AppModule implements NestModule { | ||||||
|   // TODO: check if consumer is needed or remove |   // TODO: check if consumer is needed or remove | ||||||
|   | |||||||
| @@ -1,25 +1,28 @@ | |||||||
| import { UseGuards } from '@nestjs/common'; | import { applyDecorators, SetMetadata } from '@nestjs/common'; | ||||||
| import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware'; |  | ||||||
| import { RouteNotSharedGuard } from '../middlewares/route-not-shared-guard.middleware'; |  | ||||||
| import { AuthGuard } from '../modules/immich-auth/guards/auth.guard'; |  | ||||||
|  |  | ||||||
| interface AuthenticatedOptions { | interface AuthenticatedOptions { | ||||||
|   admin?: boolean; |   admin?: boolean; | ||||||
|   isShared?: boolean; |   isShared?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum Metadata { | ||||||
|  |   AUTH_ROUTE = 'auth_route', | ||||||
|  |   ADMIN_ROUTE = 'admin_route', | ||||||
|  |   SHARED_ROUTE = 'shared_route', | ||||||
|  | } | ||||||
|  |  | ||||||
| export const Authenticated = (options?: AuthenticatedOptions) => { | export const Authenticated = (options?: AuthenticatedOptions) => { | ||||||
|   const guards: Parameters<typeof UseGuards> = [AuthGuard]; |   const decorators = [SetMetadata(Metadata.AUTH_ROUTE, true)]; | ||||||
|  |  | ||||||
|   options = options || {}; |   options = options || {}; | ||||||
|  |  | ||||||
|   if (options.admin) { |   if (options.admin) { | ||||||
|     guards.push(AdminRolesGuard); |     decorators.push(SetMetadata(Metadata.ADMIN_ROUTE, true)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!options.isShared) { |   if (options.isShared) { | ||||||
|     guards.push(RouteNotSharedGuard); |     decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return UseGuards(...guards); |   return applyDecorators(...decorators); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,23 +0,0 @@ | |||||||
| import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; |  | ||||||
| import { Request } from 'express'; |  | ||||||
| import { UserResponseDto } from '@app/domain'; |  | ||||||
|  |  | ||||||
| interface UserRequest extends Request { |  | ||||||
|   user: UserResponseDto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class AdminRolesGuard implements CanActivate { |  | ||||||
|   logger = new Logger(AdminRolesGuard.name); |  | ||||||
|  |  | ||||||
|   async canActivate(context: ExecutionContext): Promise<boolean> { |  | ||||||
|     const request = context.switchToHttp().getRequest<UserRequest>(); |  | ||||||
|     const isAdmin = request.user?.isAdmin || false; |  | ||||||
|     if (!isAdmin) { |  | ||||||
|       this.logger.log(`Denied access to admin only route: ${request.path}`); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										46
									
								
								server/apps/immich/src/middlewares/auth.guard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/apps/immich/src/middlewares/auth.guard.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | import { AuthService } from '@app/domain'; | ||||||
|  | import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; | ||||||
|  | import { Reflector } from '@nestjs/core'; | ||||||
|  | import { Request } from 'express'; | ||||||
|  | import { Metadata } from '../decorators/authenticated.decorator'; | ||||||
|  |  | ||||||
|  | @Injectable() | ||||||
|  | export class AuthGuard implements CanActivate { | ||||||
|  |   private logger = new Logger(AuthGuard.name); | ||||||
|  |  | ||||||
|  |   constructor(private reflector: Reflector, private authService: AuthService) {} | ||||||
|  |  | ||||||
|  |   async canActivate(context: ExecutionContext): Promise<boolean> { | ||||||
|  |     const targets = [context.getHandler(), context.getClass()]; | ||||||
|  |  | ||||||
|  |     const isAuthRoute = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets); | ||||||
|  |     const isAdminRoute = this.reflector.getAllAndOverride(Metadata.ADMIN_ROUTE, targets); | ||||||
|  |     const isSharedRoute = this.reflector.getAllAndOverride(Metadata.SHARED_ROUTE, targets); | ||||||
|  |  | ||||||
|  |     if (!isAuthRoute) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const req = context.switchToHttp().getRequest<Request>(); | ||||||
|  |  | ||||||
|  |     const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>); | ||||||
|  |     if (!authDto) { | ||||||
|  |       this.logger.warn(`Denied access to authenticated route: ${req.path}`); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (authDto.isPublicUser && !isSharedRoute) { | ||||||
|  |       this.logger.warn(`Denied access to non-shared route: ${req.path}`); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (isAdminRoute && !authDto.isAdmin) { | ||||||
|  |       this.logger.warn(`Denied access to admin only route: ${req.path}`); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     req.user = authDto; | ||||||
|  |  | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; |  | ||||||
| import { Request } from 'express'; |  | ||||||
| import { AuthUserDto } from '../decorators/auth-user.decorator'; |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class RouteNotSharedGuard implements CanActivate { |  | ||||||
|   logger = new Logger(RouteNotSharedGuard.name); |  | ||||||
|  |  | ||||||
|   async canActivate(context: ExecutionContext): Promise<boolean> { |  | ||||||
|     const request = context.switchToHttp().getRequest<Request>(); |  | ||||||
|     const user = request.user as AuthUserDto; |  | ||||||
|  |  | ||||||
|     // Inverse logic - I know it is weird |  | ||||||
|     if (user.isPublicUser) { |  | ||||||
|       this.logger.warn(`Denied attempt to access non-shared route: ${request.path}`); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| import { Injectable } from '@nestjs/common'; |  | ||||||
| import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; |  | ||||||
| import { API_KEY_STRATEGY } from '../strategies/api-key.strategy'; |  | ||||||
| import { AUTH_COOKIE_STRATEGY } from '../strategies/user-auth.strategy'; |  | ||||||
| import { PUBLIC_SHARE_STRATEGY } from '../strategies/public-share.strategy'; |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class AuthGuard extends PassportAuthGuard([PUBLIC_SHARE_STRATEGY, AUTH_COOKIE_STRATEGY, API_KEY_STRATEGY]) {} |  | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| import { APIKeyService, AuthUserDto } from '@app/domain'; |  | ||||||
| import { Injectable } from '@nestjs/common'; |  | ||||||
| import { PassportStrategy } from '@nestjs/passport'; |  | ||||||
| import { IStrategyOptions, Strategy } from 'passport-http-header-strategy'; |  | ||||||
|  |  | ||||||
| export const API_KEY_STRATEGY = 'api-key'; |  | ||||||
|  |  | ||||||
| const options: IStrategyOptions = { |  | ||||||
|   header: 'x-api-key', |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY) { |  | ||||||
|   constructor(private apiKeyService: APIKeyService) { |  | ||||||
|     super(options); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   validate(token: string): Promise<AuthUserDto | null> { |  | ||||||
|     return this.apiKeyService.validate(token); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| import { Injectable } from '@nestjs/common'; |  | ||||||
| import { PassportStrategy } from '@nestjs/passport'; |  | ||||||
| import { IStrategyOptions, Strategy } from 'passport-http-header-strategy'; |  | ||||||
| import { AuthUserDto, ShareService } from '@app/domain'; |  | ||||||
|  |  | ||||||
| export const PUBLIC_SHARE_STRATEGY = 'public-share'; |  | ||||||
|  |  | ||||||
| const options: IStrategyOptions = { |  | ||||||
|   header: 'x-immich-share-key', |  | ||||||
|   param: 'key', |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class PublicShareStrategy extends PassportStrategy(Strategy, PUBLIC_SHARE_STRATEGY) { |  | ||||||
|   constructor(private shareService: ShareService) { |  | ||||||
|     super(options); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   validate(key: string): Promise<AuthUserDto | null> { |  | ||||||
|     return this.shareService.validate(key); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| import { AuthService, AuthUserDto } from '@app/domain'; |  | ||||||
| import { Injectable } from '@nestjs/common'; |  | ||||||
| import { PassportStrategy } from '@nestjs/passport'; |  | ||||||
| import { Request } from 'express'; |  | ||||||
| import { Strategy } from 'passport-custom'; |  | ||||||
|  |  | ||||||
| export const AUTH_COOKIE_STRATEGY = 'auth-cookie'; |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class UserAuthStrategy extends PassportStrategy(Strategy, AUTH_COOKIE_STRATEGY) { |  | ||||||
|   constructor(private authService: AuthService) { |  | ||||||
|     super(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   validate(request: Request): Promise<AuthUserDto | null> { |  | ||||||
|     return this.authService.validate(request.headers); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -2,11 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; | |||||||
| import { INestApplication } from '@nestjs/common'; | import { INestApplication } from '@nestjs/common'; | ||||||
| import request from 'supertest'; | import request from 'supertest'; | ||||||
| import { clearDb, getAuthUser, authCustom } from './test-utils'; | import { clearDb, getAuthUser, authCustom } from './test-utils'; | ||||||
| import { InfraModule } from '@app/infra'; |  | ||||||
| import { AlbumModule } from '../src/api-v1/album/album.module'; |  | ||||||
| import { CreateAlbumDto } from '../src/api-v1/album/dto/create-album.dto'; | import { CreateAlbumDto } from '../src/api-v1/album/dto/create-album.dto'; | ||||||
| import { AuthUserDto } from '../src/decorators/auth-user.decorator'; | import { AuthUserDto } from '../src/decorators/auth-user.decorator'; | ||||||
| import { AuthService, DomainModule, UserService } from '@app/domain'; | import { AuthService, UserService } from '@app/domain'; | ||||||
| import { DataSource } from 'typeorm'; | import { DataSource } from 'typeorm'; | ||||||
| import { AppModule } from '../src/app.module'; | import { AppModule } from '../src/app.module'; | ||||||
|  |  | ||||||
| @@ -20,9 +18,7 @@ describe('Album', () => { | |||||||
|  |  | ||||||
|   describe('without auth', () => { |   describe('without auth', () => { | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|       const moduleFixture: TestingModule = await Test.createTestingModule({ |       const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); | ||||||
|         imports: [DomainModule.register({ imports: [InfraModule] }), AppModule], |  | ||||||
|       }).compile(); |  | ||||||
|  |  | ||||||
|       app = moduleFixture.createNestApplication(); |       app = moduleFixture.createNestApplication(); | ||||||
|       database = app.get(DataSource); |       database = app.get(DataSource); | ||||||
| @@ -46,9 +42,7 @@ describe('Album', () => { | |||||||
|     let authService: AuthService; |     let authService: AuthService; | ||||||
|  |  | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|       const builder = Test.createTestingModule({ |       const builder = Test.createTestingModule({ imports: [AppModule] }); | ||||||
|         imports: [DomainModule.register({ imports: [InfraModule] }), AlbumModule], |  | ||||||
|       }); |  | ||||||
|       authUser = getAuthUser(); // set default auth user |       authUser = getAuthUser(); // set default auth user | ||||||
|       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); |       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext } from '@nestjs/common'; | |||||||
| import { TestingModuleBuilder } from '@nestjs/testing'; | import { TestingModuleBuilder } from '@nestjs/testing'; | ||||||
| import { DataSource } from 'typeorm'; | import { DataSource } from 'typeorm'; | ||||||
| import { AuthUserDto } from '../src/decorators/auth-user.decorator'; | import { AuthUserDto } from '../src/decorators/auth-user.decorator'; | ||||||
| import { AuthGuard } from '../src/modules/immich-auth/guards/auth.guard'; | import { AuthGuard } from '../src/middlewares/auth.guard'; | ||||||
|  |  | ||||||
| type CustomAuthCallback = () => AuthUserDto; | type CustomAuthCallback = () => AuthUserDto; | ||||||
|  |  | ||||||
| @@ -34,5 +34,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa | |||||||
|       return true; |       return true; | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
|   return builder.overrideGuard(AuthGuard).useValue(canActivate); |   return builder.overrideProvider(AuthGuard).useValue(canActivate); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,10 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; | |||||||
| import { INestApplication } from '@nestjs/common'; | import { INestApplication } from '@nestjs/common'; | ||||||
| import request from 'supertest'; | import request from 'supertest'; | ||||||
| import { clearDb, authCustom } from './test-utils'; | import { clearDb, authCustom } from './test-utils'; | ||||||
| import { InfraModule } from '@app/infra'; | import { CreateUserDto, UserService, AuthUserDto } from '@app/domain'; | ||||||
| import { DomainModule, CreateUserDto, UserService, AuthUserDto } from '@app/domain'; |  | ||||||
| import { DataSource } from 'typeorm'; | import { DataSource } from 'typeorm'; | ||||||
| import { UserController } from '../src/controllers'; |  | ||||||
| import { AuthService } from '@app/domain'; | import { AuthService } from '@app/domain'; | ||||||
| import { AppModule } from '../src/app.module'; | import { AppModule } from '../src/app.module'; | ||||||
|  |  | ||||||
| @@ -24,10 +22,7 @@ describe('User', () => { | |||||||
|  |  | ||||||
|   describe('without auth', () => { |   describe('without auth', () => { | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|       const moduleFixture: TestingModule = await Test.createTestingModule({ |       const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); | ||||||
|         imports: [DomainModule.register({ imports: [InfraModule] }), AppModule], |  | ||||||
|         controllers: [UserController], |  | ||||||
|       }).compile(); |  | ||||||
|  |  | ||||||
|       app = moduleFixture.createNestApplication(); |       app = moduleFixture.createNestApplication(); | ||||||
|       database = app.get(DataSource); |       database = app.get(DataSource); | ||||||
| @@ -50,10 +45,7 @@ describe('User', () => { | |||||||
|     let authUser: AuthUserDto; |     let authUser: AuthUserDto; | ||||||
|  |  | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|       const builder = Test.createTestingModule({ |       const builder = Test.createTestingModule({ imports: [AppModule] }); | ||||||
|         imports: [DomainModule.register({ imports: [InfraModule] })], |  | ||||||
|         controllers: [UserController], |  | ||||||
|       }); |  | ||||||
|       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); |       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); | ||||||
|  |  | ||||||
|       app = moduleFixture.createNestApplication(); |       app = moduleFixture.createNestApplication(); | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								server/libs/domain/src/api-key/api-key.core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								server/libs/domain/src/api-key/api-key.core.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | 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, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     throw new UnauthorizedException('Invalid API key'); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,20 +1,9 @@ | |||||||
| import { APIKeyEntity } from '@app/infra/db/entities'; |  | ||||||
| import { BadRequestException } from '@nestjs/common'; | import { BadRequestException } from '@nestjs/common'; | ||||||
| import { authStub, userEntityStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test'; | import { authStub, keyStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test'; | ||||||
| import { ICryptoRepository } from '../auth'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { IKeyRepository } from './api-key.repository'; | import { IKeyRepository } from './api-key.repository'; | ||||||
| import { APIKeyService } from './api-key.service'; | import { APIKeyService } from './api-key.service'; | ||||||
|  |  | ||||||
| const adminKey = Object.freeze({ |  | ||||||
|   id: 1, |  | ||||||
|   name: 'My Key', |  | ||||||
|   key: 'my-api-key (hashed)', |  | ||||||
|   userId: authStub.admin.id, |  | ||||||
|   user: userEntityStub.admin, |  | ||||||
| } as APIKeyEntity); |  | ||||||
|  |  | ||||||
| const token = Buffer.from('my-api-key', 'utf8').toString('base64'); |  | ||||||
|  |  | ||||||
| describe(APIKeyService.name, () => { | describe(APIKeyService.name, () => { | ||||||
|   let sut: APIKeyService; |   let sut: APIKeyService; | ||||||
|   let keyMock: jest.Mocked<IKeyRepository>; |   let keyMock: jest.Mocked<IKeyRepository>; | ||||||
| @@ -28,10 +17,8 @@ describe(APIKeyService.name, () => { | |||||||
|  |  | ||||||
|   describe('create', () => { |   describe('create', () => { | ||||||
|     it('should create a new key', async () => { |     it('should create a new key', async () => { | ||||||
|       keyMock.create.mockResolvedValue(adminKey); |       keyMock.create.mockResolvedValue(keyStub.admin); | ||||||
|  |  | ||||||
|       await sut.create(authStub.admin, { name: 'Test Key' }); |       await sut.create(authStub.admin, { name: 'Test Key' }); | ||||||
|  |  | ||||||
|       expect(keyMock.create).toHaveBeenCalledWith({ |       expect(keyMock.create).toHaveBeenCalledWith({ | ||||||
|         key: 'cmFuZG9tLWJ5dGVz (hashed)', |         key: 'cmFuZG9tLWJ5dGVz (hashed)', | ||||||
|         name: 'Test Key', |         name: 'Test Key', | ||||||
| @@ -42,7 +29,7 @@ describe(APIKeyService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should not require a name', async () => { |     it('should not require a name', async () => { | ||||||
|       keyMock.create.mockResolvedValue(adminKey); |       keyMock.create.mockResolvedValue(keyStub.admin); | ||||||
|  |  | ||||||
|       await sut.create(authStub.admin, {}); |       await sut.create(authStub.admin, {}); | ||||||
|  |  | ||||||
| @@ -66,7 +53,7 @@ describe(APIKeyService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should update a key', async () => { |     it('should update a key', async () => { | ||||||
|       keyMock.getById.mockResolvedValue(adminKey); |       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||||
|  |  | ||||||
|       await sut.update(authStub.admin, 1, { name: 'New Name' }); |       await sut.update(authStub.admin, 1, { name: 'New Name' }); | ||||||
|  |  | ||||||
| @@ -84,7 +71,7 @@ describe(APIKeyService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should delete a key', async () => { |     it('should delete a key', async () => { | ||||||
|       keyMock.getById.mockResolvedValue(adminKey); |       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||||
|  |  | ||||||
|       await sut.delete(authStub.admin, 1); |       await sut.delete(authStub.admin, 1); | ||||||
|  |  | ||||||
| @@ -102,7 +89,7 @@ describe(APIKeyService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should get a key by id', async () => { |     it('should get a key by id', async () => { | ||||||
|       keyMock.getById.mockResolvedValue(adminKey); |       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||||
|  |  | ||||||
|       await sut.getById(authStub.admin, 1); |       await sut.getById(authStub.admin, 1); | ||||||
|  |  | ||||||
| @@ -112,29 +99,11 @@ describe(APIKeyService.name, () => { | |||||||
|  |  | ||||||
|   describe('getAll', () => { |   describe('getAll', () => { | ||||||
|     it('should return all the keys for a user', async () => { |     it('should return all the keys for a user', async () => { | ||||||
|       keyMock.getByUserId.mockResolvedValue([adminKey]); |       keyMock.getByUserId.mockResolvedValue([keyStub.admin]); | ||||||
|  |  | ||||||
|       await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1); |       await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1); | ||||||
|  |  | ||||||
|       expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id); |       expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('validate', () => { |  | ||||||
|     it('should throw an error for an invalid id', async () => { |  | ||||||
|       keyMock.getKey.mockResolvedValue(null); |  | ||||||
|  |  | ||||||
|       await expect(sut.validate(token)).resolves.toBeNull(); |  | ||||||
|  |  | ||||||
|       expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)'); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should validate the token', async () => { |  | ||||||
|       keyMock.getKey.mockResolvedValue(adminKey); |  | ||||||
|  |  | ||||||
|       await expect(sut.validate(token)).resolves.toEqual(authStub.admin); |  | ||||||
|  |  | ||||||
|       expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)'); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { BadRequestException, Inject, Injectable } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable } from '@nestjs/common'; | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { IKeyRepository } from './api-key.repository'; | import { IKeyRepository } from './api-key.repository'; | ||||||
| import { APIKeyCreateDto } from './dto/api-key-create.dto'; | import { APIKeyCreateDto } from './dto/api-key-create.dto'; | ||||||
| import { APIKeyCreateResponseDto } from './response-dto/api-key-create-response.dto'; | import { APIKeyCreateResponseDto } from './response-dto/api-key-create-response.dto'; | ||||||
| @@ -55,22 +56,4 @@ export class APIKeyService { | |||||||
|     const keys = await this.repository.getByUserId(authUser.id); |     const keys = await this.repository.getByUserId(authUser.id); | ||||||
|     return keys.map(mapKey); |     return keys.map(mapKey); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   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, |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| import { SystemConfig, UserEntity } from '@app/infra/db/entities'; | import { SystemConfig, UserEntity } from '@app/infra/db/entities'; | ||||||
| import { IncomingHttpHeaders } from 'http'; |  | ||||||
| import { ISystemConfigRepository } from '../system-config'; | import { ISystemConfigRepository } from '../system-config'; | ||||||
| import { SystemConfigCore } from '../system-config/system-config.core'; | import { SystemConfigCore } from '../system-config/system-config.core'; | ||||||
| import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant'; | import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant'; | ||||||
| import { ICryptoRepository } from './crypto.repository'; | import { ICryptoRepository } from '../crypto/crypto.repository'; | ||||||
| import { LoginResponseDto, mapLoginResponse } from './response-dto'; | import { LoginResponseDto, mapLoginResponse } from './response-dto'; | ||||||
| import { IUserTokenRepository, UserTokenCore } from '@app/domain'; | import { IUserTokenRepository, UserTokenCore } from '../user-token'; | ||||||
| import cookieParser from 'cookie'; |  | ||||||
|  |  | ||||||
| export type JwtValidationResult = { | export type JwtValidationResult = { | ||||||
|   status: boolean; |   status: boolean; | ||||||
| @@ -59,21 +57,4 @@ export class AuthCore { | |||||||
|     } |     } | ||||||
|     return this.cryptoRepository.compareBcrypt(inputPassword, user.password); |     return this.cryptoRepository.compareBcrypt(inputPassword, user.password); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   extractTokenFromHeader(headers: IncomingHttpHeaders) { |  | ||||||
|     if (!headers.authorization) { |  | ||||||
|       return this.extractTokenFromCookie(cookieParser.parse(headers.cookie || '')); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const [type, accessToken] = headers.authorization.split(' '); |  | ||||||
|     if (type.toLowerCase() !== 'bearer') { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return accessToken; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   extractTokenFromCookie(cookies: Record<string, string>) { |  | ||||||
|     return cookies?.[IMMICH_ACCESS_COOKIE] || null; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,25 +1,34 @@ | |||||||
| import { SystemConfig, UserEntity } from '@app/infra/db/entities'; | import { SystemConfig, UserEntity } from '@app/infra/db/entities'; | ||||||
| import { BadRequestException, UnauthorizedException } from '@nestjs/common'; | import { BadRequestException, UnauthorizedException } from '@nestjs/common'; | ||||||
|  | import { IncomingHttpHeaders } from 'http'; | ||||||
| import { generators, Issuer } from 'openid-client'; | import { generators, Issuer } from 'openid-client'; | ||||||
| import { Socket } from 'socket.io'; | import { Socket } from 'socket.io'; | ||||||
| import { | import { | ||||||
|   userEntityStub, |   authStub, | ||||||
|  |   keyStub, | ||||||
|   loginResponseStub, |   loginResponseStub, | ||||||
|   newCryptoRepositoryMock, |   newCryptoRepositoryMock, | ||||||
|  |   newKeyRepositoryMock, | ||||||
|  |   newSharedLinkRepositoryMock, | ||||||
|   newSystemConfigRepositoryMock, |   newSystemConfigRepositoryMock, | ||||||
|   newUserRepositoryMock, |   newUserRepositoryMock, | ||||||
|  |   newUserTokenRepositoryMock, | ||||||
|  |   sharedLinkStub, | ||||||
|   systemConfigStub, |   systemConfigStub, | ||||||
|  |   userEntityStub, | ||||||
|   userTokenEntityStub, |   userTokenEntityStub, | ||||||
| } from '../../test'; | } from '../../test'; | ||||||
|  | import { IKeyRepository } from '../api-key'; | ||||||
|  | import { ICryptoRepository } from '../crypto/crypto.repository'; | ||||||
|  | import { ISharedLinkRepository } from '../share'; | ||||||
| import { ISystemConfigRepository } from '../system-config'; | import { ISystemConfigRepository } from '../system-config'; | ||||||
| import { IUserRepository } from '../user'; | import { IUserRepository } from '../user'; | ||||||
| import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant'; | import { IUserTokenRepository } from '../user-token'; | ||||||
|  | import { AuthType } from './auth.constant'; | ||||||
| import { AuthService } from './auth.service'; | import { AuthService } from './auth.service'; | ||||||
| import { ICryptoRepository } from './crypto.repository'; |  | ||||||
| import { SignUpDto } from './dto'; | import { SignUpDto } from './dto'; | ||||||
| import { IUserTokenRepository } from '@app/domain'; |  | ||||||
| import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock'; | // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); | ||||||
| import { IncomingHttpHeaders } from 'http'; |  | ||||||
|  |  | ||||||
| const email = 'test@immich.com'; | const email = 'test@immich.com'; | ||||||
| const sub = 'my-auth-user-sub'; | const sub = 'my-auth-user-sub'; | ||||||
| @@ -51,6 +60,8 @@ describe('AuthService', () => { | |||||||
|   let userMock: jest.Mocked<IUserRepository>; |   let userMock: jest.Mocked<IUserRepository>; | ||||||
|   let configMock: jest.Mocked<ISystemConfigRepository>; |   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||||
|   let userTokenMock: jest.Mocked<IUserTokenRepository>; |   let userTokenMock: jest.Mocked<IUserTokenRepository>; | ||||||
|  |   let shareMock: jest.Mocked<ISharedLinkRepository>; | ||||||
|  |   let keyMock: jest.Mocked<IKeyRepository>; | ||||||
|   let callbackMock: jest.Mock; |   let callbackMock: jest.Mock; | ||||||
|   let create: (config: SystemConfig) => AuthService; |   let create: (config: SystemConfig) => AuthService; | ||||||
|  |  | ||||||
| @@ -81,8 +92,10 @@ describe('AuthService', () => { | |||||||
|     userMock = newUserRepositoryMock(); |     userMock = newUserRepositoryMock(); | ||||||
|     configMock = newSystemConfigRepositoryMock(); |     configMock = newSystemConfigRepositoryMock(); | ||||||
|     userTokenMock = newUserTokenRepositoryMock(); |     userTokenMock = newUserTokenRepositoryMock(); | ||||||
|  |     shareMock = newSharedLinkRepositoryMock(); | ||||||
|  |     keyMock = newKeyRepositoryMock(); | ||||||
|  |  | ||||||
|     create = (config) => new AuthService(cryptoMock, configMock, userMock, userTokenMock, config); |     create = (config) => new AuthService(cryptoMock, configMock, userMock, userTokenMock, shareMock, keyMock, config); | ||||||
|  |  | ||||||
|     sut = create(systemConfigStub.enabled); |     sut = create(systemConfigStub.enabled); | ||||||
|   }); |   }); | ||||||
| @@ -218,63 +231,73 @@ describe('AuthService', () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('validate - socket connections', () => { |   describe('validate - socket connections', () => { | ||||||
|  |     it('should throw token is not provided', async () => { | ||||||
|  |       await expect(sut.validate({}, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     it('should validate using authorization header', async () => { |     it('should validate using authorization header', async () => { | ||||||
|       userMock.get.mockResolvedValue(userEntityStub.user1); |       userMock.get.mockResolvedValue(userEntityStub.user1); | ||||||
|       userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken); |       userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken); | ||||||
|       const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; |       const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; | ||||||
|       await expect(sut.validate((client as Socket).request.headers)).resolves.toEqual(userEntityStub.user1); |       await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual(userEntityStub.user1); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('validate - api request', () => { |   describe('validate - shared key', () => { | ||||||
|     it('should throw if no user is found', async () => { |     it('should not accept a non-existent key', async () => { | ||||||
|  |       shareMock.getByKey.mockResolvedValue(null); | ||||||
|  |       const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; | ||||||
|  |       await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should not accept an expired key', async () => { | ||||||
|  |       shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); | ||||||
|  |       const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; | ||||||
|  |       await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should not accept a key without a user', async () => { | ||||||
|  |       shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); | ||||||
|       userMock.get.mockResolvedValue(null); |       userMock.get.mockResolvedValue(null); | ||||||
|       await expect(sut.validate({ email: 'a', userId: 'test' })).resolves.toBeNull(); |       const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; | ||||||
|  |       await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should accept a valid key', async () => { | ||||||
|  |       shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); | ||||||
|  |       userMock.get.mockResolvedValue(userEntityStub.admin); | ||||||
|  |       const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; | ||||||
|  |       await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('validate - user token', () => { | ||||||
|  |     it('should throw if no token is found', async () => { | ||||||
|  |       userTokenMock.get.mockResolvedValue(null); | ||||||
|  |       const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' }; | ||||||
|  |       await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should return an auth dto', async () => { |     it('should return an auth dto', async () => { | ||||||
|       userMock.get.mockResolvedValue(userEntityStub.user1); |  | ||||||
|       userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken); |       userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken); | ||||||
|       await expect( |       const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; | ||||||
|         sut.validate({ cookie: 'immich_access_token=auth_token', email: 'a', userId: 'test' }), |       await expect(sut.validate(headers, {})).resolves.toEqual(userEntityStub.user1); | ||||||
|       ).resolves.toEqual(userEntityStub.user1); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('extractTokenFromHeader - Cookie', () => { |   describe('validate - api key', () => { | ||||||
|     it('should extract the access token', () => { |     it('should throw an error if no api key is found', async () => { | ||||||
|       const cookie: IncomingHttpHeaders = { |       keyMock.getKey.mockResolvedValue(null); | ||||||
|         cookie: `${IMMICH_ACCESS_COOKIE}=signed-jwt;${IMMICH_AUTH_TYPE_COOKIE}=password`, |       const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; | ||||||
|       }; |       await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); | ||||||
|       expect(sut.extractTokenFromHeader(cookie)).toEqual('signed-jwt'); |       expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should work with no cookies', () => { |     it('should return an auth dto', async () => { | ||||||
|       const cookie: IncomingHttpHeaders = { |       keyMock.getKey.mockResolvedValue(keyStub.admin); | ||||||
|         cookie: undefined, |       const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; | ||||||
|       }; |       await expect(sut.validate(headers, {})).resolves.toEqual(authStub.admin); | ||||||
|       expect(sut.extractTokenFromHeader(cookie)).toBeNull(); |       expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should work on empty cookies', () => { |  | ||||||
|       const cookie: IncomingHttpHeaders = { |  | ||||||
|         cookie: '', |  | ||||||
|       }; |  | ||||||
|       expect(sut.extractTokenFromHeader(cookie)).toBeNull(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   describe('extractTokenFromHeader - Bearer Auth', () => { |  | ||||||
|     it('should extract the access token', () => { |  | ||||||
|       expect(sut.extractTokenFromHeader({ authorization: `Bearer signed-jwt` })).toEqual('signed-jwt'); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should work without the auth header', () => { |  | ||||||
|       expect(sut.extractTokenFromHeader({})).toBeNull(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should ignore basic auth', () => { |  | ||||||
|       expect(sut.extractTokenFromHeader({ authorization: `Basic stuff` })).toBeNull(); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -11,12 +11,16 @@ import { IncomingHttpHeaders } from 'http'; | |||||||
| import { OAuthCore } from '../oauth/oauth.core'; | import { OAuthCore } from '../oauth/oauth.core'; | ||||||
| import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config'; | import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config'; | ||||||
| import { IUserRepository, UserCore } from '../user'; | import { IUserRepository, UserCore } from '../user'; | ||||||
| import { AuthType } from './auth.constant'; | import { AuthType, IMMICH_ACCESS_COOKIE } from './auth.constant'; | ||||||
| import { AuthCore } from './auth.core'; | import { AuthCore } from './auth.core'; | ||||||
| import { ICryptoRepository } from './crypto.repository'; | import { ICryptoRepository } from '../crypto/crypto.repository'; | ||||||
| import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto'; | import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto'; | ||||||
| import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto'; | import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto'; | ||||||
| import { IUserTokenRepository, UserTokenCore } from '@app/domain/user-token'; | import { IUserTokenRepository, UserTokenCore } from '../user-token'; | ||||||
|  | import cookieParser from 'cookie'; | ||||||
|  | import { ISharedLinkRepository, ShareCore } from '../share'; | ||||||
|  | import { APIKeyCore } from '../api-key/api-key.core'; | ||||||
|  | import { IKeyRepository } from '../api-key'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class AuthService { | export class AuthService { | ||||||
| @@ -24,14 +28,18 @@ export class AuthService { | |||||||
|   private authCore: AuthCore; |   private authCore: AuthCore; | ||||||
|   private oauthCore: OAuthCore; |   private oauthCore: OAuthCore; | ||||||
|   private userCore: UserCore; |   private userCore: UserCore; | ||||||
|  |   private shareCore: ShareCore; | ||||||
|  |   private keyCore: APIKeyCore; | ||||||
|  |  | ||||||
|   private logger = new Logger(AuthService.name); |   private logger = new Logger(AuthService.name); | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, |     @Inject(ICryptoRepository) 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) shareRepository: ISharedLinkRepository, | ||||||
|  |     @Inject(IKeyRepository) keyRepository: IKeyRepository, | ||||||
|     @Inject(INITIAL_SYSTEM_CONFIG) |     @Inject(INITIAL_SYSTEM_CONFIG) | ||||||
|     initialConfig: SystemConfig, |     initialConfig: SystemConfig, | ||||||
|   ) { |   ) { | ||||||
| @@ -39,6 +47,8 @@ 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.shareCore = new ShareCore(shareRepository, cryptoRepository); | ||||||
|  |     this.keyCore = new APIKeyCore(cryptoRepository, keyRepository); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async login( |   public async login( | ||||||
| @@ -115,28 +125,40 @@ export class AuthService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async validate(headers: IncomingHttpHeaders): Promise<AuthUserDto | null> { |   public async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto | null> { | ||||||
|     const tokenValue = this.extractTokenFromHeader(headers); |     const shareKey = (headers['x-immich-share-key'] || params.key) as string; | ||||||
|     if (!tokenValue) { |     const userToken = (headers['x-immich-user-token'] || | ||||||
|       return null; |       params.userToken || | ||||||
|  |       this.getBearerToken(headers) || | ||||||
|  |       this.getCookieToken(headers)) as string; | ||||||
|  |     const apiKey = (headers['x-api-key'] || params.apiKey) as string; | ||||||
|  |  | ||||||
|  |     if (shareKey) { | ||||||
|  |       return this.shareCore.validate(shareKey); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const hashedToken = this.cryptoRepository.hashSha256(tokenValue); |     if (userToken) { | ||||||
|     const user = await this.userTokenCore.getUserByToken(hashedToken); |       return this.userTokenCore.validate(userToken); | ||||||
|     if (user) { |     } | ||||||
|       return { |  | ||||||
|         ...user, |     if (apiKey) { | ||||||
|         isPublicUser: false, |       return this.keyCore.validate(apiKey); | ||||||
|         isAllowUpload: true, |     } | ||||||
|         isAllowDownload: true, |  | ||||||
|         isShowExif: true, |     throw new UnauthorizedException('Authentication required'); | ||||||
|       }; |   } | ||||||
|  |  | ||||||
|  |   private getBearerToken(headers: IncomingHttpHeaders): string | null { | ||||||
|  |     const [type, token] = (headers.authorization || '').split(' '); | ||||||
|  |     if (type.toLowerCase() === 'bearer') { | ||||||
|  |       return token; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   extractTokenFromHeader(headers: IncomingHttpHeaders) { |   private getCookieToken(headers: IncomingHttpHeaders): string | null { | ||||||
|     return this.authCore.extractTokenFromHeader(headers); |     const cookies = cookieParser.parse(headers.cookie || ''); | ||||||
|  |     return cookies[IMMICH_ACCESS_COOKIE] || null; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| export * from './auth.constant'; | export * from './auth.constant'; | ||||||
| export * from './auth.service'; | export * from './auth.service'; | ||||||
| export * from './crypto.repository'; |  | ||||||
| export * from './dto'; | export * from './dto'; | ||||||
| export * from './response-dto'; | export * from './response-dto'; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								server/libs/domain/src/crypto/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/libs/domain/src/crypto/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './crypto.repository'; | ||||||
| @@ -2,6 +2,7 @@ export * from './album'; | |||||||
| export * from './api-key'; | export * from './api-key'; | ||||||
| export * from './asset'; | export * from './asset'; | ||||||
| export * from './auth'; | export * from './auth'; | ||||||
|  | export * from './crypto'; | ||||||
| export * from './domain.module'; | export * from './domain.module'; | ||||||
| export * from './job'; | export * from './job'; | ||||||
| export * from './oauth'; | export * from './oauth'; | ||||||
|   | |||||||
| @@ -11,11 +11,11 @@ import { | |||||||
|   systemConfigStub, |   systemConfigStub, | ||||||
|   userTokenEntityStub, |   userTokenEntityStub, | ||||||
| } from '../../test'; | } from '../../test'; | ||||||
| import { ICryptoRepository } from '../auth'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { OAuthService } from '../oauth'; | import { OAuthService } from '../oauth'; | ||||||
| import { ISystemConfigRepository } from '../system-config'; | import { ISystemConfigRepository } from '../system-config'; | ||||||
| import { IUserRepository } from '../user'; | import { IUserRepository } from '../user'; | ||||||
| import { IUserTokenRepository } from '@app/domain'; | import { IUserTokenRepository } from '../user-token'; | ||||||
| import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock'; | import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock'; | ||||||
|  |  | ||||||
| const email = 'user@immich.com'; | const email = 'user@immich.com'; | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| import { SystemConfig } from '@app/infra/db/entities'; | import { SystemConfig } from '@app/infra/db/entities'; | ||||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { AuthType, AuthUserDto, ICryptoRepository, LoginResponseDto } from '../auth'; | import { AuthType, AuthUserDto, LoginResponseDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { AuthCore } from '../auth/auth.core'; | import { AuthCore } from '../auth/auth.core'; | ||||||
| import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config'; | import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config'; | ||||||
| import { IUserRepository, UserCore, UserResponseDto } from '../user'; | import { IUserRepository, UserCore, UserResponseDto } from '../user'; | ||||||
| import { OAuthCallbackDto, OAuthConfigDto } from './dto'; | import { OAuthCallbackDto, OAuthConfigDto } from './dto'; | ||||||
| import { OAuthCore } from './oauth.core'; | import { OAuthCore } from './oauth.core'; | ||||||
| import { OAuthConfigResponseDto } from './response-dto'; | import { OAuthConfigResponseDto } from './response-dto'; | ||||||
| import { IUserTokenRepository } from '@app/domain/user-token'; | import { IUserTokenRepository } from '../user-token'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class OAuthService { | export class OAuthService { | ||||||
|   | |||||||
| @@ -1,6 +1,13 @@ | |||||||
| import { AssetEntity, SharedLinkEntity } from '@app/infra/db/entities'; | import { AssetEntity, SharedLinkEntity } from '@app/infra/db/entities'; | ||||||
| import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common'; | import { | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; |   BadRequestException, | ||||||
|  |   ForbiddenException, | ||||||
|  |   InternalServerErrorException, | ||||||
|  |   Logger, | ||||||
|  |   UnauthorizedException, | ||||||
|  | } from '@nestjs/common'; | ||||||
|  | import { AuthUserDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { CreateSharedLinkDto } from './dto'; | import { CreateSharedLinkDto } from './dto'; | ||||||
| import { ISharedLinkRepository } from './shared-link.repository'; | import { ISharedLinkRepository } from './shared-link.repository'; | ||||||
|  |  | ||||||
| @@ -17,10 +24,6 @@ export class ShareCore { | |||||||
|     return this.repository.get(userId, id); |     return this.repository.get(userId, id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getByKey(key: string): Promise<SharedLinkEntity | null> { |  | ||||||
|     return this.repository.getByKey(key); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> { |   create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> { | ||||||
|     try { |     try { | ||||||
|       return this.repository.create({ |       return this.repository.create({ | ||||||
| @@ -78,4 +81,26 @@ export class ShareCore { | |||||||
|       throw new ForbiddenException(); |       throw new ForbiddenException(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async validate(key: string): Promise<AuthUserDto | null> { | ||||||
|  |     const link = await this.repository.getByKey(key); | ||||||
|  |     if (link) { | ||||||
|  |       if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { | ||||||
|  |         const user = link.user; | ||||||
|  |         if (user) { | ||||||
|  |           return { | ||||||
|  |             id: user.id, | ||||||
|  |             email: user.email, | ||||||
|  |             isAdmin: user.isAdmin, | ||||||
|  |             isPublicUser: true, | ||||||
|  |             sharedLinkId: link.id, | ||||||
|  |             isAllowUpload: link.allowUpload, | ||||||
|  |             isAllowDownload: link.allowDownload, | ||||||
|  |             isShowExif: link.showExif, | ||||||
|  |           }; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     throw new UnauthorizedException('Invalid share key'); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import { BadRequestException, ForbiddenException } from '@nestjs/common'; | import { BadRequestException, ForbiddenException } from '@nestjs/common'; | ||||||
| import { | import { | ||||||
|   authStub, |   authStub, | ||||||
|   userEntityStub, |  | ||||||
|   newCryptoRepositoryMock, |   newCryptoRepositoryMock, | ||||||
|   newSharedLinkRepositoryMock, |   newSharedLinkRepositoryMock, | ||||||
|   newUserRepositoryMock, |  | ||||||
|   sharedLinkResponseStub, |   sharedLinkResponseStub, | ||||||
|   sharedLinkStub, |   sharedLinkStub, | ||||||
| } from '../../test'; | } from '../../test'; | ||||||
| import { ICryptoRepository } from '../auth'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { IUserRepository } from '../user'; |  | ||||||
| import { ShareService } from './share.service'; | import { ShareService } from './share.service'; | ||||||
| import { ISharedLinkRepository } from './shared-link.repository'; | import { ISharedLinkRepository } from './shared-link.repository'; | ||||||
|  |  | ||||||
| @@ -17,44 +14,18 @@ describe(ShareService.name, () => { | |||||||
|   let sut: ShareService; |   let sut: ShareService; | ||||||
|   let cryptoMock: jest.Mocked<ICryptoRepository>; |   let cryptoMock: jest.Mocked<ICryptoRepository>; | ||||||
|   let shareMock: jest.Mocked<ISharedLinkRepository>; |   let shareMock: jest.Mocked<ISharedLinkRepository>; | ||||||
|   let userMock: jest.Mocked<IUserRepository>; |  | ||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     cryptoMock = newCryptoRepositoryMock(); |     cryptoMock = newCryptoRepositoryMock(); | ||||||
|     shareMock = newSharedLinkRepositoryMock(); |     shareMock = newSharedLinkRepositoryMock(); | ||||||
|     userMock = newUserRepositoryMock(); |  | ||||||
|  |  | ||||||
|     sut = new ShareService(cryptoMock, shareMock, userMock); |     sut = new ShareService(cryptoMock, shareMock); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   it('should work', () => { |   it('should work', () => { | ||||||
|     expect(sut).toBeDefined(); |     expect(sut).toBeDefined(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('validate', () => { |  | ||||||
|     it('should not accept a non-existant key', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(null); |  | ||||||
|       await expect(sut.validate('key')).resolves.toBeNull(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should not accept an expired key', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); |  | ||||||
|       await expect(sut.validate('key')).resolves.toBeNull(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should not accept a key without a user', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); |  | ||||||
|       userMock.get.mockResolvedValue(null); |  | ||||||
|       await expect(sut.validate('key')).resolves.toBeNull(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should accept a valid key', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); |  | ||||||
|       userMock.get.mockResolvedValue(userEntityStub.admin); |  | ||||||
|       await expect(sut.validate('key')).resolves.toEqual(authStub.adminSharedLink); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   describe('getAll', () => { |   describe('getAll', () => { | ||||||
|     it('should return all keys for a user', async () => { |     it('should return all keys for a user', async () => { | ||||||
|       shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]); |       shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]); | ||||||
| @@ -131,20 +102,6 @@ describe(ShareService.name, () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('getByKey', () => { |  | ||||||
|     it('should not work on a missing key', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(null); |  | ||||||
|       await expect(sut.getByKey('secret-key')).rejects.toBeInstanceOf(BadRequestException); |  | ||||||
|       expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key'); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should find a key', async () => { |  | ||||||
|       shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); |  | ||||||
|       await expect(sut.getByKey('secret-key')).resolves.toEqual(sharedLinkResponseStub.valid); |  | ||||||
|       expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key'); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   describe('edit', () => { |   describe('edit', () => { | ||||||
|     it('should not work on a missing key', async () => { |     it('should not work on a missing key', async () => { | ||||||
|       shareMock.get.mockResolvedValue(null); |       shareMock.get.mockResolvedValue(null); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { BadRequestException, ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common'; | import { BadRequestException, ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
| import { IUserRepository, UserCore } from '../user'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { EditSharedLinkDto } from './dto'; | import { EditSharedLinkDto } from './dto'; | ||||||
| import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto'; | import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto'; | ||||||
| import { ShareCore } from './share.core'; | import { ShareCore } from './share.core'; | ||||||
| @@ -10,37 +10,12 @@ import { ISharedLinkRepository } from './shared-link.repository'; | |||||||
| export class ShareService { | export class ShareService { | ||||||
|   readonly logger = new Logger(ShareService.name); |   readonly logger = new Logger(ShareService.name); | ||||||
|   private shareCore: ShareCore; |   private shareCore: ShareCore; | ||||||
|   private userCore: UserCore; |  | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, |     @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, | ||||||
|     @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, |     @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, | ||||||
|     @Inject(IUserRepository) userRepository: IUserRepository, |  | ||||||
|   ) { |   ) { | ||||||
|     this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository); |     this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository); | ||||||
|     this.userCore = new UserCore(userRepository, cryptoRepository); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async validate(key: string): Promise<AuthUserDto | null> { |  | ||||||
|     const link = await this.shareCore.getByKey(key); |  | ||||||
|     if (link) { |  | ||||||
|       if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { |  | ||||||
|         const user = await this.userCore.get(link.userId); |  | ||||||
|         if (user) { |  | ||||||
|           return { |  | ||||||
|             id: user.id, |  | ||||||
|             email: user.email, |  | ||||||
|             isAdmin: user.isAdmin, |  | ||||||
|             isPublicUser: true, |  | ||||||
|             sharedLinkId: link.id, |  | ||||||
|             isAllowUpload: link.allowUpload, |  | ||||||
|             isAllowDownload: link.allowDownload, |  | ||||||
|             isShowExif: link.showExif, |  | ||||||
|           }; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { |   async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { | ||||||
| @@ -74,14 +49,6 @@ export class ShareService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getByKey(key: string): Promise<SharedLinkResponseDto> { |  | ||||||
|     const link = await this.shareCore.getByKey(key); |  | ||||||
|     if (!link) { |  | ||||||
|       throw new BadRequestException('Shared link not found'); |  | ||||||
|     } |  | ||||||
|     return mapSharedLink(link); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async remove(authUser: AuthUserDto, id: string): Promise<void> { |   async remove(authUser: AuthUserDto, id: string): Promise<void> { | ||||||
|     await this.shareCore.remove(authUser.id, id); |     await this.shareCore.remove(authUser.id, id); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ export interface ISharedLinkRepository { | |||||||
|   getAll(userId: string): Promise<SharedLinkEntity[]>; |   getAll(userId: string): Promise<SharedLinkEntity[]>; | ||||||
|   get(userId: string, id: string): Promise<SharedLinkEntity | null>; |   get(userId: string, id: string): Promise<SharedLinkEntity | null>; | ||||||
|   getByKey(key: string): Promise<SharedLinkEntity | null>; |   getByKey(key: string): Promise<SharedLinkEntity | null>; | ||||||
|   create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity>; |   create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>; | ||||||
|   remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>; |   remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>; | ||||||
|   save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>; |   save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>; | ||||||
|   hasAssetAccess(id: string, assetId: string): Promise<boolean>; |   hasAssetAccess(id: string, assetId: string): Promise<boolean>; | ||||||
|   | |||||||
| @@ -1,12 +1,28 @@ | |||||||
| import { UserEntity } from '@app/infra/db/entities'; | import { UserEntity } from '@app/infra/db/entities'; | ||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable, UnauthorizedException } from '@nestjs/common'; | ||||||
| import { ICryptoRepository } from '../auth'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { IUserTokenRepository } from './user-token.repository'; | import { IUserTokenRepository } from './user-token.repository'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class UserTokenCore { | export class UserTokenCore { | ||||||
|   constructor(private crypto: ICryptoRepository, private repository: IUserTokenRepository) {} |   constructor(private crypto: ICryptoRepository, private repository: IUserTokenRepository) {} | ||||||
|  |  | ||||||
|  |   async validate(tokenValue: string) { | ||||||
|  |     const hashedToken = this.crypto.hashSha256(tokenValue); | ||||||
|  |     const user = await this.getUserByToken(hashedToken); | ||||||
|  |     if (user) { | ||||||
|  |       return { | ||||||
|  |         ...user, | ||||||
|  |         isPublicUser: false, | ||||||
|  |         isAllowUpload: true, | ||||||
|  |         isAllowDownload: true, | ||||||
|  |         isShowExif: true, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     throw new UnauthorizedException('Invalid user token'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public async getUserByToken(tokenValue: string): Promise<UserEntity | null> { |   public async getUserByToken(tokenValue: string): Promise<UserEntity | null> { | ||||||
|     const token = await this.repository.get(tokenValue); |     const token = await this.repository.get(tokenValue); | ||||||
|     if (token?.user) { |     if (token?.user) { | ||||||
|   | |||||||
| @@ -10,7 +10,8 @@ import { | |||||||
| import { hash } from 'bcrypt'; | import { hash } from 'bcrypt'; | ||||||
| import { constants, createReadStream, ReadStream } from 'fs'; | import { constants, createReadStream, ReadStream } from 'fs'; | ||||||
| import fs from 'fs/promises'; | import fs from 'fs/promises'; | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto'; | import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto'; | ||||||
| import { IUserRepository, UserListFilter } from './user.repository'; | import { IUserRepository, UserListFilter } from './user.repository'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,8 @@ import { UserEntity } from '@app/infra/db/entities'; | |||||||
| import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; | import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; | ||||||
| import { when } from 'jest-when'; | import { when } from 'jest-when'; | ||||||
| import { newCryptoRepositoryMock, newUserRepositoryMock } from '../../test'; | import { newCryptoRepositoryMock, newUserRepositoryMock } from '../../test'; | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { UpdateUserDto } from './dto/update-user.dto'; | import { UpdateUserDto } from './dto/update-user.dto'; | ||||||
| import { UserService } from './user.service'; | import { UserService } from './user.service'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | ||||||
| import { randomBytes } from 'crypto'; | import { randomBytes } from 'crypto'; | ||||||
| import { ReadStream } from 'fs'; | import { ReadStream } from 'fs'; | ||||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
|  | import { ICryptoRepository } from '../crypto'; | ||||||
| import { IUserRepository } from '../user'; | import { IUserRepository } from '../user'; | ||||||
| import { CreateUserDto } from './dto/create-user.dto'; | import { CreateUserDto } from './dto/create-user.dto'; | ||||||
| import { UpdateUserDto } from './dto/update-user.dto'; | import { UpdateUserDto } from './dto/update-user.dto'; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { | import { | ||||||
|  |   APIKeyEntity, | ||||||
|   AssetType, |   AssetType, | ||||||
|   SharedLinkEntity, |   SharedLinkEntity, | ||||||
|   SharedLinkType, |   SharedLinkType, | ||||||
| @@ -148,6 +149,16 @@ export const userTokenEntityStub = { | |||||||
|   }), |   }), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const keyStub = { | ||||||
|  |   admin: Object.freeze({ | ||||||
|  |     id: 1, | ||||||
|  |     name: 'My Key', | ||||||
|  |     key: 'my-api-key (hashed)', | ||||||
|  |     userId: authStub.admin.id, | ||||||
|  |     user: userEntityStub.admin, | ||||||
|  |   } as APIKeyEntity), | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const systemConfigStub = { | export const systemConfigStub = { | ||||||
|   defaults: Object.freeze({ |   defaults: Object.freeze({ | ||||||
|     ffmpeg: { |     ffmpeg: { | ||||||
| @@ -275,6 +286,7 @@ export const sharedLinkStub = { | |||||||
|   valid: Object.freeze({ |   valid: Object.freeze({ | ||||||
|     id: '123', |     id: '123', | ||||||
|     userId: authStub.admin.id, |     userId: authStub.admin.id, | ||||||
|  |     user: userEntityStub.admin, | ||||||
|     key: Buffer.from('secret-key', 'utf8'), |     key: Buffer.from('secret-key', 'utf8'), | ||||||
|     type: SharedLinkType.ALBUM, |     type: SharedLinkType.ALBUM, | ||||||
|     createdAt: today.toISOString(), |     createdAt: today.toISOString(), | ||||||
| @@ -288,6 +300,7 @@ export const sharedLinkStub = { | |||||||
|   expired: Object.freeze({ |   expired: Object.freeze({ | ||||||
|     id: '123', |     id: '123', | ||||||
|     userId: authStub.admin.id, |     userId: authStub.admin.id, | ||||||
|  |     user: userEntityStub.admin, | ||||||
|     key: Buffer.from('secret-key', 'utf8'), |     key: Buffer.from('secret-key', 'utf8'), | ||||||
|     type: SharedLinkType.ALBUM, |     type: SharedLinkType.ALBUM, | ||||||
|     createdAt: today.toISOString(), |     createdAt: today.toISOString(), | ||||||
| @@ -300,6 +313,7 @@ export const sharedLinkStub = { | |||||||
|   readonly: Object.freeze<SharedLinkEntity>({ |   readonly: Object.freeze<SharedLinkEntity>({ | ||||||
|     id: '123', |     id: '123', | ||||||
|     userId: authStub.admin.id, |     userId: authStub.admin.id, | ||||||
|  |     user: userEntityStub.admin, | ||||||
|     key: Buffer.from('secret-key', 'utf8'), |     key: Buffer.from('secret-key', 'utf8'), | ||||||
|     type: SharedLinkType.ALBUM, |     type: SharedLinkType.ALBUM, | ||||||
|     createdAt: today.toISOString(), |     createdAt: today.toISOString(), | ||||||
|   | |||||||
| @@ -4,4 +4,5 @@ export * from './fixtures'; | |||||||
| export * from './job.repository.mock'; | export * from './job.repository.mock'; | ||||||
| export * from './shared-link.repository.mock'; | export * from './shared-link.repository.mock'; | ||||||
| export * from './system-config.repository.mock'; | export * from './system-config.repository.mock'; | ||||||
|  | export * from './user-token.repository.mock'; | ||||||
| export * from './user.repository.mock'; | export * from './user.repository.mock'; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; | import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; | ||||||
| import { AlbumEntity } from './album.entity'; | import { AlbumEntity } from './album.entity'; | ||||||
| import { AssetEntity } from './asset.entity'; | import { AssetEntity } from './asset.entity'; | ||||||
|  | import { UserEntity } from './user.entity'; | ||||||
|  |  | ||||||
| @Entity('shared_links') | @Entity('shared_links') | ||||||
| @Unique('UQ_sharedlink_key', ['key']) | @Unique('UQ_sharedlink_key', ['key']) | ||||||
| @@ -14,6 +15,9 @@ export class SharedLinkEntity { | |||||||
|   @Column() |   @Column() | ||||||
|   userId!: string; |   userId!: string; | ||||||
|  |  | ||||||
|  |   @ManyToOne(() => UserEntity) | ||||||
|  |   user!: UserEntity; | ||||||
|  |  | ||||||
|   @Index('IDX_sharedlink_key') |   @Index('IDX_sharedlink_key') | ||||||
|   @Column({ type: 'bytea' }) |   @Column({ type: 'bytea' }) | ||||||
|   key!: Buffer; // use to access the inidividual asset |   key!: Buffer; // use to access the inidividual asset | ||||||
|   | |||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||||
|  |  | ||||||
|  | export class AddSharedLinkUserForeignKeyConstraint1674939383309 implements MigrationInterface { | ||||||
|  |   name = 'AddSharedLinkUserForeignKeyConstraint1674939383309'; | ||||||
|  |  | ||||||
|  |   public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE varchar(36)`); | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE uuid using "userId"::uuid`); | ||||||
|  |     await queryRunner.query( | ||||||
|  |       `ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340"`); | ||||||
|  |     await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "userId" TYPE character varying`); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -73,6 +73,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { | |||||||
|             assetInfo: true, |             assetInfo: true, | ||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|  |         user: true, | ||||||
|       }, |       }, | ||||||
|       order: { |       order: { | ||||||
|         createdAt: 'DESC', |         createdAt: 'DESC', | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										106
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -6,7 +6,7 @@ | |||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "immich", |       "name": "immich", | ||||||
|       "version": "1.42.0", |       "version": "1.43.1", | ||||||
|       "license": "UNLICENSED", |       "license": "UNLICENSED", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@nestjs/bull": "^0.6.2", |         "@nestjs/bull": "^0.6.2", | ||||||
| @@ -14,7 +14,6 @@ | |||||||
|         "@nestjs/config": "^2.2.0", |         "@nestjs/config": "^2.2.0", | ||||||
|         "@nestjs/core": "^9.2.1", |         "@nestjs/core": "^9.2.1", | ||||||
|         "@nestjs/mapped-types": "1.2.0", |         "@nestjs/mapped-types": "1.2.0", | ||||||
|         "@nestjs/passport": "^9.0.0", |  | ||||||
|         "@nestjs/platform-express": "^9.2.1", |         "@nestjs/platform-express": "^9.2.1", | ||||||
|         "@nestjs/platform-socket.io": "^9.2.1", |         "@nestjs/platform-socket.io": "^9.2.1", | ||||||
|         "@nestjs/schedule": "^2.1.0", |         "@nestjs/schedule": "^2.1.0", | ||||||
| @@ -46,9 +45,6 @@ | |||||||
|         "mv": "^2.1.1", |         "mv": "^2.1.1", | ||||||
|         "nest-commander": "^3.3.0", |         "nest-commander": "^3.3.0", | ||||||
|         "openid-client": "^5.2.1", |         "openid-client": "^5.2.1", | ||||||
|         "passport": "^0.6.0", |  | ||||||
|         "passport-custom": "^1.1.1", |  | ||||||
|         "passport-http-header-strategy": "^1.1.0", |  | ||||||
|         "pg": "^8.8.0", |         "pg": "^8.8.0", | ||||||
|         "redis": "^4.5.1", |         "redis": "^4.5.1", | ||||||
|         "reflect-metadata": "^0.1.13", |         "reflect-metadata": "^0.1.13", | ||||||
| @@ -1537,15 +1533,6 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@nestjs/passport": { |  | ||||||
|       "version": "9.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", |  | ||||||
|       "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", |  | ||||||
|       "peerDependencies": { |  | ||||||
|         "@nestjs/common": "^8.0.0 || ^9.0.0", |  | ||||||
|         "passport": "^0.4.0 || ^0.5.0 || ^0.6.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/@nestjs/platform-express": { |     "node_modules/@nestjs/platform-express": { | ||||||
|       "version": "9.2.1", |       "version": "9.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", |       "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", | ||||||
| @@ -8869,50 +8856,6 @@ | |||||||
|         "node": ">= 0.8" |         "node": ">= 0.8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/passport": { |  | ||||||
|       "version": "0.6.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", |  | ||||||
|       "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "passport-strategy": "1.x.x", |  | ||||||
|         "pause": "0.0.1", |  | ||||||
|         "utils-merge": "^1.0.1" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">= 0.4.0" |  | ||||||
|       }, |  | ||||||
|       "funding": { |  | ||||||
|         "type": "github", |  | ||||||
|         "url": "https://github.com/sponsors/jaredhanson" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/passport-custom": { |  | ||||||
|       "version": "1.1.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", |  | ||||||
|       "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "passport-strategy": "1.x.x" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">= 0.10.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/passport-http-header-strategy": { |  | ||||||
|       "version": "1.1.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", |  | ||||||
|       "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "passport-strategy": "^1.0.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/passport-strategy": { |  | ||||||
|       "version": "1.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", |  | ||||||
|       "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=", |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">= 0.4.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/path-exists": { |     "node_modules/path-exists": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", | ||||||
| @@ -8964,11 +8907,6 @@ | |||||||
|         "node": ">=8" |         "node": ">=8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/pause": { |  | ||||||
|       "version": "0.0.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", |  | ||||||
|       "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" |  | ||||||
|     }, |  | ||||||
|     "node_modules/pbf": { |     "node_modules/pbf": { | ||||||
|       "version": "3.2.1", |       "version": "3.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", |       "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", | ||||||
| @@ -12666,12 +12604,6 @@ | |||||||
|       "integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==", |       "integrity": "sha512-NTFwPZkQWsArQH8QSyFWGZvJ08gR+R4TofglqZoihn/vU+ktHEJjMqsIsADwb7XD97DhiD+TVv5ac+jG33BHrg==", | ||||||
|       "requires": {} |       "requires": {} | ||||||
|     }, |     }, | ||||||
|     "@nestjs/passport": { |  | ||||||
|       "version": "9.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", |  | ||||||
|       "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", |  | ||||||
|       "requires": {} |  | ||||||
|     }, |  | ||||||
|     "@nestjs/platform-express": { |     "@nestjs/platform-express": { | ||||||
|       "version": "9.2.1", |       "version": "9.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", |       "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", | ||||||
| @@ -18330,37 +18262,6 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", |       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", | ||||||
|       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" |       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" | ||||||
|     }, |     }, | ||||||
|     "passport": { |  | ||||||
|       "version": "0.6.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", |  | ||||||
|       "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", |  | ||||||
|       "requires": { |  | ||||||
|         "passport-strategy": "1.x.x", |  | ||||||
|         "pause": "0.0.1", |  | ||||||
|         "utils-merge": "^1.0.1" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "passport-custom": { |  | ||||||
|       "version": "1.1.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", |  | ||||||
|       "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", |  | ||||||
|       "requires": { |  | ||||||
|         "passport-strategy": "1.x.x" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "passport-http-header-strategy": { |  | ||||||
|       "version": "1.1.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", |  | ||||||
|       "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", |  | ||||||
|       "requires": { |  | ||||||
|         "passport-strategy": "^1.0.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "passport-strategy": { |  | ||||||
|       "version": "1.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", |  | ||||||
|       "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" |  | ||||||
|     }, |  | ||||||
|     "path-exists": { |     "path-exists": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", | ||||||
| @@ -18400,11 +18301,6 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", | ||||||
|       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" |       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" | ||||||
|     }, |     }, | ||||||
|     "pause": { |  | ||||||
|       "version": "0.0.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", |  | ||||||
|       "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" |  | ||||||
|     }, |  | ||||||
|     "pbf": { |     "pbf": { | ||||||
|       "version": "3.2.1", |       "version": "3.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", |       "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", | ||||||
|   | |||||||
| @@ -43,7 +43,6 @@ | |||||||
|     "@nestjs/config": "^2.2.0", |     "@nestjs/config": "^2.2.0", | ||||||
|     "@nestjs/core": "^9.2.1", |     "@nestjs/core": "^9.2.1", | ||||||
|     "@nestjs/mapped-types": "1.2.0", |     "@nestjs/mapped-types": "1.2.0", | ||||||
|     "@nestjs/passport": "^9.0.0", |  | ||||||
|     "@nestjs/platform-express": "^9.2.1", |     "@nestjs/platform-express": "^9.2.1", | ||||||
|     "@nestjs/platform-socket.io": "^9.2.1", |     "@nestjs/platform-socket.io": "^9.2.1", | ||||||
|     "@nestjs/schedule": "^2.1.0", |     "@nestjs/schedule": "^2.1.0", | ||||||
| @@ -75,9 +74,6 @@ | |||||||
|     "mv": "^2.1.1", |     "mv": "^2.1.1", | ||||||
|     "nest-commander": "^3.3.0", |     "nest-commander": "^3.3.0", | ||||||
|     "openid-client": "^5.2.1", |     "openid-client": "^5.2.1", | ||||||
|     "passport": "^0.6.0", |  | ||||||
|     "passport-custom": "^1.1.1", |  | ||||||
|     "passport-http-header-strategy": "^1.1.0", |  | ||||||
|     "pg": "^8.8.0", |     "pg": "^8.8.0", | ||||||
|     "redis": "^4.5.1", |     "redis": "^4.5.1", | ||||||
|     "reflect-metadata": "^0.1.13", |     "reflect-metadata": "^0.1.13", | ||||||
| @@ -147,10 +143,10 @@ | |||||||
|         "statements": 20 |         "statements": 20 | ||||||
|       }, |       }, | ||||||
|       "./libs/domain/": { |       "./libs/domain/": { | ||||||
|         "branches": 75, |         "branches": 80, | ||||||
|         "functions": 85, |         "functions": 90, | ||||||
|         "lines": 90, |         "lines": 95, | ||||||
|         "statements": 90 |         "statements": 95 | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "testEnvironment": "node", |     "testEnvironment": "node", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user