Add logging and toasts

This commit is contained in:
2025-12-10 14:01:59 +01:00
parent 8444f1873a
commit 53410dc235
10 changed files with 363 additions and 31 deletions

View File

@@ -10,6 +10,8 @@ import com.imgfloat.app.service.ChannelDirectoryService;
import com.imgfloat.app.service.TwitchUserLookupService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@@ -40,6 +42,7 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RequestMapping("/api/channels/{broadcaster}")
@SecurityRequirement(name = "twitchOAuth")
public class ChannelApiController {
private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class);
private final ChannelDirectoryService channelDirectoryService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final TwitchUserLookupService twitchUserLookupService;
@@ -58,7 +61,11 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureBroadcaster(broadcaster, login);
LOG.info("User {} adding admin {} to {}", login, request.getUsername(), broadcaster);
boolean added = channelDirectoryService.addAdmin(broadcaster, request.getUsername());
if (!added) {
LOG.info("User {} already admin for {} or could not be added", request.getUsername(), broadcaster);
}
return ResponseEntity.ok().body(added);
}
@@ -67,6 +74,7 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureBroadcaster(broadcaster, login);
LOG.debug("Listing admins for {} by {}", broadcaster, login);
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
List<String> admins = channel.getAdmins().stream()
.sorted(Comparator.naturalOrder())
@@ -91,6 +99,7 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureBroadcaster(broadcaster, login);
LOG.info("User {} removing admin {} from {}", login, username, broadcaster);
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username);
return ResponseEntity.ok().body(removed);
}
@@ -101,8 +110,10 @@ public class ChannelApiController {
String login = TwitchUser.from(authentication).login();
if (!channelDirectoryService.isBroadcaster(broadcaster, login)
&& !channelDirectoryService.isAdmin(broadcaster, login)) {
LOG.warn("Unauthorized asset listing attempt for {} by {}", broadcaster, login);
throw new ResponseStatusException(FORBIDDEN, "Not authorized");
}
LOG.info("Listing assets for {} requested by {}", broadcaster, login);
return channelDirectoryService.getAssetsForAdmin(broadcaster);
}
@@ -113,6 +124,7 @@ public class ChannelApiController {
@GetMapping("/canvas")
public CanvasSettingsRequest getCanvas(@PathVariable("broadcaster") String broadcaster) {
LOG.debug("Fetching canvas settings for {}", broadcaster);
return channelDirectoryService.getCanvasSettings(broadcaster);
}
@@ -122,6 +134,7 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureBroadcaster(broadcaster, login);
LOG.info("Updating canvas for {} by {}: {}x{}", broadcaster, login, request.getWidth(), request.getHeight());
return channelDirectoryService.updateCanvasSettings(broadcaster, request);
}
@@ -132,13 +145,16 @@ public class ChannelApiController {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
if (file == null || file.isEmpty()) {
LOG.warn("User {} attempted to upload empty file to {}", login, broadcaster);
throw new ResponseStatusException(BAD_REQUEST, "Asset file is required");
}
try {
LOG.info("User {} uploading asset {} to {}", login, file.getOriginalFilename(), broadcaster);
return channelDirectoryService.createAsset(broadcaster, file)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to read image"));
} catch (IOException e) {
LOG.error("Failed to process asset upload for {} by {}", broadcaster, login, e);
throw new ResponseStatusException(BAD_REQUEST, "Failed to process image", e);
}
}
@@ -150,9 +166,13 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
LOG.debug("Applying transform to asset {} on {} by {}", assetId, broadcaster, login);
return channelDirectoryService.updateTransform(broadcaster, assetId, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
.orElseThrow(() -> {
LOG.warn("Transform request for missing asset {} on {} by {}", assetId, broadcaster, login);
return new ResponseStatusException(NOT_FOUND, "Asset not found");
});
}
@PutMapping("/assets/{assetId}/visibility")
@@ -162,9 +182,13 @@ public class ChannelApiController {
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
LOG.info("Updating visibility for asset {} on {} by {} to hidden={} ", assetId, broadcaster, login, request.isHidden());
return channelDirectoryService.updateVisibility(broadcaster, assetId, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
.orElseThrow(() -> {
LOG.warn("Visibility update for missing asset {} on {} by {}", assetId, broadcaster, login);
return new ResponseStatusException(NOT_FOUND, "Asset not found");
});
}
@GetMapping("/assets/{assetId}/content")
@@ -179,6 +203,7 @@ public class ChannelApiController {
}
if (authorized) {
LOG.debug("Serving asset {} for broadcaster {} to authenticated user {}", assetId, broadcaster, authentication.getName());
return channelDirectoryService.getAssetContent(broadcaster, assetId)
.map(content -> ResponseEntity.ok()
.contentType(MediaType.parseMediaType(content.mediaType()))
@@ -201,13 +226,16 @@ public class ChannelApiController {
ensureAuthorized(broadcaster, login);
boolean removed = channelDirectoryService.deleteAsset(broadcaster, assetId);
if (!removed) {
LOG.warn("Attempt to delete missing asset {} on {} by {}", assetId, broadcaster, login);
throw new ResponseStatusException(NOT_FOUND, "Asset not found");
}
LOG.info("Asset {} deleted on {} by {}", assetId, broadcaster, login);
return ResponseEntity.ok().build();
}
private void ensureBroadcaster(String broadcaster, String login) {
if (!channelDirectoryService.isBroadcaster(broadcaster, login)) {
LOG.warn("Access denied for broadcaster-only action on {} by {}", broadcaster, login);
throw new ResponseStatusException(FORBIDDEN, "Only broadcasters can manage admins");
}
}
@@ -215,6 +243,7 @@ public class ChannelApiController {
private void ensureAuthorized(String broadcaster, String login) {
if (!channelDirectoryService.isBroadcaster(broadcaster, login)
&& !channelDirectoryService.isAdmin(broadcaster, login)) {
LOG.warn("Unauthorized access to channel {} by {}", broadcaster, login);
throw new ResponseStatusException(FORBIDDEN, "No permission for channel");
}
}

View File

@@ -1,6 +1,8 @@
package com.imgfloat.app.controller;
import com.imgfloat.app.service.ChannelDirectoryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -10,6 +12,7 @@ import static org.springframework.http.HttpStatus.FORBIDDEN;
@Controller
public class ViewController {
private static final Logger LOG = LoggerFactory.getLogger(ViewController.class);
private final ChannelDirectoryService channelDirectoryService;
public ViewController(ChannelDirectoryService channelDirectoryService) {
@@ -20,6 +23,7 @@ public class ViewController {
public String home(OAuth2AuthenticationToken authentication, Model model) {
if (authentication != null) {
String login = TwitchUser.from(authentication).login();
LOG.info("Rendering dashboard for {}", login);
model.addAttribute("username", login);
model.addAttribute("channel", login);
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(login));
@@ -35,8 +39,10 @@ public class ViewController {
String login = TwitchUser.from(authentication).login();
if (!channelDirectoryService.isBroadcaster(broadcaster, login)
&& !channelDirectoryService.isAdmin(broadcaster, login)) {
LOG.warn("Unauthorized admin console access attempt for {} by {}", broadcaster, login);
throw new ResponseStatusException(FORBIDDEN, "Not authorized for admin tools");
}
LOG.info("Rendering admin console for {} (requested by {})", broadcaster, login);
model.addAttribute("broadcaster", broadcaster.toLowerCase());
model.addAttribute("username", login);
return "admin";
@@ -45,6 +51,7 @@ public class ViewController {
@org.springframework.web.bind.annotation.GetMapping("/view/{broadcaster}/broadcast")
public String broadcastView(@org.springframework.web.bind.annotation.PathVariable("broadcaster") String broadcaster,
Model model) {
LOG.debug("Rendering broadcast overlay for {}", broadcaster);
model.addAttribute("broadcaster", broadcaster.toLowerCase());
return "broadcast";
}