mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 18:35:00 -04:00 
			
		
		
		
	refactor(server): cli service (#9672)
This commit is contained in:
		
							parent
							
								
									967d195a05
								
							
						
					
					
						commit
						13cbdf6851
					
				| @ -1,18 +1,18 @@ | |||||||
| import { Command, CommandRunner } from 'nest-commander'; | import { Command, CommandRunner } from 'nest-commander'; | ||||||
| import { UserService } from 'src/services/user.service'; | import { CliService } from 'src/services/cli.service'; | ||||||
| 
 | 
 | ||||||
| @Command({ | @Command({ | ||||||
|   name: 'list-users', |   name: 'list-users', | ||||||
|   description: 'List Immich users', |   description: 'List Immich users', | ||||||
| }) | }) | ||||||
| export class ListUsersCommand extends CommandRunner { | export class ListUsersCommand extends CommandRunner { | ||||||
|   constructor(private userService: UserService) { |   constructor(private service: CliService) { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       const users = await this.userService.listUsers(); |       const users = await this.service.listUsers(); | ||||||
|       console.dir(users); |       console.dir(users); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error(error); |       console.error(error); | ||||||
|  | |||||||
| @ -1,19 +1,17 @@ | |||||||
| import { Command, CommandRunner } from 'nest-commander'; | import { Command, CommandRunner } from 'nest-commander'; | ||||||
| import { SystemConfigService } from 'src/services/system-config.service'; | import { CliService } from 'src/services/cli.service'; | ||||||
| 
 | 
 | ||||||
| @Command({ | @Command({ | ||||||
|   name: 'enable-oauth-login', |   name: 'enable-oauth-login', | ||||||
|   description: 'Enable OAuth login', |   description: 'Enable OAuth login', | ||||||
| }) | }) | ||||||
| export class EnableOAuthLogin extends CommandRunner { | export class EnableOAuthLogin extends CommandRunner { | ||||||
|   constructor(private configService: SystemConfigService) { |   constructor(private service: CliService) { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     const config = await this.configService.getConfig(); |     await this.service.enableOAuthLogin(); | ||||||
|     config.oauth.enabled = true; |  | ||||||
|     await this.configService.updateConfig(config); |  | ||||||
|     console.log('OAuth login has been enabled.'); |     console.log('OAuth login has been enabled.'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -23,14 +21,12 @@ export class EnableOAuthLogin extends CommandRunner { | |||||||
|   description: 'Disable OAuth login', |   description: 'Disable OAuth login', | ||||||
| }) | }) | ||||||
| export class DisableOAuthLogin extends CommandRunner { | export class DisableOAuthLogin extends CommandRunner { | ||||||
|   constructor(private configService: SystemConfigService) { |   constructor(private service: CliService) { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     const config = await this.configService.getConfig(); |     await this.service.disableOAuthLogin(); | ||||||
|     config.oauth.enabled = false; |  | ||||||
|     await this.configService.updateConfig(config); |  | ||||||
|     console.log('OAuth login has been disabled.'); |     console.log('OAuth login has been disabled.'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,19 +1,17 @@ | |||||||
| import { Command, CommandRunner } from 'nest-commander'; | import { Command, CommandRunner } from 'nest-commander'; | ||||||
| import { SystemConfigService } from 'src/services/system-config.service'; | import { CliService } from 'src/services/cli.service'; | ||||||
| 
 | 
 | ||||||
| @Command({ | @Command({ | ||||||
|   name: 'enable-password-login', |   name: 'enable-password-login', | ||||||
|   description: 'Enable password login', |   description: 'Enable password login', | ||||||
| }) | }) | ||||||
| export class EnablePasswordLoginCommand extends CommandRunner { | export class EnablePasswordLoginCommand extends CommandRunner { | ||||||
|   constructor(private configService: SystemConfigService) { |   constructor(private service: CliService) { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     const config = await this.configService.getConfig(); |     await this.service.enablePasswordLogin(); | ||||||
|     config.passwordLogin.enabled = true; |  | ||||||
|     await this.configService.updateConfig(config); |  | ||||||
|     console.log('Password login has been enabled.'); |     console.log('Password login has been enabled.'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -23,14 +21,12 @@ export class EnablePasswordLoginCommand extends CommandRunner { | |||||||
|   description: 'Disable password login', |   description: 'Disable password login', | ||||||
| }) | }) | ||||||
| export class DisablePasswordLoginCommand extends CommandRunner { | export class DisablePasswordLoginCommand extends CommandRunner { | ||||||
|   constructor(private configService: SystemConfigService) { |   constructor(private service: CliService) { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     const config = await this.configService.getConfig(); |     await this.service.disablePasswordLogin(); | ||||||
|     config.passwordLogin.enabled = false; |  | ||||||
|     await this.configService.updateConfig(config); |  | ||||||
|     console.log('Password login has been disabled.'); |     console.log('Password login has been disabled.'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,20 +1,9 @@ | |||||||
| import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; | import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; | ||||||
| import { UserResponseDto } from 'src/dtos/user.dto'; | import { UserResponseDto } from 'src/dtos/user.dto'; | ||||||
| import { UserService } from 'src/services/user.service'; | import { CliService } from 'src/services/cli.service'; | ||||||
| 
 | 
 | ||||||
| @Command({ | const prompt = (inquirer: InquirerService) => { | ||||||
|   name: 'reset-admin-password', |   return function ask(admin: UserResponseDto) { | ||||||
|   description: 'Reset the admin password', |  | ||||||
| }) |  | ||||||
| export class ResetAdminPasswordCommand extends CommandRunner { |  | ||||||
|   constructor( |  | ||||||
|     private userService: UserService, |  | ||||||
|     private inquirer: InquirerService, |  | ||||||
|   ) { |  | ||||||
|     super(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   ask = (admin: UserResponseDto) => { |  | ||||||
|     const { id, oauthId, email, name } = admin; |     const { id, oauthId, email, name } = admin; | ||||||
|     console.log(`Found Admin:
 |     console.log(`Found Admin:
 | ||||||
| - ID=${id} | - ID=${id} | ||||||
| @ -22,12 +11,25 @@ export class ResetAdminPasswordCommand extends CommandRunner { | |||||||
| - Email=${email} | - Email=${email} | ||||||
| - Name=${name}`);
 | - Name=${name}`);
 | ||||||
| 
 | 
 | ||||||
|     return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password); |     return inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password); | ||||||
|   }; |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | @Command({ | ||||||
|  |   name: 'reset-admin-password', | ||||||
|  |   description: 'Reset the admin password', | ||||||
|  | }) | ||||||
|  | export class ResetAdminPasswordCommand extends CommandRunner { | ||||||
|  |   constructor( | ||||||
|  |     private service: CliService, | ||||||
|  |     private inquirer: InquirerService, | ||||||
|  |   ) { | ||||||
|  |     super(); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       const { password, provided } = await this.userService.resetAdminPassword(this.ask); |       const { password, provided } = await this.service.resetAdminPassword(prompt(this.inquirer)); | ||||||
| 
 | 
 | ||||||
|       if (provided) { |       if (provided) { | ||||||
|         console.log(`The admin password has been updated.`); |         console.log(`The admin password has been updated.`); | ||||||
|  | |||||||
							
								
								
									
										72
									
								
								server/src/services/cli.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/src/services/cli.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | import { ICryptoRepository } from 'src/interfaces/crypto.interface'; | ||||||
|  | import { ILibraryRepository } from 'src/interfaces/library.interface'; | ||||||
|  | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
|  | import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | ||||||
|  | import { IUserRepository } from 'src/interfaces/user.interface'; | ||||||
|  | import { CliService } from 'src/services/cli.service'; | ||||||
|  | import { userStub } from 'test/fixtures/user.stub'; | ||||||
|  | import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; | ||||||
|  | import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; | ||||||
|  | import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; | ||||||
|  | import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; | ||||||
|  | import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; | ||||||
|  | import { Mocked, describe, it } from 'vitest'; | ||||||
|  | 
 | ||||||
|  | describe(CliService.name, () => { | ||||||
|  |   let sut: CliService; | ||||||
|  | 
 | ||||||
|  |   let userMock: Mocked<IUserRepository>; | ||||||
|  |   let cryptoMock: Mocked<ICryptoRepository>; | ||||||
|  |   let libraryMock: Mocked<ILibraryRepository>; | ||||||
|  |   let systemMock: Mocked<ISystemMetadataRepository>; | ||||||
|  |   let loggerMock: Mocked<ILoggerRepository>; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     cryptoMock = newCryptoRepositoryMock(); | ||||||
|  |     libraryMock = newLibraryRepositoryMock(); | ||||||
|  |     systemMock = newSystemMetadataRepositoryMock(); | ||||||
|  |     userMock = newUserRepositoryMock(); | ||||||
|  |     loggerMock = newLoggerRepositoryMock(); | ||||||
|  | 
 | ||||||
|  |     sut = new CliService(cryptoMock, libraryMock, systemMock, userMock, loggerMock); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('resetAdminPassword', () => { | ||||||
|  |     it('should only work when there is an admin account', async () => { | ||||||
|  |       userMock.getAdmin.mockResolvedValue(null); | ||||||
|  |       const ask = vitest.fn().mockResolvedValue('new-password'); | ||||||
|  | 
 | ||||||
|  |       await expect(sut.resetAdminPassword(ask)).rejects.toThrowError('Admin account does not exist'); | ||||||
|  | 
 | ||||||
|  |       expect(ask).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should default to a random password', async () => { | ||||||
|  |       userMock.getAdmin.mockResolvedValue(userStub.admin); | ||||||
|  |       const ask = vitest.fn().mockImplementation(() => {}); | ||||||
|  | 
 | ||||||
|  |       const response = await sut.resetAdminPassword(ask); | ||||||
|  | 
 | ||||||
|  |       const [id, update] = userMock.update.mock.calls[0]; | ||||||
|  | 
 | ||||||
|  |       expect(response.provided).toBe(false); | ||||||
|  |       expect(ask).toHaveBeenCalled(); | ||||||
|  |       expect(id).toEqual(userStub.admin.id); | ||||||
|  |       expect(update.password).toBeDefined(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should use the supplied password', async () => { | ||||||
|  |       userMock.getAdmin.mockResolvedValue(userStub.admin); | ||||||
|  |       const ask = vitest.fn().mockResolvedValue('new-password'); | ||||||
|  | 
 | ||||||
|  |       const response = await sut.resetAdminPassword(ask); | ||||||
|  | 
 | ||||||
|  |       const [id, update] = userMock.update.mock.calls[0]; | ||||||
|  | 
 | ||||||
|  |       expect(response.provided).toBe(true); | ||||||
|  |       expect(ask).toHaveBeenCalled(); | ||||||
|  |       expect(id).toEqual(userStub.admin.id); | ||||||
|  |       expect(update.password).toBeDefined(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										70
									
								
								server/src/services/cli.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								server/src/services/cli.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | import { Inject, Injectable } from '@nestjs/common'; | ||||||
|  | import { SystemConfigCore } from 'src/cores/system-config.core'; | ||||||
|  | import { UserCore } from 'src/cores/user.core'; | ||||||
|  | import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; | ||||||
|  | import { ICryptoRepository } from 'src/interfaces/crypto.interface'; | ||||||
|  | import { ILibraryRepository } from 'src/interfaces/library.interface'; | ||||||
|  | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
|  | import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | ||||||
|  | import { IUserRepository } from 'src/interfaces/user.interface'; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export class CliService { | ||||||
|  |   private configCore: SystemConfigCore; | ||||||
|  |   private userCore: UserCore; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, | ||||||
|  |     @Inject(ILibraryRepository) libraryRepository: ILibraryRepository, | ||||||
|  |     @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, | ||||||
|  |     @Inject(IUserRepository) private userRepository: IUserRepository, | ||||||
|  |     @Inject(ILoggerRepository) private logger: ILoggerRepository, | ||||||
|  |   ) { | ||||||
|  |     this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); | ||||||
|  |     this.logger.setContext(CliService.name); | ||||||
|  |     this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async listUsers(): Promise<UserResponseDto[]> { | ||||||
|  |     const users = await this.userRepository.getList({ withDeleted: true }); | ||||||
|  |     return users.map((user) => mapUser(user)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) { | ||||||
|  |     const admin = await this.userRepository.getAdmin(); | ||||||
|  |     if (!admin) { | ||||||
|  |       throw new Error('Admin account does not exist'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const providedPassword = await ask(mapUser(admin)); | ||||||
|  |     const password = providedPassword || this.cryptoRepository.newPassword(24); | ||||||
|  | 
 | ||||||
|  |     await this.userCore.updateUser(admin, admin.id, { password }); | ||||||
|  | 
 | ||||||
|  |     return { admin, password, provided: !!providedPassword }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async disablePasswordLogin(): Promise<void> { | ||||||
|  |     const config = await this.configCore.getConfig(); | ||||||
|  |     config.passwordLogin.enabled = false; | ||||||
|  |     await this.configCore.updateConfig(config); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async enablePasswordLogin(): Promise<void> { | ||||||
|  |     const config = await this.configCore.getConfig(); | ||||||
|  |     config.passwordLogin.enabled = true; | ||||||
|  |     await this.configCore.updateConfig(config); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async disableOAuthLogin(): Promise<void> { | ||||||
|  |     const config = await this.configCore.getConfig(); | ||||||
|  |     config.oauth.enabled = false; | ||||||
|  |     await this.configCore.updateConfig(config); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async enableOAuthLogin(): Promise<void> { | ||||||
|  |     const config = await this.configCore.getConfig(); | ||||||
|  |     config.oauth.enabled = true; | ||||||
|  |     await this.configCore.updateConfig(config); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -6,6 +6,7 @@ import { AssetServiceV1 } from 'src/services/asset-v1.service'; | |||||||
| import { AssetService } from 'src/services/asset.service'; | import { AssetService } from 'src/services/asset.service'; | ||||||
| import { AuditService } from 'src/services/audit.service'; | import { AuditService } from 'src/services/audit.service'; | ||||||
| import { AuthService } from 'src/services/auth.service'; | import { AuthService } from 'src/services/auth.service'; | ||||||
|  | import { CliService } from 'src/services/cli.service'; | ||||||
| import { DatabaseService } from 'src/services/database.service'; | import { DatabaseService } from 'src/services/database.service'; | ||||||
| import { DownloadService } from 'src/services/download.service'; | import { DownloadService } from 'src/services/download.service'; | ||||||
| import { DuplicateService } from 'src/services/duplicate.service'; | import { DuplicateService } from 'src/services/duplicate.service'; | ||||||
| @ -44,6 +45,7 @@ export const services = [ | |||||||
|   AssetServiceV1, |   AssetServiceV1, | ||||||
|   AuditService, |   AuditService, | ||||||
|   AuthService, |   AuthService, | ||||||
|  |   CliService, | ||||||
|   DatabaseService, |   DatabaseService, | ||||||
|   DownloadService, |   DownloadService, | ||||||
|   DuplicateService, |   DuplicateService, | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.moc | |||||||
| import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; | import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; | ||||||
| import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; | import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; | ||||||
| import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; | import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; | ||||||
| import { Mocked, vitest } from 'vitest'; | import { Mocked } from 'vitest'; | ||||||
| 
 | 
 | ||||||
| const makeDeletedAt = (daysAgo: number) => { | const makeDeletedAt = (daysAgo: number) => { | ||||||
|   const deletedAt = new Date(); |   const deletedAt = new Date(); | ||||||
| @ -436,45 +436,6 @@ describe(UserService.name, () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('resetAdminPassword', () => { |  | ||||||
|     it('should only work when there is an admin account', async () => { |  | ||||||
|       userMock.getAdmin.mockResolvedValue(null); |  | ||||||
|       const ask = vitest.fn().mockResolvedValue('new-password'); |  | ||||||
| 
 |  | ||||||
|       await expect(sut.resetAdminPassword(ask)).rejects.toBeInstanceOf(BadRequestException); |  | ||||||
| 
 |  | ||||||
|       expect(ask).not.toHaveBeenCalled(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('should default to a random password', async () => { |  | ||||||
|       userMock.getAdmin.mockResolvedValue(userStub.admin); |  | ||||||
|       const ask = vitest.fn().mockImplementation(() => {}); |  | ||||||
| 
 |  | ||||||
|       const response = await sut.resetAdminPassword(ask); |  | ||||||
| 
 |  | ||||||
|       const [id, update] = userMock.update.mock.calls[0]; |  | ||||||
| 
 |  | ||||||
|       expect(response.provided).toBe(false); |  | ||||||
|       expect(ask).toHaveBeenCalled(); |  | ||||||
|       expect(id).toEqual(userStub.admin.id); |  | ||||||
|       expect(update.password).toBeDefined(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('should use the supplied password', async () => { |  | ||||||
|       userMock.getAdmin.mockResolvedValue(userStub.admin); |  | ||||||
|       const ask = vitest.fn().mockResolvedValue('new-password'); |  | ||||||
| 
 |  | ||||||
|       const response = await sut.resetAdminPassword(ask); |  | ||||||
| 
 |  | ||||||
|       const [id, update] = userMock.update.mock.calls[0]; |  | ||||||
| 
 |  | ||||||
|       expect(response.provided).toBe(true); |  | ||||||
|       expect(ask).toHaveBeenCalled(); |  | ||||||
|       expect(id).toEqual(userStub.admin.id); |  | ||||||
|       expect(update.password).toBeDefined(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('handleQueueUserDelete', () => { |   describe('handleQueueUserDelete', () => { | ||||||
|     it('should skip users not ready for deletion', async () => { |     it('should skip users not ready for deletion', async () => { | ||||||
|       userMock.getDeletedUsers.mockResolvedValue([ |       userMock.getDeletedUsers.mockResolvedValue([ | ||||||
|  | |||||||
| @ -170,20 +170,6 @@ export class UserService { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) { |  | ||||||
|     const admin = await this.userRepository.getAdmin(); |  | ||||||
|     if (!admin) { |  | ||||||
|       throw new BadRequestException('Admin account does not exist'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const providedPassword = await ask(mapUser(admin)); |  | ||||||
|     const password = providedPassword || this.cryptoRepository.newPassword(24); |  | ||||||
| 
 |  | ||||||
|     await this.userCore.updateUser(admin, admin.id, { password }); |  | ||||||
| 
 |  | ||||||
|     return { admin, password, provided: !!providedPassword }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async handleUserSyncUsage(): Promise<JobStatus> { |   async handleUserSyncUsage(): Promise<JobStatus> { | ||||||
|     await this.userRepository.syncUsage(); |     await this.userRepository.syncUsage(); | ||||||
|     return JobStatus.SUCCESS; |     return JobStatus.SUCCESS; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user