Temo commit

This commit is contained in:
2026-01-09 21:15:42 +01:00
parent cc9eea9c08
commit e8b9c31688
4 changed files with 62 additions and 14 deletions

View File

@@ -11,6 +11,7 @@ import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
import dev.kruhlmann.imgfloat.service.GitInfoService; import dev.kruhlmann.imgfloat.service.GitInfoService;
import dev.kruhlmann.imgfloat.service.GithubReleaseService; import dev.kruhlmann.imgfloat.service.GithubReleaseService;
import dev.kruhlmann.imgfloat.service.SettingsService; import dev.kruhlmann.imgfloat.service.SettingsService;
import dev.kruhlmann.imgfloat.service.SystemAdministratorService;
import dev.kruhlmann.imgfloat.service.VersionService; import dev.kruhlmann.imgfloat.service.VersionService;
import dev.kruhlmann.imgfloat.util.LogSanitizer; import dev.kruhlmann.imgfloat.util.LogSanitizer;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -31,6 +32,7 @@ public class ViewController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final GithubReleaseService githubReleaseService; private final GithubReleaseService githubReleaseService;
private final SystemAdministratorService systemAdministratorService;
private final long uploadLimitBytes; private final long uploadLimitBytes;
public ViewController( public ViewController(
@@ -41,6 +43,7 @@ public class ViewController {
ObjectMapper objectMapper, ObjectMapper objectMapper,
AuthorizationService authorizationService, AuthorizationService authorizationService,
GithubReleaseService githubReleaseService, GithubReleaseService githubReleaseService,
SystemAdministratorService systemAdministratorService,
long uploadLimitBytes long uploadLimitBytes
) { ) {
this.channelDirectoryService = channelDirectoryService; this.channelDirectoryService = channelDirectoryService;
@@ -50,6 +53,7 @@ public class ViewController {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.githubReleaseService = githubReleaseService; this.githubReleaseService = githubReleaseService;
this.systemAdministratorService = systemAdministratorService;
this.uploadLimitBytes = uploadLimitBytes; this.uploadLimitBytes = uploadLimitBytes;
} }
@@ -111,6 +115,7 @@ public class ViewController {
LOG.error("Failed to serialize settings for settings view", e); LOG.error("Failed to serialize settings for settings view", e);
throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Failed to serialize settings"); throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Failed to serialize settings");
} }
model.addAttribute("initialSysadmin", systemAdministratorService.getInitialSysadmin());
return "settings"; return "settings";
} }

View File

@@ -8,7 +8,6 @@ import java.util.Locale;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -18,25 +17,18 @@ public class SystemAdministratorService {
private static final Logger logger = LoggerFactory.getLogger(SystemAdministratorService.class); private static final Logger logger = LoggerFactory.getLogger(SystemAdministratorService.class);
private final SystemAdministratorRepository repo; private final SystemAdministratorRepository repo;
private final String initialSysadmin;
private final Environment environment; private final Environment environment;
public SystemAdministratorService( public SystemAdministratorService(
SystemAdministratorRepository repo, SystemAdministratorRepository repo,
@Value("${IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN:#{null}}") String initialSysadmin,
Environment environment Environment environment
) { ) {
this.repo = repo; this.repo = repo;
this.initialSysadmin = initialSysadmin;
this.environment = environment; this.environment = environment;
} }
@PostConstruct @PostConstruct
public void initDefaults() { public void initDefaults() {
if (repo.count() > 0) {
return;
}
if ( if (
Boolean.parseBoolean( Boolean.parseBoolean(
environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper") environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper")
@@ -46,18 +38,34 @@ public class SystemAdministratorService {
return; return;
} }
String initialSysadmin = getInitialSysadmin();
if (initialSysadmin != null) {
long deleted = repo.deleteByTwitchUsername(initialSysadmin);
if (deleted > 0) {
logger.info("Removed persisted initial system administrator '{}' to use environment value", initialSysadmin);
}
}
if (repo.count() > 0) {
return;
}
if (initialSysadmin == null || initialSysadmin.isBlank()) { if (initialSysadmin == null || initialSysadmin.isBlank()) {
throw new IllegalStateException( throw new IllegalStateException(
"No system administrators exist and IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN is not set" "No system administrators exist and IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN is not set"
); );
} }
addSysadmin(initialSysadmin); logger.info("Using initial system administrator '{}' from environment", initialSysadmin);
logger.info("Created initial system administrator '{}'", initialSysadmin);
} }
public void addSysadmin(String twitchUsername) { public void addSysadmin(String twitchUsername) {
String normalized = normalize(twitchUsername); String normalized = normalize(twitchUsername);
String initialSysadmin = getInitialSysadmin();
if (initialSysadmin != null && initialSysadmin.equals(normalized)) {
return;
}
if (repo.existsByTwitchUsername(normalized)) { if (repo.existsByTwitchUsername(normalized)) {
return; return;
@@ -67,11 +75,18 @@ public class SystemAdministratorService {
} }
public void removeSysadmin(String twitchUsername) { public void removeSysadmin(String twitchUsername) {
if (repo.count() <= 1) { String normalized = normalize(twitchUsername);
String initialSysadmin = getInitialSysadmin();
if (initialSysadmin != null && initialSysadmin.equals(normalized)) {
throw new IllegalStateException("Cannot remove the initial system administrator");
}
if (initialSysadmin == null && repo.count() <= 1) {
throw new IllegalStateException("Cannot remove the last system administrator"); throw new IllegalStateException("Cannot remove the last system administrator");
} }
long deleted = repo.deleteByTwitchUsername(normalize(twitchUsername)); long deleted = repo.deleteByTwitchUsername(normalized);
if (deleted == 0) { if (deleted == 0) {
throw new IllegalArgumentException("System administrator does not exist"); throw new IllegalArgumentException("System administrator does not exist");
@@ -79,7 +94,20 @@ public class SystemAdministratorService {
} }
public boolean isSysadmin(String twitchUsername) { public boolean isSysadmin(String twitchUsername) {
return repo.existsByTwitchUsername(normalize(twitchUsername)); String normalized = normalize(twitchUsername);
String initialSysadmin = getInitialSysadmin();
if (initialSysadmin != null && initialSysadmin.equals(normalized)) {
return true;
}
return repo.existsByTwitchUsername(normalized);
}
public String getInitialSysadmin() {
String value = environment.getProperty("IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN");
if (value == null || value.isBlank()) {
return null;
}
return normalize(value);
} }
public List<String> listSysadmins() { public List<String> listSysadmins() {

View File

@@ -19,6 +19,10 @@ const sysadminInputElement = document.getElementById("new-sysadmin");
const addSysadminButtonElement = document.getElementById("add-sysadmin-button"); const addSysadminButtonElement = document.getElementById("add-sysadmin-button");
let currentSettings = JSON.parse(serverRenderedSettings); let currentSettings = JSON.parse(serverRenderedSettings);
const initialSysadmin =
typeof serverRenderedInitialSysadmin === "string" && serverRenderedInitialSysadmin.trim() !== ""
? serverRenderedInitialSysadmin.trim().toLowerCase()
: null;
let userSettings = { ...currentSettings }; let userSettings = { ...currentSettings };
function jsonEquals(a, b) { function jsonEquals(a, b) {
@@ -165,7 +169,7 @@ function renderSystemAdministrators(admins) {
}); });
} }
function loadSystemAdministrators() { async function loadSystemAdministrators() {
return fetch("/api/system-administrators") return fetch("/api/system-administrators")
.then((r) => { .then((r) => {
if (!r.ok) { if (!r.ok) {
@@ -254,3 +258,13 @@ setFormSettings(currentSettings);
updateStatCards(currentSettings); updateStatCards(currentSettings);
updateSubmitButtonDisabledState(); updateSubmitButtonDisabledState();
loadSystemAdministrators(); loadSystemAdministrators();
if (initialSysadmin) {
document.querySelectorAll("[data-sysadmin-remove]").forEach((button) => {
const username = button.getAttribute("data-sysadmin-username");
if (username && username.trim().toLowerCase() === initialSysadmin) {
button.disabled = true;
button.title = "The initial system administrator cannot be removed.";
}
});
}

View File

@@ -285,6 +285,7 @@
</div> </div>
<script th:inline="javascript"> <script th:inline="javascript">
const serverRenderedSettings = /*[[${settingsJson}]]*/; const serverRenderedSettings = /*[[${settingsJson}]]*/;
const serverRenderedInitialSysadmin = /*[[${initialSysadmin}]]*/;
</script> </script>
<script src="/js/cookie-consent.js"></script> <script src="/js/cookie-consent.js"></script>
<script src="/js/settings.js"></script> <script src="/js/settings.js"></script>