Add device management foundation
This commit is contained in:
parent
8107458ce3
commit
df2d4f99e3
308
backend/src/modules/users/device-management.service.spec.ts
Normal file
308
backend/src/modules/users/device-management.service.spec.ts
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { UserAccountStatus } from '@prisma/client';
|
||||||
|
import { DeviceAuthService } from '../auth/device-auth.service';
|
||||||
|
import { DeviceManagementService } from './device-management.service';
|
||||||
|
|
||||||
|
describe('DeviceManagementService', () => {
|
||||||
|
const activeUserId = randomUUID();
|
||||||
|
const foreignUserId = randomUUID();
|
||||||
|
const lockedUserId = randomUUID();
|
||||||
|
const deletedUserId = randomUUID();
|
||||||
|
|
||||||
|
const buildService = () => {
|
||||||
|
const ownedDeviceId = randomUUID();
|
||||||
|
const foreignDeviceId = randomUUID();
|
||||||
|
const devices = new Map<string, any>([
|
||||||
|
[
|
||||||
|
ownedDeviceId,
|
||||||
|
{
|
||||||
|
id: ownedDeviceId,
|
||||||
|
userId: activeUserId,
|
||||||
|
platform: 'IOS',
|
||||||
|
deviceName: 'Primary iPhone',
|
||||||
|
appVersion: '1.2.3',
|
||||||
|
lastSeenAt: new Date('2026-06-25T08:00:00.000Z'),
|
||||||
|
tokenHash: 'owned-token-hash',
|
||||||
|
tokenLastUsedAt: new Date('2026-06-25T08:30:00.000Z'),
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
linkedAt: new Date('2026-06-20T08:00:00.000Z'),
|
||||||
|
relinkedAt: null,
|
||||||
|
createdAt: new Date('2026-06-20T08:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
foreignDeviceId,
|
||||||
|
{
|
||||||
|
id: foreignDeviceId,
|
||||||
|
userId: foreignUserId,
|
||||||
|
platform: 'ANDROID',
|
||||||
|
deviceName: 'Foreign Pixel',
|
||||||
|
appVersion: '9.9.9',
|
||||||
|
lastSeenAt: new Date('2026-06-24T08:00:00.000Z'),
|
||||||
|
tokenHash: 'foreign-token-hash',
|
||||||
|
tokenLastUsedAt: new Date('2026-06-24T08:30:00.000Z'),
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
linkedAt: new Date('2026-06-18T08:00:00.000Z'),
|
||||||
|
relinkedAt: new Date('2026-06-21T08:00:00.000Z'),
|
||||||
|
createdAt: new Date('2026-06-18T08:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const users = new Map<string, any>([
|
||||||
|
[
|
||||||
|
activeUserId,
|
||||||
|
{
|
||||||
|
id: activeUserId,
|
||||||
|
accountStatus: UserAccountStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
foreignUserId,
|
||||||
|
{
|
||||||
|
id: foreignUserId,
|
||||||
|
accountStatus: UserAccountStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
lockedUserId,
|
||||||
|
{
|
||||||
|
id: lockedUserId,
|
||||||
|
accountStatus: UserAccountStatus.LOCKED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
deletedUserId,
|
||||||
|
{
|
||||||
|
id: deletedUserId,
|
||||||
|
accountStatus: UserAccountStatus.DELETED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deviceLinkHistoryCreates: any[] = [];
|
||||||
|
|
||||||
|
const selectFields = (record: Record<string, unknown>, select?: Record<string, boolean>) => {
|
||||||
|
if (!select) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(select).map((key) => [key, record[key]]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const device = {
|
||||||
|
findMany: jest.fn().mockImplementation(async ({ where, select }) =>
|
||||||
|
Array.from(devices.values())
|
||||||
|
.filter((candidate) => candidate.userId === where.userId)
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.createdAt.getTime() - right.createdAt.getTime(),
|
||||||
|
)
|
||||||
|
.map((candidate) =>
|
||||||
|
selectFields(candidate as Record<string, unknown>, select),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
findUnique: jest.fn().mockImplementation(async ({ where, select }) => {
|
||||||
|
let record: any = null;
|
||||||
|
|
||||||
|
if (where.id) {
|
||||||
|
record = devices.get(where.id) ?? null;
|
||||||
|
} else if (where.tokenHash) {
|
||||||
|
record =
|
||||||
|
Array.from(devices.values()).find(
|
||||||
|
(candidate) => candidate.tokenHash === where.tokenHash,
|
||||||
|
) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectFields(record as Record<string, unknown>, select);
|
||||||
|
}),
|
||||||
|
update: jest.fn().mockImplementation(async ({ where, data, select }) => {
|
||||||
|
const record = devices.get(where.id);
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(record, data);
|
||||||
|
|
||||||
|
return selectFields(record as Record<string, unknown>, select);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
findUnique: jest.fn().mockImplementation(async ({ where, select }) => {
|
||||||
|
const record = users.get(where.id) ?? null;
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectFields(record as Record<string, unknown>, select);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tx = {
|
||||||
|
device,
|
||||||
|
deviceLinkHistory: {
|
||||||
|
create: jest.fn().mockImplementation(async ({ data }) => {
|
||||||
|
deviceLinkHistoryCreates.push(data);
|
||||||
|
return {
|
||||||
|
id: randomUUID(),
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prismaService = {
|
||||||
|
device,
|
||||||
|
user,
|
||||||
|
deviceLinkHistory: tx.deviceLinkHistory,
|
||||||
|
$transaction: jest.fn().mockImplementation(async (callback) => callback(tx)),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const deviceAuthService = {
|
||||||
|
generateDeviceAccessToken: jest.fn().mockReturnValue('rotated-device-token'),
|
||||||
|
hashDeviceAccessToken: jest.fn().mockReturnValue('rotated-device-token-hash'),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const service = new DeviceManagementService(
|
||||||
|
prismaService,
|
||||||
|
deviceAuthService as DeviceAuthService,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
prismaService,
|
||||||
|
deviceAuthService,
|
||||||
|
devices,
|
||||||
|
deviceLinkHistoryCreates,
|
||||||
|
ownedDeviceId,
|
||||||
|
foreignDeviceId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it('lists only devices owned by the requested user', async () => {
|
||||||
|
const { service, ownedDeviceId, foreignDeviceId } = buildService();
|
||||||
|
|
||||||
|
const result = await service.listDevicesForUser(activeUserId);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]?.id).toBe(ownedDeviceId);
|
||||||
|
expect(result.map((device) => device.id)).not.toContain(foreignDeviceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never exposes token hashes when listing devices', async () => {
|
||||||
|
const { service } = buildService();
|
||||||
|
|
||||||
|
const result = await service.listDevicesForUser(activeUserId);
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
platform: 'IOS',
|
||||||
|
deviceName: 'Primary iPhone',
|
||||||
|
appVersion: '1.2.3',
|
||||||
|
lastSeenAt: new Date('2026-06-25T08:00:00.000Z'),
|
||||||
|
tokenLastUsedAt: new Date('2026-06-25T08:30:00.000Z'),
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
linkedAt: new Date('2026-06-20T08:00:00.000Z'),
|
||||||
|
relinkedAt: null,
|
||||||
|
});
|
||||||
|
expect(result[0]).not.toHaveProperty('tokenHash');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokes an owned device without deleting it', async () => {
|
||||||
|
const { service, devices, ownedDeviceId } = buildService();
|
||||||
|
const revokedAt = new Date('2026-06-25T12:00:00.000Z');
|
||||||
|
|
||||||
|
const result = await service.revokeDevice(activeUserId, ownedDeviceId, revokedAt);
|
||||||
|
|
||||||
|
expect(result.tokenRevokedAt).toEqual(revokedAt);
|
||||||
|
expect(devices.get(ownedDeviceId)?.tokenRevokedAt).toEqual(revokedAt);
|
||||||
|
expect(devices.get(ownedDeviceId)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects revoking a foreign device', async () => {
|
||||||
|
const { service, foreignDeviceId } = buildService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.revokeDevice(activeUserId, foreignDeviceId),
|
||||||
|
).rejects.toThrow(new BadRequestException('Device does not belong to account'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rotates the token for an owned device during recovery', async () => {
|
||||||
|
const { service, devices, deviceLinkHistoryCreates, deviceAuthService, ownedDeviceId } =
|
||||||
|
buildService();
|
||||||
|
const now = new Date('2026-06-25T14:00:00.000Z');
|
||||||
|
|
||||||
|
const result = await service.rotateDeviceTokenForRecovery(
|
||||||
|
activeUserId,
|
||||||
|
ownedDeviceId,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
deviceId: ownedDeviceId,
|
||||||
|
deviceAccessToken: 'rotated-device-token',
|
||||||
|
tokenHash: 'rotated-device-token-hash',
|
||||||
|
tokenCreatedAt: now,
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
tokenLastUsedAt: null,
|
||||||
|
});
|
||||||
|
expect(deviceAuthService.generateDeviceAccessToken).toHaveBeenCalled();
|
||||||
|
expect(deviceAuthService.hashDeviceAccessToken).toHaveBeenCalledWith(
|
||||||
|
'rotated-device-token',
|
||||||
|
);
|
||||||
|
expect(devices.get(ownedDeviceId)).toMatchObject({
|
||||||
|
tokenHash: 'rotated-device-token-hash',
|
||||||
|
tokenCreatedAt: now,
|
||||||
|
tokenLastUsedAt: null,
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
});
|
||||||
|
expect(deviceLinkHistoryCreates).toContainEqual({
|
||||||
|
userId: activeUserId,
|
||||||
|
deviceId: ownedDeviceId,
|
||||||
|
linkedByDeviceId: null,
|
||||||
|
linkMethod: DeviceManagementService.recoveryAuditMethod,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects recovery rotation for locked users', async () => {
|
||||||
|
const { service, ownedDeviceId, devices } = buildService();
|
||||||
|
devices.get(ownedDeviceId).userId = lockedUserId;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.rotateDeviceTokenForRecovery(lockedUserId, ownedDeviceId),
|
||||||
|
).rejects.toThrow(new BadRequestException('Account is locked'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects recovery rotation for deleted users', async () => {
|
||||||
|
const { service, ownedDeviceId, devices } = buildService();
|
||||||
|
devices.get(ownedDeviceId).userId = deletedUserId;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.rotateDeviceTokenForRecovery(deletedUserId, ownedDeviceId),
|
||||||
|
).rejects.toThrow(new BadRequestException('Account is deleted'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects recovery rotation for foreign devices', async () => {
|
||||||
|
const { service, foreignDeviceId } = buildService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.rotateDeviceTokenForRecovery(activeUserId, foreignDeviceId),
|
||||||
|
).rejects.toThrow(new BadRequestException('Device does not belong to account'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when validating an unknown device', async () => {
|
||||||
|
const { service } = buildService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.validateDeviceBelongsToUser(activeUserId, randomUUID()),
|
||||||
|
).rejects.toThrow(new NotFoundException('Device not found'));
|
||||||
|
});
|
||||||
|
});
|
||||||
244
backend/src/modules/users/device-management.service.ts
Normal file
244
backend/src/modules/users/device-management.service.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Prisma, UserAccountStatus } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
import { DeviceAuthService } from '../auth/device-auth.service';
|
||||||
|
|
||||||
|
const RECOVERY_AUDIT_METHOD = 'INTERNAL_DEVICE_RECOVERY';
|
||||||
|
|
||||||
|
type DeviceManagementTransactionClient = Pick<
|
||||||
|
Prisma.TransactionClient,
|
||||||
|
'device' | 'deviceLinkHistory'
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface UserDeviceRecord {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
platform: string;
|
||||||
|
deviceName: string;
|
||||||
|
appVersion: string;
|
||||||
|
lastSeenAt: Date;
|
||||||
|
tokenLastUsedAt: Date | null;
|
||||||
|
tokenRevokedAt: Date | null;
|
||||||
|
linkedAt: Date | null;
|
||||||
|
relinkedAt: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceOwnerRecord {
|
||||||
|
id: string;
|
||||||
|
accountStatus: UserAccountStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManagedDeviceSummary {
|
||||||
|
id: string;
|
||||||
|
platform: string;
|
||||||
|
deviceName: string;
|
||||||
|
appVersion: string;
|
||||||
|
lastSeenAt: Date;
|
||||||
|
tokenLastUsedAt: Date | null;
|
||||||
|
tokenRevokedAt: Date | null;
|
||||||
|
linkedAt: Date | null;
|
||||||
|
relinkedAt: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotatedRecoveryDeviceTokenResult {
|
||||||
|
deviceId: string;
|
||||||
|
deviceAccessToken: string;
|
||||||
|
tokenHash: string;
|
||||||
|
tokenCreatedAt: Date;
|
||||||
|
tokenRevokedAt: null;
|
||||||
|
tokenLastUsedAt: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeviceManagementService {
|
||||||
|
static readonly recoveryAuditMethod = RECOVERY_AUDIT_METHOD;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly deviceAuthService: DeviceAuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async listDevicesForUser(userId: string): Promise<ManagedDeviceSummary[]> {
|
||||||
|
return this.prismaService.device.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'asc',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
platform: true,
|
||||||
|
deviceName: true,
|
||||||
|
appVersion: true,
|
||||||
|
lastSeenAt: true,
|
||||||
|
tokenLastUsedAt: true,
|
||||||
|
tokenRevokedAt: true,
|
||||||
|
linkedAt: true,
|
||||||
|
relinkedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateDeviceBelongsToUser(
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
): Promise<UserDeviceRecord> {
|
||||||
|
const device = await this.prismaService.device.findUnique({
|
||||||
|
where: {
|
||||||
|
id: deviceId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
platform: true,
|
||||||
|
deviceName: true,
|
||||||
|
appVersion: true,
|
||||||
|
lastSeenAt: true,
|
||||||
|
tokenLastUsedAt: true,
|
||||||
|
tokenRevokedAt: true,
|
||||||
|
linkedAt: true,
|
||||||
|
relinkedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
throw new NotFoundException('Device not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.userId !== userId) {
|
||||||
|
throw new BadRequestException('Device does not belong to account');
|
||||||
|
}
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeDevice(
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
now = new Date(),
|
||||||
|
): Promise<ManagedDeviceSummary> {
|
||||||
|
await this.validateDeviceBelongsToUser(userId, deviceId);
|
||||||
|
|
||||||
|
return this.prismaService.device.update({
|
||||||
|
where: {
|
||||||
|
id: deviceId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
tokenRevokedAt: now,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
platform: true,
|
||||||
|
deviceName: true,
|
||||||
|
appVersion: true,
|
||||||
|
lastSeenAt: true,
|
||||||
|
tokenLastUsedAt: true,
|
||||||
|
tokenRevokedAt: true,
|
||||||
|
linkedAt: true,
|
||||||
|
relinkedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async rotateDeviceTokenForRecovery(
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
now = new Date(),
|
||||||
|
): Promise<RotatedRecoveryDeviceTokenResult> {
|
||||||
|
const [device, user] = await Promise.all([
|
||||||
|
this.validateDeviceBelongsToUser(userId, deviceId),
|
||||||
|
this.prismaService.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
accountStatus: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('Account not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertUserCanRecoverDevice(user);
|
||||||
|
|
||||||
|
return this.prismaService.$transaction(async (tx) => {
|
||||||
|
const rotatedToken = await this.rotateDeviceToken(tx, device, now);
|
||||||
|
|
||||||
|
await tx.deviceLinkHistory.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
deviceId: device.id,
|
||||||
|
linkedByDeviceId: null,
|
||||||
|
linkMethod: RECOVERY_AUDIT_METHOD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceId: device.id,
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
tokenLastUsedAt: null,
|
||||||
|
...rotatedToken,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async rotateDeviceToken(
|
||||||
|
client: Pick<Prisma.TransactionClient, 'device'>,
|
||||||
|
device: Pick<UserDeviceRecord, 'id'>,
|
||||||
|
now = new Date(),
|
||||||
|
): Promise<{
|
||||||
|
deviceAccessToken: string;
|
||||||
|
tokenHash: string;
|
||||||
|
tokenCreatedAt: Date;
|
||||||
|
}> {
|
||||||
|
const deviceAccessToken = this.deviceAuthService.generateDeviceAccessToken();
|
||||||
|
const tokenHash =
|
||||||
|
this.deviceAuthService.hashDeviceAccessToken(deviceAccessToken);
|
||||||
|
|
||||||
|
await client.device.update({
|
||||||
|
where: {
|
||||||
|
id: device.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
tokenHash: null,
|
||||||
|
tokenRevokedAt: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.device.update({
|
||||||
|
where: {
|
||||||
|
id: device.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
tokenHash,
|
||||||
|
tokenCreatedAt: now,
|
||||||
|
tokenLastUsedAt: null,
|
||||||
|
tokenRevokedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceAccessToken,
|
||||||
|
tokenHash,
|
||||||
|
tokenCreatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertUserCanRecoverDevice(user: DeviceOwnerRecord): void {
|
||||||
|
if (user.accountStatus === UserAccountStatus.LOCKED) {
|
||||||
|
throw new BadRequestException('Account is locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.accountStatus === UserAccountStatus.DELETED) {
|
||||||
|
throw new BadRequestException('Account is deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { AuthModule } from '../auth/auth.module';
|
|||||||
import { AccountController } from './account.controller';
|
import { AccountController } from './account.controller';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
import { DefaultUserService } from './default-user.service';
|
import { DefaultUserService } from './default-user.service';
|
||||||
|
import { DeviceManagementService } from './device-management.service';
|
||||||
import { DeviceLinkingService } from './device-linking.service';
|
import { DeviceLinkingService } from './device-linking.service';
|
||||||
import { OAuthIdentityService } from './oauth-identity.service';
|
import { OAuthIdentityService } from './oauth-identity.service';
|
||||||
import { OwnershipTransferService } from './ownership-transfer.service';
|
import { OwnershipTransferService } from './ownership-transfer.service';
|
||||||
@ -20,6 +21,7 @@ import { StorageModule } from '../storage/storage.module';
|
|||||||
providers: [
|
providers: [
|
||||||
AccountService,
|
AccountService,
|
||||||
DefaultUserService,
|
DefaultUserService,
|
||||||
|
DeviceManagementService,
|
||||||
DeviceLinkingService,
|
DeviceLinkingService,
|
||||||
OAuthIdentityService,
|
OAuthIdentityService,
|
||||||
OwnershipTransferService,
|
OwnershipTransferService,
|
||||||
@ -33,6 +35,7 @@ import { StorageModule } from '../storage/storage.module';
|
|||||||
DefaultUserService,
|
DefaultUserService,
|
||||||
OwnerContext,
|
OwnerContext,
|
||||||
AccountService,
|
AccountService,
|
||||||
|
DeviceManagementService,
|
||||||
DeviceLinkingService,
|
DeviceLinkingService,
|
||||||
OAuthIdentityService,
|
OAuthIdentityService,
|
||||||
OwnershipTransferService,
|
OwnershipTransferService,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user