From 57864e00da0e1bd35b27a567d20debd8f752eae9 Mon Sep 17 00:00:00 2001 From: echo Date: Sun, 22 Feb 2026 04:00:20 +0100 Subject: [PATCH] push notification fix implemented --- backend/src/modules/push/push.controller.ts | 33 +- backend/src/modules/push/push.dto.ts | 20 +- backend/src/modules/push/push.service.ts | 14 +- .../admin/PushNotificationManager.tsx | 166 + .../routes/AdminDashboardComponent.tsx | 27 +- frontend/src/lib/api.ts | 38 + frontend/src/queries/push.ts | 20 + pwa/dev-dist/registerSW.js | 1 + pwa/dev-dist/sw.js | 101 + pwa/dev-dist/workbox-d975e299.js | 4612 +++++++++++++++++ pwa/src/components/ui/notification-banner.tsx | 15 +- pwa/src/hooks/usePushNotifications.ts | 53 +- pwa/src/lib/push-api.ts | 31 +- pwa/src/main.tsx | 20 +- pwa/vite.config.ts | 1 + 15 files changed, 5126 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/admin/PushNotificationManager.tsx create mode 100644 frontend/src/queries/push.ts create mode 100644 pwa/dev-dist/registerSW.js create mode 100644 pwa/dev-dist/sw.js create mode 100644 pwa/dev-dist/workbox-d975e299.js diff --git a/backend/src/modules/push/push.controller.ts b/backend/src/modules/push/push.controller.ts index cc0d1f8..042234a 100644 --- a/backend/src/modules/push/push.controller.ts +++ b/backend/src/modules/push/push.controller.ts @@ -6,12 +6,17 @@ import { Body, HttpCode, HttpStatus, + UseGuards, } from '@nestjs/common'; import { PushService } from './push.service'; -import { SubscribeDto, UnsubscribeDto } from './push.dto'; +import { SubscribeDto, UnsubscribeDto, SendNotificationDto } from './push.dto'; import { Public } from '../auth/public.decorator'; +import { Roles } from '../auth/roles.decorator'; +import { RolesGuard } from '../auth/roles.guard'; +import { UserRole } from '../entities'; @Controller('push') +@UseGuards(RolesGuard) export class PushController { constructor(private readonly pushService: PushService) {} @@ -25,6 +30,12 @@ export class PushController { @Post('subscribe') @HttpCode(HttpStatus.CREATED) async subscribe(@Body() dto: SubscribeDto): Promise<{ success: boolean }> { + console.log('Received push subscription:', { + endpoint: dto.endpoint?.substring(0, 50) + '...', + hasP256dh: !!dto.p256dh, + hasAuth: !!dto.auth, + userId: dto.userId, + }); await this.pushService.subscribe(dto); return { success: true }; } @@ -35,4 +46,24 @@ export class PushController { async unsubscribe(@Body() dto: UnsubscribeDto): Promise { await this.pushService.unsubscribe(dto); } + + @Roles(UserRole.ADMIN) + @Get('stats') + getStats(): Promise<{ totalSubscribers: number }> { + return this.pushService.getStats(); + } + + @Roles(UserRole.ADMIN, UserRole.CONTRIBUTOR) + @Post('send') + @HttpCode(HttpStatus.OK) + async sendNotification( + @Body() dto: SendNotificationDto, + ): Promise<{ sent: number; failed: number }> { + return this.pushService.sendToAll({ + title: dto.title, + body: dto.body, + url: dto.url, + tag: 'admin-notification', + }); + } } diff --git a/backend/src/modules/push/push.dto.ts b/backend/src/modules/push/push.dto.ts index 25f95b5..d440462 100644 --- a/backend/src/modules/push/push.dto.ts +++ b/backend/src/modules/push/push.dto.ts @@ -1,7 +1,7 @@ -import { IsString, IsNotEmpty, IsOptional, IsUrl } from 'class-validator'; +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; export class SubscribeDto { - @IsUrl() + @IsString() @IsNotEmpty() endpoint: string; @@ -19,7 +19,21 @@ export class SubscribeDto { } export class UnsubscribeDto { - @IsUrl() + @IsString() @IsNotEmpty() endpoint: string; } + +export class SendNotificationDto { + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + @IsNotEmpty() + body: string; + + @IsOptional() + @IsString() + url?: string; +} diff --git a/backend/src/modules/push/push.service.ts b/backend/src/modules/push/push.service.ts index 2253fe4..e4a8593 100644 --- a/backend/src/modules/push/push.service.ts +++ b/backend/src/modules/push/push.service.ts @@ -49,11 +49,15 @@ export class PushService implements OnModuleInit { } async subscribe(dto: SubscribeDto): Promise { + this.logger.log('Subscribing push notification...'); + this.logger.debug(`Endpoint: ${dto.endpoint?.substring(0, 50)}...`); + const existing = await this.subscriptionRepo.findOne({ where: { endpoint: dto.endpoint }, }); if (existing) { + this.logger.log('Subscription already exists, updating...'); if (dto.userId && existing.userId !== dto.userId) { existing.userId = dto.userId; return this.subscriptionRepo.save(existing); @@ -61,6 +65,7 @@ export class PushService implements OnModuleInit { return existing; } + this.logger.log('Creating new subscription...'); const subscription = this.subscriptionRepo.create({ endpoint: dto.endpoint, p256dh: dto.p256dh, @@ -68,7 +73,9 @@ export class PushService implements OnModuleInit { userId: dto.userId ?? null, }); - return this.subscriptionRepo.save(subscription); + const saved = await this.subscriptionRepo.save(subscription); + this.logger.log(`Subscription saved with ID: ${saved.id}`); + return saved; } async unsubscribe(dto: UnsubscribeDto): Promise { @@ -170,4 +177,9 @@ export class PushService implements OnModuleInit { tag: `live-blog-${blogSlug}`, }); } + + async getStats(): Promise<{ totalSubscribers: number }> { + const totalSubscribers = await this.subscriptionRepo.count(); + return { totalSubscribers }; + } } diff --git a/frontend/src/components/admin/PushNotificationManager.tsx b/frontend/src/components/admin/PushNotificationManager.tsx new file mode 100644 index 0000000..47c47da --- /dev/null +++ b/frontend/src/components/admin/PushNotificationManager.tsx @@ -0,0 +1,166 @@ +import { useState } from 'react'; +import { usePushStats, useSendPushNotification } from '@/queries/push'; +import { Button } from '@/components/ui/button'; +import { Bell, Send, Users, AlertCircle, CheckCircle } from 'lucide-react'; + +export function PushNotificationManager() { + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [url, setUrl] = useState(''); + const [showSuccess, setShowSuccess] = useState(false); + const [result, setResult] = useState<{ sent: number; failed: number } | null>( + null, + ); + + const { data: stats, isLoading: loadingStats } = usePushStats(); + const sendMutation = useSendPushNotification(); + + const handleSendTest = async () => { + if (!title.trim() || !body.trim()) return; + + try { + const result = await sendMutation.mutateAsync({ + title: title.trim(), + body: body.trim(), + url: url.trim() || undefined, + }); + setResult(result); + setShowSuccess(true); + setTimeout(() => setShowSuccess(false), 5000); + } catch (error) { + console.error('Failed to send notification:', error); + } + }; + + const isValid = title.trim().length > 0 && body.trim().length > 0; + + return ( +
+
+ +

Push Notifications

+
+ +
+
+
+
+ + + Subscribers + +
+ {loadingStats ? ( +
...
+ ) : ( +
+ {stats?.totalSubscribers ?? 0} +
+ )} +

+ Active push subscribers +

+
+
+ +
+
+

+ Send Notification +

+ +
+
+ + setTitle(e.target.value)} + placeholder="Enter notification title..." + className="w-full border-2 border-foreground bg-background px-3 py-2 font-body text-sm focus:border-accent focus:outline-none" + maxLength={50} + /> +
+ +
+ +