update: add new logic for push token
This commit is contained in:
@@ -0,0 +1,214 @@
|
|||||||
|
# Multi-Device Push Notifications Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Updated the push notification system to support multiple devices per user. Users can now receive push notifications on all their logged-in devices (mobile, tablet, desktop, etc.).
|
||||||
|
|
||||||
|
## Database Changes
|
||||||
|
|
||||||
|
### Migration: `update_push_tokens_to_array.sql`
|
||||||
|
|
||||||
|
- Converted single `push_token` field to `push_tokens` JSON array
|
||||||
|
- Automatically migrates existing single tokens to array format
|
||||||
|
- Adds optimized GIN index for JSON array queries
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Add Push Token (New Device Login)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /users/push-token
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"pushToken": "device_push_token_from_najva_sdk",
|
||||||
|
"deviceId": "optional_device_identifier"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Remove Push Token (Device Logout)
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /users/push-token
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"pushToken": "device_push_token_to_remove"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Get All Push Tokens
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /users/push-tokens
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pushTokens": ["token1", "token2", "token3"],
|
||||||
|
"totalTokens": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update Profile (Push Token Removed)
|
||||||
|
|
||||||
|
```http
|
||||||
|
PATCH /users/profile
|
||||||
|
Authorization: Bearer <jwt_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"profilePic": "https://example.com/profile.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### ✅ Multi-Device Support
|
||||||
|
|
||||||
|
- Users can have multiple push tokens (one per device)
|
||||||
|
- Notifications sent to all registered devices simultaneously
|
||||||
|
- Automatic deduplication prevents duplicate tokens
|
||||||
|
|
||||||
|
### ✅ Invalid Token Cleanup
|
||||||
|
|
||||||
|
- Automatically detects invalid/expired tokens from Najva API response
|
||||||
|
- Removes invalid tokens from user's token list
|
||||||
|
- Logs cleanup activities for monitoring
|
||||||
|
|
||||||
|
### ✅ Device Management
|
||||||
|
|
||||||
|
- Add tokens when user logs in on new device
|
||||||
|
- Remove tokens when user logs out from device
|
||||||
|
- View all registered devices/tokens
|
||||||
|
|
||||||
|
### ✅ Backward Compatibility
|
||||||
|
|
||||||
|
- Existing single token data automatically migrated to array format
|
||||||
|
- No breaking changes to existing notification flow
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### User Entity Changes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
@Property({ type: "varchar", nullable: true })
|
||||||
|
pushToken?: string;
|
||||||
|
|
||||||
|
// After
|
||||||
|
@Property({ type: "json", nullable: true })
|
||||||
|
pushTokens?: string[];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add token for new device
|
||||||
|
await usersService.addPushToken(userId, { pushToken: "token" });
|
||||||
|
|
||||||
|
// Remove token when logging out
|
||||||
|
await usersService.removePushToken(userId, { pushToken: "token" });
|
||||||
|
|
||||||
|
// Send to all user devices
|
||||||
|
await notificationsService.sendPushNotificationToUserDevices(userId, userTokens, message);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Token Cleanup
|
||||||
|
|
||||||
|
When sending notifications, invalid tokens are automatically:
|
||||||
|
|
||||||
|
1. Detected from Najva API response
|
||||||
|
2. Logged for monitoring
|
||||||
|
3. Removed from user's token array
|
||||||
|
4. Database updated to clean state
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
### When User Logs In
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get push token from Najva SDK
|
||||||
|
const pushToken = await najva.getPushToken();
|
||||||
|
|
||||||
|
// Register token for this device
|
||||||
|
await fetch("/api/users/push-token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
pushToken: pushToken,
|
||||||
|
deviceId: navigator.userAgent, // optional
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### When User Logs Out
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Remove token for this device
|
||||||
|
await fetch("/api/users/push-token", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
pushToken: currentDeviceToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
NAJVA_API_KEY=your_najva_api_key
|
||||||
|
NAJVA_WEBSITE_ID=your_website_id
|
||||||
|
NAJVA_API_URL=https://push.najva.com/v1/send/token/
|
||||||
|
PUSH_TTL=24
|
||||||
|
FRONTEND_URL=https://your-frontend-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Multi-Device Support**: Users get notifications on all their devices
|
||||||
|
2. **Device Management**: Add/remove devices as needed
|
||||||
|
3. **Automatic Cleanup**: Invalid tokens automatically removed
|
||||||
|
4. **Scalable**: Efficient JSON array storage with GIN indexing
|
||||||
|
5. **Reliable**: Handles edge cases like duplicate tokens and API failures
|
||||||
|
6. **Monitoring**: Comprehensive logging for debugging and monitoring
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
1. **Run Database Migration**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Execute: database/migrations/update_push_tokens_to_array.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update Frontend**:
|
||||||
|
- Use new `/users/push-token` endpoints
|
||||||
|
- Remove `pushToken` field from profile updates
|
||||||
|
|
||||||
|
3. **Deploy Backend**:
|
||||||
|
- All changes are backward compatible
|
||||||
|
- Existing tokens automatically migrated
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test the multi-device functionality:
|
||||||
|
|
||||||
|
1. Login from Device A → Add token A
|
||||||
|
2. Login from Device B → Add token B
|
||||||
|
3. Create notification → Both devices receive push
|
||||||
|
4. Logout from Device A → Remove token A
|
||||||
|
5. Create notification → Only device B receives push
|
||||||
|
|
||||||
|
The system now properly supports users accessing your application from multiple devices while maintaining reliable push notification delivery!
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- Update push notification token field to support multiple devices
|
||||||
|
-- Step 1: Add new push_tokens JSON array column
|
||||||
|
ALTER TABLE users ADD COLUMN push_tokens JSONB DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Step 2: Migrate existing single push_token data to the new array format
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
push_tokens = CASE
|
||||||
|
WHEN push_token IS NOT NULL
|
||||||
|
AND push_token != '' THEN jsonb_build_array (push_token)
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
push_token IS NOT NULL;
|
||||||
|
|
||||||
|
-- Step 3: Drop the old single push_token column
|
||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS push_token;
|
||||||
|
|
||||||
|
-- Step 4: Add index for better performance on push_tokens
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_push_tokens ON users USING GIN (push_tokens)
|
||||||
|
WHERE
|
||||||
|
push_tokens IS NOT NULL
|
||||||
|
AND push_tokens != 'null'::jsonb
|
||||||
|
AND jsonb_array_length(push_tokens) > 0
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- Step 5: Add comment for documentation
|
||||||
|
COMMENT ON COLUMN users.push_tokens IS 'JSON array of Najva push notification tokens from different user devices';
|
||||||
@@ -110,6 +110,10 @@ export const enum UserMessage {
|
|||||||
PROFILE_PICTURE_URL = "آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد",
|
PROFILE_PICTURE_URL = "آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد",
|
||||||
USER_UPDATED_SUCCESSFULLY = "کاربر با موفقیت به روز رسانی شد",
|
USER_UPDATED_SUCCESSFULLY = "کاربر با موفقیت به روز رسانی شد",
|
||||||
PROFILE_PICTURE_NOT_EMPTY = "تصویر پروفایل نمیتواند خالی باشد",
|
PROFILE_PICTURE_NOT_EMPTY = "تصویر پروفایل نمیتواند خالی باشد",
|
||||||
|
PUSH_TOKEN_NOT_FOUND = "توکن پوش نامعتبر است",
|
||||||
|
PUSH_TOKEN_ADDED_SUCCESSFULLY = "توکن پوش با موفقیت اضافه شد",
|
||||||
|
NO_PUSH_TOKENS_FOUND = "توکن پوشی برای این کاربر یافت نشد",
|
||||||
|
PUSH_TOKEN_REMOVED_SUCCESSFULLY = "توکن پوش با موفقیت حذف شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CommonMessage {
|
export const enum CommonMessage {
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ import { NotificationsService } from "./services/notifications.service";
|
|||||||
import { najvaConfigProvider } from "../../common/providers/najva-config.provider";
|
import { najvaConfigProvider } from "../../common/providers/najva-config.provider";
|
||||||
import { NotificationSetting } from "../settings/entities/notification-setting.entity";
|
import { NotificationSetting } from "../settings/entities/notification-setting.entity";
|
||||||
import { SettingModule } from "../settings/settings.module";
|
import { SettingModule } from "../settings/settings.module";
|
||||||
|
import { User } from "../users/entities/user.entity";
|
||||||
import { UtilsModule } from "../utils/utils.module";
|
import { UtilsModule } from "../utils/utils.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
HttpModule,
|
HttpModule,
|
||||||
MikroOrmModule.forFeature([Notification, NotificationSetting]),
|
MikroOrmModule.forFeature([Notification, NotificationSetting, User]),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: NOTIFICATION.QUEUE_NAME,
|
name: NOTIFICATION.QUEUE_NAME,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ export class NajvaPushService {
|
|||||||
|
|
||||||
const successCount = result.Entries.tokens.filter((t) => t.status === "Sent").length;
|
const successCount = result.Entries.tokens.filter((t) => t.status === "Sent").length;
|
||||||
const failedCount = result.Entries.tokens.filter((t) => t.status === "InvalidToken").length;
|
const failedCount = result.Entries.tokens.filter((t) => t.status === "InvalidToken").length;
|
||||||
|
const invalidTokens = result.Entries.tokens.filter((t) => t.status === "InvalidToken").map((t) => t.token);
|
||||||
|
|
||||||
return { success: successCount, failed: failedCount };
|
return { success: successCount, failed: failedCount, invalidTokens };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to send notifications to multiple users:`, error);
|
this.logger.error(`Failed to send notifications to multiple users:`, error);
|
||||||
return { success: 0, failed: userTokens.length };
|
return { success: 0, failed: userTokens.length };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CommonMessage, NotificationMessage, UserMessage } from "../../../common
|
|||||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||||
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||||
import { Notification } from "../entities/notification.entity";
|
import { Notification } from "../entities/notification.entity";
|
||||||
@@ -23,6 +24,7 @@ export class NotificationsService {
|
|||||||
private readonly userSettingsService: UserSettingsService,
|
private readonly userSettingsService: UserSettingsService,
|
||||||
private readonly najvaPushService: NajvaPushService,
|
private readonly najvaPushService: NajvaPushService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly userRepository: UserRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//************************ */
|
//************************ */
|
||||||
@@ -42,8 +44,9 @@ export class NotificationsService {
|
|||||||
});
|
});
|
||||||
await localEm.persistAndFlush(notification);
|
await localEm.persistAndFlush(notification);
|
||||||
|
|
||||||
if (user.pushToken) {
|
// Send push notification to all user devices if user has push tokens
|
||||||
await this.sendPushNotificationToUser(user.pushToken, {
|
if (user.pushTokens && user.pushTokens.length > 0) {
|
||||||
|
await this.sendPushNotificationToUserDevices(user.id, user.pushTokens, {
|
||||||
title: createNotificationDto.title,
|
title: createNotificationDto.title,
|
||||||
body: createNotificationDto.message,
|
body: createNotificationDto.message,
|
||||||
notification_click: {
|
notification_click: {
|
||||||
@@ -75,8 +78,9 @@ export class NotificationsService {
|
|||||||
|
|
||||||
await em.persistAndFlush(notification);
|
await em.persistAndFlush(notification);
|
||||||
|
|
||||||
if (user.pushToken) {
|
// Send push notification to all user devices if user has push tokens
|
||||||
await this.sendPushNotificationToUser(user.pushToken, {
|
if (user.pushTokens && user.pushTokens.length > 0) {
|
||||||
|
await this.sendPushNotificationToUserDevices(user.id, user.pushTokens, {
|
||||||
title: createNotificationDto.title,
|
title: createNotificationDto.title,
|
||||||
body: createNotificationDto.message,
|
body: createNotificationDto.message,
|
||||||
notification_click: {
|
notification_click: {
|
||||||
@@ -103,6 +107,25 @@ export class NotificationsService {
|
|||||||
|
|
||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
|
async sendPushNotificationToUserDevices(userId: string, userTokens: string[], message: INajvaNotificationMessage) {
|
||||||
|
try {
|
||||||
|
const result = await this.najvaPushService.sendNotificationToMultipleUsers(userTokens, message);
|
||||||
|
|
||||||
|
if (result.invalidTokens && result.invalidTokens.length > 0) {
|
||||||
|
this.logger.warn(`Found ${result.invalidTokens.length} invalid tokens for user ${userId}: ${result.invalidTokens.join(", ")}`);
|
||||||
|
|
||||||
|
await this.cleanupInvalidPushTokens(userId, result.invalidTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to send push notifications to user devices:`, error);
|
||||||
|
return { success: 0, failed: userTokens.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
|
||||||
async markAsRead(notificationId: string, userId: string) {
|
async markAsRead(notificationId: string, userId: string) {
|
||||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId }, deletedAt: null });
|
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId }, deletedAt: null });
|
||||||
|
|
||||||
@@ -177,4 +200,20 @@ export class NotificationsService {
|
|||||||
return this.createNotification({ title: NotificationMessage.CHANGE_PASSWORD, type: NotifType.CHANGE_PASSWORD, message, recipientId }, em);
|
return this.createNotification({ title: NotificationMessage.CHANGE_PASSWORD, type: NotifType.CHANGE_PASSWORD, message, recipientId }, em);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
private async cleanupInvalidPushTokens(userId: string, invalidTokens: string[]) {
|
||||||
|
if (!invalidTokens || invalidTokens.length === 0) return;
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
||||||
|
if (!user || !user.pushTokens) return;
|
||||||
|
|
||||||
|
const initialLength = user.pushTokens.length;
|
||||||
|
user.pushTokens = user.pushTokens.filter((token) => !invalidTokens.includes(token));
|
||||||
|
|
||||||
|
if (user.pushTokens.length !== initialLength) {
|
||||||
|
await this.em.flush();
|
||||||
|
this.logger.log(`Cleaned up ${initialLength - user.pushTokens.length} invalid push tokens for user ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class AddPushTokenDto {
|
||||||
|
@IsString({ message: "Push notification token must be a string" })
|
||||||
|
@IsNotEmpty({ message: "Push notification token is required" })
|
||||||
|
@ApiProperty({ description: "Push notification token from frontend device", example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
||||||
|
pushToken: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: "Device identifier must be a string" })
|
||||||
|
@ApiPropertyOptional({ description: "Device identifier (optional, for tracking)", example: "mobile-chrome-android" })
|
||||||
|
deviceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemovePushTokenDto {
|
||||||
|
@IsString({ message: "Push notification token must be a string" })
|
||||||
|
@IsNotEmpty({ message: "Push notification token is required" })
|
||||||
|
@ApiProperty({ description: "Push notification token to remove", example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
||||||
|
pushToken!: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
|
import { IsNotEmpty, IsOptional, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import { UserMessage } from "../../../common/enums/message.enum";
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -9,9 +9,4 @@ export class UpdateUserProfileDto {
|
|||||||
@IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL })
|
@IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL })
|
||||||
@ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" })
|
@ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" })
|
||||||
profilePic?: string;
|
profilePic?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@ApiPropertyOptional({ description: "Push notification token from frontend", example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
|
||||||
pushToken?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ export class User extends BaseEntity {
|
|||||||
@Property({ type: "timestamptz", nullable: true })
|
@Property({ type: "timestamptz", nullable: true })
|
||||||
twoFactorEnabledAt?: Date;
|
twoFactorEnabledAt?: Date;
|
||||||
|
|
||||||
@Property({ type: "varchar", nullable: true })
|
// Push notification tokens from different devices
|
||||||
pushToken?: string;
|
@Property({ type: "json", nullable: true })
|
||||||
|
pushTokens?: string[];
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { UserSettingsService } from "../../settings/services/user-settings.servi
|
|||||||
import { Template } from "../../templates/entities/template.entity";
|
import { Template } from "../../templates/entities/template.entity";
|
||||||
import { PasswordService } from "../../utils/services/password.service";
|
import { PasswordService } from "../../utils/services/password.service";
|
||||||
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
|
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
|
||||||
|
import { AddPushTokenDto, RemovePushTokenDto } from "../DTO/manage-push-token.dto";
|
||||||
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
|
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
|
||||||
import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto";
|
import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto";
|
||||||
import { UserListQueryDto } from "../DTO/user-list-query.dto";
|
import { UserListQueryDto } from "../DTO/user-list-query.dto";
|
||||||
@@ -92,6 +93,85 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
/*******************************/
|
/*******************************/
|
||||||
|
|
||||||
|
async addPushToken(userId: string, addPushTokenDto: AddPushTokenDto) {
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (!user.pushTokens) {
|
||||||
|
user.pushTokens = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.pushTokens.includes(addPushTokenDto.pushToken)) {
|
||||||
|
user.pushTokens.push(addPushTokenDto.pushToken);
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
this.logger.log(`Push token added for user ${userId}. Total tokens: ${user.pushTokens.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: UserMessage.PUSH_TOKEN_ADDED_SUCCESSFULLY,
|
||||||
|
totalTokens: user.pushTokens.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************/
|
||||||
|
|
||||||
|
async removePushToken(userId: string, removePushTokenDto: RemovePushTokenDto) {
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (!user.pushTokens || user.pushTokens.length === 0) {
|
||||||
|
throw new BadRequestException(UserMessage.NO_PUSH_TOKENS_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialLength = user.pushTokens.length;
|
||||||
|
user.pushTokens = user.pushTokens.filter((token) => token !== removePushTokenDto.pushToken);
|
||||||
|
|
||||||
|
if (user.pushTokens.length === initialLength) {
|
||||||
|
throw new BadRequestException(UserMessage.PUSH_TOKEN_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
this.logger.log(`Push token removed for user ${userId}. Remaining tokens: ${user.pushTokens.length}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: UserMessage.PUSH_TOKEN_REMOVED_SUCCESSFULLY,
|
||||||
|
totalTokens: user.pushTokens.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************/
|
||||||
|
|
||||||
|
async getPushTokens(userId: string) {
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pushTokens: user.pushTokens || [],
|
||||||
|
totalTokens: user.pushTokens?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************/
|
||||||
|
|
||||||
|
async cleanupInvalidPushTokens(userId: string, invalidTokens: string[]) {
|
||||||
|
if (!invalidTokens || invalidTokens.length === 0) return;
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
||||||
|
if (!user || !user.pushTokens) return;
|
||||||
|
|
||||||
|
const initialLength = user.pushTokens.length;
|
||||||
|
user.pushTokens = user.pushTokens.filter((token) => !invalidTokens.includes(token));
|
||||||
|
|
||||||
|
if (user.pushTokens.length !== initialLength) {
|
||||||
|
await this.em.flush();
|
||||||
|
this.logger.log(`Cleaned up ${initialLength - user.pushTokens.length} invalid push tokens for user ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************/
|
||||||
|
|
||||||
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
|
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
|
||||||
const user = await em.findOne(User, { id }, { populate: ["role"] });
|
const user = await em.findOne(User, { id }, { populate: ["role"] });
|
||||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseIntercepto
|
|||||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
|
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
|
||||||
|
import { AddPushTokenDto, RemovePushTokenDto } from "./DTO/manage-push-token.dto";
|
||||||
import { UpdateEmailUserDto } from "./DTO/update-email-user.dto";
|
import { UpdateEmailUserDto } from "./DTO/update-email-user.dto";
|
||||||
import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto";
|
import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto";
|
||||||
import { UserListQueryDto } from "./DTO/user-list-query.dto";
|
import { UserListQueryDto } from "./DTO/user-list-query.dto";
|
||||||
@@ -24,6 +25,34 @@ export class UsersController {
|
|||||||
return this.usersService.getMe(userId);
|
return this.usersService.getMe(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch("profile")
|
||||||
|
@ApiOperation({ summary: "Update current user profile" })
|
||||||
|
@ApiResponse({ status: 200, description: "Current user updated successfully" })
|
||||||
|
updateProfile(@UserDec("id") userId: string, @Body() updateMeDto: UpdateUserProfileDto) {
|
||||||
|
return this.usersService.updateProfile(userId, updateMeDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("push-token")
|
||||||
|
@ApiOperation({ summary: "Add push notification token for current device" })
|
||||||
|
@ApiResponse({ status: 200, description: "Push token added successfully" })
|
||||||
|
addPushToken(@UserDec("id") userId: string, @Body() addPushTokenDto: AddPushTokenDto) {
|
||||||
|
return this.usersService.addPushToken(userId, addPushTokenDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("push-token")
|
||||||
|
@ApiOperation({ summary: "Remove push notification token for current device" })
|
||||||
|
@ApiResponse({ status: 200, description: "Push token removed successfully" })
|
||||||
|
removePushToken(@UserDec("id") userId: string, @Body() removePushTokenDto: RemovePushTokenDto) {
|
||||||
|
return this.usersService.removePushToken(userId, removePushTokenDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("push-tokens")
|
||||||
|
@ApiOperation({ summary: "Get all push tokens for current user" })
|
||||||
|
@ApiResponse({ status: 200, description: "Push tokens retrieved successfully" })
|
||||||
|
getPushTokens(@UserDec("id") userId: string) {
|
||||||
|
return this.usersService.getPushTokens(userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
@UseInterceptors(BusinessInterceptor)
|
@UseInterceptors(BusinessInterceptor)
|
||||||
@ApiOperation({ summary: "Update email user" })
|
@ApiOperation({ summary: "Update email user" })
|
||||||
@@ -32,13 +61,6 @@ export class UsersController {
|
|||||||
return this.usersService.updateEmailUser(paramDto.id, businessId, body);
|
return this.usersService.updateEmailUser(paramDto.id, businessId, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("profile")
|
|
||||||
@ApiOperation({ summary: "Update current user" })
|
|
||||||
@ApiResponse({ status: 200, description: "Current user updated successfully" })
|
|
||||||
updateProfile(@UserDec("id") userId: string, @Body() updateMeDto: UpdateUserProfileDto) {
|
|
||||||
return this.usersService.updateProfile(userId, updateMeDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseInterceptors(BusinessInterceptor)
|
@UseInterceptors(BusinessInterceptor)
|
||||||
@ApiOperation({ summary: "Create a new email user" })
|
@ApiOperation({ summary: "Create a new email user" })
|
||||||
|
|||||||
Reference in New Issue
Block a user