mirror of
https://github.com/immich-app/immich.git
synced 2025-11-01 02:57:08 -04:00
refactor: users.total metric (#23158)
* refactor: users.total metric * fix: broken test
This commit is contained in:
parent
0b941d78c4
commit
a70843e2b4
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
JobName,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
createStack,
|
createStack,
|
||||||
deleteUserAdmin,
|
deleteUserAdmin,
|
||||||
@ -327,6 +328,8 @@ describe('/admin/users', () => {
|
|||||||
{ headers: asBearerAuth(user.accessToken) },
|
{ headers: asBearerAuth(user.accessToken) },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/admin/users/${user.userId}`)
|
.delete(`/admin/users/${user.userId}`)
|
||||||
.send({ force: true })
|
.send({ force: true })
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
|||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { CliService } from 'src/services/cli.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
@ -56,7 +55,6 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
private jobService: JobService,
|
private jobService: JobService,
|
||||||
private telemetryRepository: TelemetryRepository,
|
private telemetryRepository: TelemetryRepository,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private userRepository: UserRepository,
|
|
||||||
) {
|
) {
|
||||||
logger.setAppName(this.worker);
|
logger.setAppName(this.worker);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto';
|
import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto';
|
||||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
import { ImmichWorker, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { JobItem, JobSource } from 'src/types';
|
import { JobItem, JobSource } from 'src/types';
|
||||||
@ -82,11 +82,33 @@ type EventMap = {
|
|||||||
|
|
||||||
// user events
|
// user events
|
||||||
UserSignup: [{ notify: boolean; id: string; password?: string }];
|
UserSignup: [{ notify: boolean; id: string; password?: string }];
|
||||||
|
UserCreate: [UserEvent];
|
||||||
|
UserDelete: [UserEvent];
|
||||||
|
UserRestore: [UserEvent];
|
||||||
|
|
||||||
// websocket events
|
// websocket events
|
||||||
WebsocketConnect: [{ userId: string }];
|
WebsocketConnect: [{ userId: string }];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserEvent = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
status: UserStatus;
|
||||||
|
email: string;
|
||||||
|
profileImagePath: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
shouldChangePassword: boolean;
|
||||||
|
avatarColor: UserAvatarColor | null;
|
||||||
|
oauthId: string;
|
||||||
|
storageLabel: string | null;
|
||||||
|
quotaSizeInBytes: number | null;
|
||||||
|
quotaUsageInBytes: number;
|
||||||
|
profileChangedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
export const serverEvents = ['ConfigUpdate'] as const;
|
export const serverEvents = ['ConfigUpdate'] as const;
|
||||||
export type ServerEvents = (typeof serverEvents)[number];
|
export type ServerEvents = (typeof serverEvents)[number];
|
||||||
|
|
||||||
|
|||||||
@ -198,8 +198,8 @@ export class BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> {
|
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> {
|
||||||
const user = await this.userRepository.getByEmail(dto.email);
|
const exists = await this.userRepository.getByEmail(dto.email);
|
||||||
if (user) {
|
if (exists) {
|
||||||
throw new BadRequestException('User exists');
|
throw new BadRequestException('User exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +218,10 @@ export class BaseService {
|
|||||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
const user = await this.userRepository.create(payload);
|
||||||
return this.userRepository.create(payload);
|
|
||||||
|
await this.eventRepository.emit('UserCreate', user);
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { SyncService } from 'src/services/sync.service';
|
|||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||||
import { TagService } from 'src/services/tag.service';
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import { TelemetryService } from 'src/services/telemetry.service';
|
||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { TrashService } from 'src/services/trash.service';
|
import { TrashService } from 'src/services/trash.service';
|
||||||
import { UserAdminService } from 'src/services/user-admin.service';
|
import { UserAdminService } from 'src/services/user-admin.service';
|
||||||
@ -78,6 +79,7 @@ export const services = [
|
|||||||
SystemConfigService,
|
SystemConfigService,
|
||||||
SystemMetadataService,
|
SystemMetadataService,
|
||||||
TagService,
|
TagService,
|
||||||
|
TelemetryService,
|
||||||
TimelineService,
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
UserAdminService,
|
UserAdminService,
|
||||||
|
|||||||
26
server/src/services/telemetry.service.ts
Normal file
26
server/src/services/telemetry.service.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { OnEvent } from 'src/decorators';
|
||||||
|
import { ImmichWorker } from 'src/enum';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
|
export class TelemetryService extends BaseService {
|
||||||
|
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
|
||||||
|
async onBootstrap(): Promise<void> {
|
||||||
|
const userCount = await this.userRepository.getCount();
|
||||||
|
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'UserCreate' })
|
||||||
|
onUserCreate() {
|
||||||
|
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'UserDelete' })
|
||||||
|
onUserDelete() {
|
||||||
|
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'UserRestore' })
|
||||||
|
onUserRestore() {
|
||||||
|
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -103,7 +103,8 @@ export class UserAdminService extends BaseService {
|
|||||||
|
|
||||||
const status = force ? UserStatus.Removing : UserStatus.Deleted;
|
const status = force ? UserStatus.Removing : UserStatus.Deleted;
|
||||||
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
||||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
|
|
||||||
|
await this.eventRepository.emit('UserDelete', user);
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
|
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
|
||||||
@ -116,7 +117,7 @@ export class UserAdminService extends BaseService {
|
|||||||
await this.findOrFail(id, { withDeleted: true });
|
await this.findOrFail(id, { withDeleted: true });
|
||||||
await this.albumRepository.restoreAll(id);
|
await this.albumRepository.restoreAll(id);
|
||||||
const user = await this.userRepository.restore(id);
|
const user = await this.userRepository.restore(id);
|
||||||
this.telemetryRepository.api.addToGauge('immich.users.total', 1);
|
await this.eventRepository.emit('UserRestore', user);
|
||||||
return mapUserAdmin(user);
|
return mapUserAdmin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { Updateable } from 'kysely';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnJob } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { CacheControl, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@ -213,12 +213,6 @@ export class UserService extends BaseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
|
|
||||||
async onBootstrap(): Promise<void> {
|
|
||||||
const userCount = await this.userRepository.getCount();
|
|
||||||
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
||||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
|
|||||||
@ -44,7 +44,8 @@ beforeAll(async () => {
|
|||||||
describe(AuthService.name, () => {
|
describe(AuthService.name, () => {
|
||||||
describe('adminSignUp', () => {
|
describe('adminSignUp', () => {
|
||||||
it(`should sign up the admin`, async () => {
|
it(`should sign up the admin`, async () => {
|
||||||
const { sut } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' };
|
const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' };
|
||||||
|
|
||||||
await expect(sut.adminSignUp(dto)).resolves.toEqual(
|
await expect(sut.adminSignUp(dto)).resolves.toEqual(
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { DateTime } from 'luxon';
|
|||||||
import { ImmichEnvironment, JobName, JobStatus } from 'src/enum';
|
import { ImmichEnvironment, JobName, JobStatus } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
@ -22,7 +22,7 @@ const setup = (db?: Kysely<DB>) => {
|
|||||||
return newMediumService(UserService, {
|
return newMediumService(UserService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
||||||
mock: [LoggingRepository, JobRepository, TelemetryRepository],
|
mock: [LoggingRepository, JobRepository, EventRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,7 +35,8 @@ beforeAll(async () => {
|
|||||||
describe(UserService.name, () => {
|
describe(UserService.name, () => {
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create a user', async () => {
|
it('should create a user', async () => {
|
||||||
const { sut } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
const user = mediumFactory.userInsert();
|
const user = mediumFactory.userInsert();
|
||||||
await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual(
|
await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual(
|
||||||
expect.objectContaining({ name: user.name, email: user.email }),
|
expect.objectContaining({ name: user.name, email: user.email }),
|
||||||
@ -43,14 +44,16 @@ describe(UserService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject user with duplicate email', async () => {
|
it('should reject user with duplicate email', async () => {
|
||||||
const { sut } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
const user = mediumFactory.userInsert();
|
const user = mediumFactory.userInsert();
|
||||||
await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email });
|
await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email });
|
||||||
await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists');
|
await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return password', async () => {
|
it('should not return password', async () => {
|
||||||
const { sut } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
const dto = mediumFactory.userInsert({ password: 'password' });
|
const dto = mediumFactory.userInsert({ password: 'password' });
|
||||||
const user = await sut.createUser({ email: dto.email, password: 'password' });
|
const user = await sut.createUser({ email: dto.email, password: 'password' });
|
||||||
expect((user as any).password).toBeUndefined();
|
expect((user as any).password).toBeUndefined();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user