feat: stabilize social backend contracts
هذا الالتزام موجود في:
26
src/modules/devices/devices.controller.ts
Normal file
26
src/modules/devices/devices.controller.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { JwtPayload } from '../../common/interfaces/jwt-payload.interface';
|
||||
import { RegisterDeviceDto } from './dto/register-device.dto';
|
||||
import { UnregisterDeviceDto } from './dto/unregister-device.dto';
|
||||
import { DevicesService } from './devices.service';
|
||||
|
||||
@ApiTags('Devices')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('devices')
|
||||
export class DevicesController {
|
||||
constructor(private readonly devicesService: DevicesService) {}
|
||||
|
||||
@Post('register')
|
||||
async register(@CurrentUser() user: JwtPayload, @Body() dto: RegisterDeviceDto) {
|
||||
return this.devicesService.register(user.sub, dto);
|
||||
}
|
||||
|
||||
@Post('unregister')
|
||||
async unregister(@CurrentUser() user: JwtPayload, @Body() dto: UnregisterDeviceDto) {
|
||||
return this.devicesService.unregister(user.sub, dto);
|
||||
}
|
||||
}
|
||||
14
src/modules/devices/devices.module.ts
Normal file
14
src/modules/devices/devices.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { DevicesController } from './devices.controller';
|
||||
import { DevicesRepository } from './devices.repository';
|
||||
import { DevicesService } from './devices.service';
|
||||
import { Device, DeviceSchema } from './schemas/device.schema';
|
||||
|
||||
@Module({
|
||||
imports: [MongooseModule.forFeature([{ name: Device.name, schema: DeviceSchema }])],
|
||||
controllers: [DevicesController],
|
||||
providers: [DevicesService, DevicesRepository],
|
||||
exports: [DevicesService, DevicesRepository],
|
||||
})
|
||||
export class DevicesModule {}
|
||||
45
src/modules/devices/devices.repository.ts
Normal file
45
src/modules/devices/devices.repository.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model, Types } from 'mongoose';
|
||||
import { Device, DeviceDocument } from './schemas/device.schema';
|
||||
|
||||
@Injectable()
|
||||
export class DevicesRepository {
|
||||
constructor(@InjectModel(Device.name) private readonly deviceModel: Model<DeviceDocument>) {}
|
||||
|
||||
async upsert(userId: string, payload: Omit<Partial<Device>, 'userId'>): Promise<DeviceDocument> {
|
||||
const filter = payload.deviceId
|
||||
? { userId: new Types.ObjectId(userId), deviceId: payload.deviceId }
|
||||
: { userId: new Types.ObjectId(userId), fcmToken: payload.fcmToken };
|
||||
|
||||
return this.deviceModel
|
||||
.findOneAndUpdate(
|
||||
filter,
|
||||
{
|
||||
$set: {
|
||||
...payload,
|
||||
userId: new Types.ObjectId(userId),
|
||||
isActive: true,
|
||||
lastSeenAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ new: true, upsert: true, setDefaultsOnInsert: true },
|
||||
)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async deactivate(userId: string, payload: { fcmToken?: string; deviceId?: string }): Promise<DeviceDocument | null> {
|
||||
const filter: Record<string, unknown> = { userId: new Types.ObjectId(userId) };
|
||||
if (payload.deviceId) {
|
||||
filter.deviceId = payload.deviceId;
|
||||
} else if (payload.fcmToken) {
|
||||
filter.fcmToken = payload.fcmToken;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.deviceModel
|
||||
.findOneAndUpdate(filter, { isActive: false, lastSeenAt: new Date() }, { new: true })
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
45
src/modules/devices/devices.service.ts
Normal file
45
src/modules/devices/devices.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { RegisterDeviceDto } from './dto/register-device.dto';
|
||||
import { UnregisterDeviceDto } from './dto/unregister-device.dto';
|
||||
import { DevicesRepository } from './devices.repository';
|
||||
|
||||
@Injectable()
|
||||
export class DevicesService {
|
||||
constructor(private readonly devicesRepository: DevicesRepository) {}
|
||||
|
||||
async register(userId: string, dto: RegisterDeviceDto) {
|
||||
const fcmToken = dto.fcmToken.trim();
|
||||
if (!fcmToken) {
|
||||
throw new BadRequestException('fcmToken is required');
|
||||
}
|
||||
|
||||
const device = await this.devicesRepository.upsert(userId, {
|
||||
fcmToken,
|
||||
platform: dto.platform,
|
||||
deviceId: dto.deviceId?.trim() ?? '',
|
||||
appVersion: dto.appVersion?.trim() ?? '',
|
||||
locale: dto.locale?.trim() ?? '',
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'Device registered successfully',
|
||||
device,
|
||||
};
|
||||
}
|
||||
|
||||
async unregister(userId: string, dto: UnregisterDeviceDto) {
|
||||
if (!dto.deviceId?.trim() && !dto.fcmToken?.trim()) {
|
||||
throw new BadRequestException('deviceId or fcmToken is required');
|
||||
}
|
||||
|
||||
const device = await this.devicesRepository.deactivate(userId, {
|
||||
deviceId: dto.deviceId?.trim(),
|
||||
fcmToken: dto.fcmToken?.trim(),
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'Device unregistered successfully',
|
||||
device,
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/modules/devices/dto/register-device.dto.ts
Normal file
25
src/modules/devices/dto/register-device.dto.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class RegisterDeviceDto {
|
||||
@IsString()
|
||||
@MaxLength(4096)
|
||||
fcmToken!: string;
|
||||
|
||||
@IsEnum(['android', 'ios', 'web'])
|
||||
platform!: 'android' | 'ios' | 'web';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(160)
|
||||
deviceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(60)
|
||||
appVersion?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
locale?: string;
|
||||
}
|
||||
13
src/modules/devices/dto/unregister-device.dto.ts
Normal file
13
src/modules/devices/dto/unregister-device.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class UnregisterDeviceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(4096)
|
||||
fcmToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(160)
|
||||
deviceId?: string;
|
||||
}
|
||||
36
src/modules/devices/schemas/device.schema.ts
Normal file
36
src/modules/devices/schemas/device.schema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { HydratedDocument, Types } from 'mongoose';
|
||||
import { User } from '../../users/schemas/user.schema';
|
||||
|
||||
export type DeviceDocument = HydratedDocument<Device>;
|
||||
|
||||
@Schema({ timestamps: true, versionKey: false })
|
||||
export class Device {
|
||||
@Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true })
|
||||
userId!: Types.ObjectId;
|
||||
|
||||
@Prop({ required: true, trim: true, index: true })
|
||||
fcmToken!: string;
|
||||
|
||||
@Prop({ enum: ['android', 'ios', 'web'], required: true, index: true })
|
||||
platform!: 'android' | 'ios' | 'web';
|
||||
|
||||
@Prop({ default: '', trim: true, index: true })
|
||||
deviceId!: string;
|
||||
|
||||
@Prop({ default: '', trim: true })
|
||||
appVersion!: string;
|
||||
|
||||
@Prop({ default: '', trim: true })
|
||||
locale!: string;
|
||||
|
||||
@Prop({ default: true, index: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Prop({ type: Date, default: null })
|
||||
lastSeenAt?: Date | null;
|
||||
}
|
||||
|
||||
export const DeviceSchema = SchemaFactory.createForClass(Device);
|
||||
DeviceSchema.index({ userId: 1, fcmToken: 1 }, { unique: true });
|
||||
DeviceSchema.index({ userId: 1, deviceId: 1 }, { sparse: true });
|
||||
المرجع في مشكلة جديدة
حظر مستخدم