feat: stabilize social backend contracts

هذا الالتزام موجود في:
boutmoun123
2026-05-31 16:13:23 +03:00
الأصل ad6da6754d
التزام 49e132909e
40 ملفات معدلة مع 12037 إضافات و9562 حذوفات

عرض الملف

@@ -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);
}
}

عرض الملف

@@ -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 {}

عرض الملف

@@ -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();
}
}

عرض الملف

@@ -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,
};
}
}

عرض الملف

@@ -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;
}

عرض الملف

@@ -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;
}

عرض الملف

@@ -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 });