|
|
|
|
@@ -0,0 +1,132 @@
|
|
|
|
|
package com.cameleer.server.app.alerting.controller;
|
|
|
|
|
|
|
|
|
|
import com.cameleer.server.app.alerting.dto.AlertDto;
|
|
|
|
|
import com.cameleer.server.app.alerting.dto.BulkReadRequest;
|
|
|
|
|
import com.cameleer.server.app.alerting.dto.UnreadCountResponse;
|
|
|
|
|
import com.cameleer.server.app.alerting.notify.InAppInboxQuery;
|
|
|
|
|
import com.cameleer.server.app.web.EnvPath;
|
|
|
|
|
import com.cameleer.server.core.alerting.AlertInstance;
|
|
|
|
|
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
|
|
|
|
import com.cameleer.server.core.alerting.AlertReadRepository;
|
|
|
|
|
import com.cameleer.server.core.runtime.Environment;
|
|
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
|
|
import jakarta.validation.Valid;
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestParam;
|
|
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
|
|
|
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REST controller for the in-app alert inbox (env-scoped).
|
|
|
|
|
* VIEWER+ can read their own inbox; OPERATOR+ can ack any alert.
|
|
|
|
|
*/
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/api/v1/environments/{envSlug}/alerts")
|
|
|
|
|
@Tag(name = "Alerts Inbox", description = "In-app alert inbox, ack and read tracking (env-scoped)")
|
|
|
|
|
@PreAuthorize("hasAnyRole('VIEWER','OPERATOR','ADMIN')")
|
|
|
|
|
public class AlertController {
|
|
|
|
|
|
|
|
|
|
private static final int DEFAULT_LIMIT = 50;
|
|
|
|
|
|
|
|
|
|
private final InAppInboxQuery inboxQuery;
|
|
|
|
|
private final AlertInstanceRepository instanceRepo;
|
|
|
|
|
private final AlertReadRepository readRepo;
|
|
|
|
|
|
|
|
|
|
public AlertController(InAppInboxQuery inboxQuery,
|
|
|
|
|
AlertInstanceRepository instanceRepo,
|
|
|
|
|
AlertReadRepository readRepo) {
|
|
|
|
|
this.inboxQuery = inboxQuery;
|
|
|
|
|
this.instanceRepo = instanceRepo;
|
|
|
|
|
this.readRepo = readRepo;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping
|
|
|
|
|
public List<AlertDto> list(
|
|
|
|
|
@EnvPath Environment env,
|
|
|
|
|
@RequestParam(defaultValue = "50") int limit) {
|
|
|
|
|
String userId = currentUserId();
|
|
|
|
|
int effectiveLimit = Math.min(limit, 200);
|
|
|
|
|
return inboxQuery.listInbox(env.id(), userId, effectiveLimit)
|
|
|
|
|
.stream().map(AlertDto::from).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/unread-count")
|
|
|
|
|
public UnreadCountResponse unreadCount(@EnvPath Environment env) {
|
|
|
|
|
String userId = currentUserId();
|
|
|
|
|
long count = inboxQuery.countUnread(env.id(), userId);
|
|
|
|
|
return new UnreadCountResponse(count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/{id}")
|
|
|
|
|
public AlertDto get(@EnvPath Environment env, @PathVariable UUID id) {
|
|
|
|
|
AlertInstance instance = requireInstance(id, env.id());
|
|
|
|
|
return AlertDto.from(instance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/{id}/ack")
|
|
|
|
|
public AlertDto ack(@EnvPath Environment env, @PathVariable UUID id) {
|
|
|
|
|
AlertInstance instance = requireInstance(id, env.id());
|
|
|
|
|
String userId = currentUserId();
|
|
|
|
|
instanceRepo.ack(id, userId, Instant.now());
|
|
|
|
|
// Re-fetch to return fresh state
|
|
|
|
|
return AlertDto.from(instanceRepo.findById(id)
|
|
|
|
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/{id}/read")
|
|
|
|
|
public void read(@EnvPath Environment env, @PathVariable UUID id) {
|
|
|
|
|
requireInstance(id, env.id());
|
|
|
|
|
String userId = currentUserId();
|
|
|
|
|
readRepo.markRead(userId, id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/bulk-read")
|
|
|
|
|
public void bulkRead(@EnvPath Environment env,
|
|
|
|
|
@Valid @RequestBody BulkReadRequest req) {
|
|
|
|
|
String userId = currentUserId();
|
|
|
|
|
// filter to only instances in this env
|
|
|
|
|
List<UUID> filtered = req.instanceIds().stream()
|
|
|
|
|
.filter(instanceId -> instanceRepo.findById(instanceId)
|
|
|
|
|
.map(i -> i.environmentId().equals(env.id()))
|
|
|
|
|
.orElse(false))
|
|
|
|
|
.toList();
|
|
|
|
|
if (!filtered.isEmpty()) {
|
|
|
|
|
readRepo.bulkMarkRead(userId, filtered);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
private AlertInstance requireInstance(UUID id, UUID envId) {
|
|
|
|
|
AlertInstance instance = instanceRepo.findById(id)
|
|
|
|
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
|
|
|
|
|
"Alert not found: " + id));
|
|
|
|
|
if (!instance.environmentId().equals(envId)) {
|
|
|
|
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
|
|
|
|
"Alert not found in this environment: " + id);
|
|
|
|
|
}
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String currentUserId() {
|
|
|
|
|
var auth = SecurityContextHolder.getContext().getAuthentication();
|
|
|
|
|
if (auth == null || auth.getName() == null) {
|
|
|
|
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No authentication");
|
|
|
|
|
}
|
|
|
|
|
String name = auth.getName();
|
|
|
|
|
return name.startsWith("user:") ? name.substring(5) : name;
|
|
|
|
|
}
|
|
|
|
|
}
|