feat(sprint-6): Phase 6 — Notifications (WebSocket) + PWA
- WebSocket: Spring STOMP + SockJS, NotificationService, persistent notifications table - NotificationController: GET/PUT endpoints for notification management - Frontend: notification bell with unread badge, dropdown panel, real-time via STOMP - PWA: manifest.json, service worker (manual sw.js), offline page, install prompt - PWA icons (192+512), dark theme colors, standalone display - Full i18n (de/en) for notifications and PWA - Flyway V10 migration for notifications table - spring-boot-starter-websocket dependency added
This commit is contained in:
@@ -123,6 +123,11 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<!-- WebSocket (STOMP + SockJS for notifications) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.cannamanage.api.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket configuration — enables STOMP messaging over SockJS.
|
||||
* Clients connect to /ws, subscribe to /user/queue/notifications for personal notifications.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// Enable simple in-memory broker for /topic (broadcast) and /queue (user-specific)
|
||||
config.enableSimpleBroker("/topic", "/queue");
|
||||
// Prefix for @MessageMapping methods
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
// User-specific destination prefix
|
||||
config.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// WebSocket endpoint with SockJS fallback
|
||||
registry.addEndpoint("/ws")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package de.cannamanage.api.controller;
|
||||
|
||||
import de.cannamanage.domain.entity.Notification;
|
||||
import de.cannamanage.service.NotificationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* REST endpoints for notification management.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/notifications")
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationController {
|
||||
|
||||
private final NotificationService notificationService;
|
||||
|
||||
/**
|
||||
* Get current user's notifications (last 10, unread first).
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getNotifications(@AuthenticationPrincipal UserDetails user) {
|
||||
UUID userId = UUID.fromString(user.getUsername());
|
||||
List<Notification> notifications = notificationService.getRecentNotifications(userId);
|
||||
long unreadCount = notificationService.getUnreadCount(userId);
|
||||
|
||||
var items = notifications.stream().map(n -> Map.of(
|
||||
"id", (Object) n.getId(),
|
||||
"type", n.getType().name(),
|
||||
"title", n.getTitle(),
|
||||
"message", n.getMessage(),
|
||||
"link", n.getLink() != null ? n.getLink() : "",
|
||||
"read", n.isRead(),
|
||||
"createdAt", n.getCreatedAt().toString()
|
||||
)).toList();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"notifications", items,
|
||||
"unreadCount", unreadCount
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single notification as read.
|
||||
*/
|
||||
@PutMapping("/{id}/read")
|
||||
public ResponseEntity<Void> markAsRead(@PathVariable UUID id) {
|
||||
notificationService.markAsRead(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read.
|
||||
*/
|
||||
@PutMapping("/read-all")
|
||||
public ResponseEntity<Map<String, Object>> markAllAsRead(@AuthenticationPrincipal UserDetails user) {
|
||||
UUID userId = UUID.fromString(user.getUsername());
|
||||
int updated = notificationService.markAllAsRead(userId);
|
||||
return ResponseEntity.ok(Map.of("updated", updated));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
-- V10: Notifications table for real-time + persistent notification system
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
link VARCHAR(500),
|
||||
read BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notifications_user ON notifications(user_id, read, created_at DESC);
|
||||
CREATE INDEX idx_notifications_tenant ON notifications(tenant_id);
|
||||
Reference in New Issue
Block a user