mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-23 14:40:49 +08:00
feat: add browser push notifications
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.PushSubscriptionService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/push")
|
||||
@RequiredArgsConstructor
|
||||
public class PushSubscriptionController {
|
||||
private final PushSubscriptionService pushSubscriptionService;
|
||||
@Value("${app.webpush.public-key}")
|
||||
private String publicKey;
|
||||
|
||||
@GetMapping("/public-key")
|
||||
public PublicKeyResponse getPublicKey() {
|
||||
PublicKeyResponse r = new PublicKeyResponse();
|
||||
r.setKey(publicKey);
|
||||
return r;
|
||||
}
|
||||
|
||||
@PostMapping("/subscribe")
|
||||
public void subscribe(@RequestBody SubscriptionRequest req, Authentication auth) {
|
||||
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PublicKeyResponse {
|
||||
private String key;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SubscriptionRequest {
|
||||
private String endpoint;
|
||||
private String p256dh;
|
||||
private String auth;
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/openisle/model/PushSubscription.java
Normal file
40
src/main/java/com/openisle/model/PushSubscription.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entity storing a browser push subscription for a user.
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "push_subscriptions")
|
||||
public class PushSubscription {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String endpoint;
|
||||
|
||||
@Column(nullable = false, length = 256)
|
||||
private String p256dh;
|
||||
|
||||
@Column(nullable = false, length = 256)
|
||||
private String auth;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PushSubscription;
|
||||
import com.openisle.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PushSubscriptionRepository extends JpaRepository<PushSubscription, Long> {
|
||||
List<PushSubscription> findByUser(User user);
|
||||
void deleteByUserAndEndpoint(User user, String endpoint);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import java.util.List;
|
||||
public class NotificationService {
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PushNotificationService pushNotificationService;
|
||||
|
||||
public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) {
|
||||
return createNotification(user, type, post, comment, approved, null, null, null);
|
||||
@@ -30,7 +31,9 @@ public class NotificationService {
|
||||
n.setFromUser(fromUser);
|
||||
n.setReactionType(reactionType);
|
||||
n.setContent(content);
|
||||
return notificationRepository.save(n);
|
||||
Notification saved = notificationRepository.save(n);
|
||||
pushNotificationService.sendNotification(user, "You have a new notification");
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.PushSubscription;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.PushSubscriptionRepository;
|
||||
import nl.martijndwars.webpush.Notification;
|
||||
import nl.martijndwars.webpush.PushService;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Security;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class PushNotificationService {
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final PushService pushService;
|
||||
|
||||
public PushNotificationService(PushSubscriptionRepository subscriptionRepository,
|
||||
@Value("${app.webpush.public-key}") String publicKey,
|
||||
@Value("${app.webpush.private-key}") String privateKey) throws GeneralSecurityException {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
this.pushService = new PushService(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public void sendNotification(User user, String payload) {
|
||||
List<PushSubscription> subs = subscriptionRepository.findByUser(user);
|
||||
for (PushSubscription sub : subs) {
|
||||
try {
|
||||
Notification notification = new Notification(sub.getEndpoint(), sub.getP256dh(), sub.getAuth(), payload);
|
||||
pushService.send(notification);
|
||||
} catch (GeneralSecurityException | IOException | JoseException | InterruptedException | java.util.concurrent.ExecutionException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.PushSubscription;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.PushSubscriptionRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PushSubscriptionService {
|
||||
private final PushSubscriptionRepository subscriptionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public void saveSubscription(String username, String endpoint, String p256dh, String auth) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
subscriptionRepository.deleteByUserAndEndpoint(user, endpoint);
|
||||
PushSubscription sub = new PushSubscription();
|
||||
sub.setUser(user);
|
||||
sub.setEndpoint(endpoint);
|
||||
sub.setP256dh(p256dh);
|
||||
sub.setAuth(auth);
|
||||
subscriptionRepository.save(sub);
|
||||
}
|
||||
|
||||
public List<PushSubscription> listByUser(User user) {
|
||||
return subscriptionRepository.findByUser(user);
|
||||
}
|
||||
}
|
||||
@@ -70,3 +70,7 @@ app.ai.format-limit=${AI_FORMAT_LIMIT:3}
|
||||
|
||||
# Website URL for emails and redirects
|
||||
app.website-url=${WEBSITE_URL:https://www.open-isle.com}
|
||||
|
||||
# Web push configuration
|
||||
app.webpush.public-key=${WEBPUSH_PUBLIC_KEY:}
|
||||
app.webpush.private-key=${WEBPUSH_PRIVATE_KEY:}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.PushSubscriptionService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(PushSubscriptionController.class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
class PushSubscriptionControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private PushSubscriptionService pushSubscriptionService;
|
||||
|
||||
@Test
|
||||
void subscribeEndpoint() throws Exception {
|
||||
mockMvc.perform(post("/api/push/subscribe")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"endpoint\":\"e\",\"p256dh\":\"p\",\"auth\":\"a\"}")
|
||||
.principal(new UsernamePasswordAuthenticationToken("u","p")))
|
||||
.andExpect(status().isOk());
|
||||
Mockito.verify(pushSubscriptionService).saveSubscription("u","e","p","a");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user