Add system admin settings

This commit is contained in:
2026-01-09 20:23:12 +01:00
parent f14dc9d783
commit cc9eea9c08
8 changed files with 228 additions and 19 deletions

View File

@@ -1,36 +1,19 @@
package dev.kruhlmann.imgfloat.controller;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
import dev.kruhlmann.imgfloat.model.Settings;
import dev.kruhlmann.imgfloat.service.AuthorizationService;
import dev.kruhlmann.imgfloat.service.SettingsService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import java.io.IOException;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/settings")
@@ -60,6 +43,7 @@ public class SettingsApiController {
settingsService.logSettings("From: ", currentSettings);
settingsService.logSettings("To: ", newSettings);
return ResponseEntity.ok().body(newSettings);
Settings savedSettings = settingsService.save(newSettings);
return ResponseEntity.ok().body(savedSettings);
}
}

View File

@@ -0,0 +1,84 @@
package dev.kruhlmann.imgfloat.controller;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
import dev.kruhlmann.imgfloat.service.AuthorizationService;
import dev.kruhlmann.imgfloat.service.SystemAdministratorService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/system-administrators")
@SecurityRequirement(name = "administrator")
public class SystemAdministratorApiController {
private static final Logger LOG = LoggerFactory.getLogger(SystemAdministratorApiController.class);
private final SystemAdministratorService systemAdministratorService;
private final AuthorizationService authorizationService;
public SystemAdministratorApiController(
SystemAdministratorService systemAdministratorService,
AuthorizationService authorizationService
) {
this.systemAdministratorService = systemAdministratorService;
this.authorizationService = authorizationService;
}
@GetMapping
public ResponseEntity<List<String>> listSystemAdministrators(OAuth2AuthenticationToken oauthToken) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
authorizationService.userIsSystemAdministratorOrThrowHttpError(sessionUsername);
return ResponseEntity.ok().body(systemAdministratorService.listSysadmins());
}
@PostMapping
public ResponseEntity<List<String>> addSystemAdministrator(
@RequestBody SystemAdministratorRequest request,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
authorizationService.userIsSystemAdministratorOrThrowHttpError(sessionUsername);
if (request == null || request.twitchUsername() == null || request.twitchUsername().isBlank()) {
throw new ResponseStatusException(BAD_REQUEST, "Username is required");
}
String username = request.twitchUsername().trim();
systemAdministratorService.addSysadmin(username);
LOG.info("System administrator added: {} (requested by {})", username, sessionUsername);
return ResponseEntity.ok().body(systemAdministratorService.listSysadmins());
}
@DeleteMapping("/{twitchUsername}")
public ResponseEntity<List<String>> removeSystemAdministrator(
@PathVariable("twitchUsername") String twitchUsername,
OAuth2AuthenticationToken oauthToken
) {
String sessionUsername = OauthSessionUser.from(oauthToken).login();
authorizationService.userIsSystemAdministratorOrThrowHttpError(sessionUsername);
try {
systemAdministratorService.removeSysadmin(twitchUsername);
} catch (IllegalStateException e) {
throw new ResponseStatusException(BAD_REQUEST, e.getMessage(), e);
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(NOT_FOUND, e.getMessage(), e);
}
LOG.info("System administrator removed: {} (requested by {})", twitchUsername, sessionUsername);
return ResponseEntity.ok().body(systemAdministratorService.listSysadmins());
}
private record SystemAdministratorRequest(String twitchUsername) {}
}

View File

@@ -62,6 +62,7 @@ public class ViewController {
model.addAttribute("username", sessionUsername);
model.addAttribute("channel", sessionUsername);
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(sessionUsername));
model.addAttribute("isSystemAdmin", authorizationService.userIsSystemAdministrator(sessionUsername));
addVersionAttributes(model);
return "dashboard";
}

View File

@@ -1,9 +1,11 @@
package dev.kruhlmann.imgfloat.repository;
import dev.kruhlmann.imgfloat.model.SystemAdministrator;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SystemAdministratorRepository extends JpaRepository<SystemAdministrator, String> {
boolean existsByTwitchUsername(String twitchUsername);
long deleteByTwitchUsername(String twitchUsername);
List<SystemAdministrator> findAllByOrderByTwitchUsernameAsc();
}

View File

@@ -3,7 +3,9 @@ package dev.kruhlmann.imgfloat.service;
import dev.kruhlmann.imgfloat.model.SystemAdministrator;
import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository;
import jakarta.annotation.PostConstruct;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -80,6 +82,14 @@ public class SystemAdministratorService {
return repo.existsByTwitchUsername(normalize(twitchUsername));
}
public List<String> listSysadmins() {
return repo
.findAllByOrderByTwitchUsernameAsc()
.stream()
.map(SystemAdministrator::getTwitchUsername)
.collect(Collectors.toList());
}
private String normalize(String username) {
return username.trim().toLowerCase(Locale.ROOT);
}