mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Add account deletion
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package dev.kruhlmann.imgfloat.controller;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.service.AccountService;
|
||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/account")
|
||||
public class AccountApiController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AccountApiController.class);
|
||||
|
||||
private final AccountService accountService;
|
||||
|
||||
public AccountApiController(AccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public void deleteAccount(
|
||||
OAuth2AuthenticationToken oauthToken,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
LOG.info("Deleting account for {}", logSessionUsername);
|
||||
accountService.deleteAccount(sessionUsername);
|
||||
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null) {
|
||||
new SecurityContextLogoutHandler().logout(request, response, auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,6 @@ public interface MarketplaceScriptHeartRepository
|
||||
long countByScriptId(String scriptId);
|
||||
|
||||
void deleteByScriptIdAndUsername(String scriptId, String username);
|
||||
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetFile;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScriptAssetFileRepository extends JpaRepository<ScriptAssetFile, String> {}
|
||||
public interface ScriptAssetFileRepository extends JpaRepository<ScriptAssetFile, String> {
|
||||
List<ScriptAssetFile> findByBroadcaster(String broadcaster);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.Asset;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetFile;
|
||||
import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class AccountService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AccountService.class);
|
||||
|
||||
private final ChannelDirectoryService channelDirectoryService;
|
||||
private final AssetRepository assetRepository;
|
||||
private final ChannelRepository channelRepository;
|
||||
private final ScriptAssetFileRepository scriptAssetFileRepository;
|
||||
private final MarketplaceScriptHeartRepository marketplaceScriptHeartRepository;
|
||||
private final SystemAdministratorRepository systemAdministratorRepository;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AccountService(
|
||||
ChannelDirectoryService channelDirectoryService,
|
||||
AssetRepository assetRepository,
|
||||
ChannelRepository channelRepository,
|
||||
ScriptAssetFileRepository scriptAssetFileRepository,
|
||||
MarketplaceScriptHeartRepository marketplaceScriptHeartRepository,
|
||||
SystemAdministratorRepository systemAdministratorRepository,
|
||||
AssetStorageService assetStorageService,
|
||||
JdbcTemplate jdbcTemplate
|
||||
) {
|
||||
this.channelDirectoryService = channelDirectoryService;
|
||||
this.assetRepository = assetRepository;
|
||||
this.channelRepository = channelRepository;
|
||||
this.scriptAssetFileRepository = scriptAssetFileRepository;
|
||||
this.marketplaceScriptHeartRepository = marketplaceScriptHeartRepository;
|
||||
this.systemAdministratorRepository = systemAdministratorRepository;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAccount(String username) {
|
||||
String normalized = normalize(username);
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> assetIds = assetRepository
|
||||
.findByBroadcaster(normalized)
|
||||
.stream()
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
assetIds.forEach(channelDirectoryService::deleteAsset);
|
||||
|
||||
List<ScriptAssetFile> scriptFiles = scriptAssetFileRepository.findByBroadcaster(normalized);
|
||||
scriptFiles.forEach(this::deleteScriptAssetFile);
|
||||
|
||||
marketplaceScriptHeartRepository.deleteByUsername(normalized);
|
||||
systemAdministratorRepository.deleteByTwitchUsername(normalized);
|
||||
channelRepository.deleteById(normalized);
|
||||
|
||||
deleteSessionsForUser(normalized);
|
||||
LOG.info("Account data deleted for {}", normalized);
|
||||
}
|
||||
|
||||
private void deleteScriptAssetFile(ScriptAssetFile file) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
assetStorageService.deleteAsset(file.getBroadcaster(), file.getId(), file.getMediaType(), false);
|
||||
scriptAssetFileRepository.delete(file);
|
||||
}
|
||||
|
||||
private void deleteSessionsForUser(String username) {
|
||||
jdbcTemplate.update(
|
||||
"DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE SESSION_PRIMARY_ID IN (" +
|
||||
"SELECT PRIMARY_ID FROM SPRING_SESSION WHERE PRINCIPAL_NAME = ?)",
|
||||
username
|
||||
);
|
||||
jdbcTemplate.update("DELETE FROM SPRING_SESSION WHERE PRINCIPAL_NAME = ?", username);
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const elements = {
|
||||
allowScriptChat: document.getElementById("allow-script-chat"),
|
||||
scriptSettingsStatus: document.getElementById("script-settings-status"),
|
||||
scriptSettingsSaveButton: document.getElementById("save-script-settings-btn"),
|
||||
deleteAccountButton: document.getElementById("delete-account-btn"),
|
||||
};
|
||||
|
||||
const apiBase = `/api/channels/${encodeURIComponent(broadcaster)}`;
|
||||
@@ -307,6 +308,31 @@ async function saveScriptSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
const confirmation = window.prompt(
|
||||
"Type DELETE to permanently remove your account, assets, and session.",
|
||||
);
|
||||
if (confirmation !== "DELETE") {
|
||||
if (confirmation !== null) {
|
||||
showToast("Account deletion cancelled.", "info");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setButtonBusy(elements.deleteAccountButton, true, "Deleting...");
|
||||
try {
|
||||
const response = await fetch("/api/account", { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
throw new Error("Delete account failed");
|
||||
}
|
||||
showToast("Account deleted. Redirecting...", "success");
|
||||
window.location.href = "/";
|
||||
} catch (error) {
|
||||
showToast("Unable to delete account right now. Please retry.", "error");
|
||||
setButtonBusy(elements.deleteAccountButton, false, "Deleting...");
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.adminInput) {
|
||||
elements.adminInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
@@ -320,3 +346,7 @@ fetchAdmins();
|
||||
fetchSuggestedAdmins();
|
||||
fetchCanvasSettings();
|
||||
fetchScriptSettings();
|
||||
|
||||
if (elements.deleteAccountButton) {
|
||||
elements.deleteAccountButton.addEventListener("click", deleteAccount);
|
||||
}
|
||||
|
||||
@@ -161,6 +161,23 @@
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card dashboard-span-full">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Account</p>
|
||||
<h3>Delete account</h3>
|
||||
<p class="muted">
|
||||
Permanently remove your account, assets, and session. This cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-actions">
|
||||
<button id="delete-account-btn" class="secondary danger" type="button">
|
||||
Delete my account
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/cookie-consent.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user