fix(api): return JSON responses for admin settings + API key endpoints to prevent Tauri client parse errors (#5437)

# Description of Changes

```console
index-DsORDqQQ.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/admin/settings', method: 'PUT', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'S', "Successful"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'S', "Successful"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"PUT"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'S', \"Successful\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'S', \"Successful\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DsORDqQQ.js:124:22714)\n    at http://tauri.localhost/assets/index-DsORDqQQ.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/admin/settings"

index-DXbk7lbS.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/user/get-api-key', method: 'POST', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'a', "a72f6b26-1"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'a', "a72f6b26-1"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"POST"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DXbk7lbS.js:124:22714)\n    at http://tauri.localhost/assets/index-DXbk7lbS.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/user/get-api-key"
```


This pull request fixes a self-hosting issue where the Tauri HTTP client
fails with `Unexpected token ... is not valid JSON` because certain API
endpoints returned plain text responses.

## What was changed

- Updated `AdminSettingsController`:
- Changed `updateSettings` and `updateSettingsSection` to return
structured JSON objects instead of raw strings.
- Standardized success and error payloads using a `Map<String, Object>`
with keys like `message` and `error`.
- Updated `UserController`:
- Changed `/api/v1/user/get-api-key` and `/api/v1/user/update-api-key`
to return JSON objects (`{ "apiKey": "..." }`) and JSON error objects
instead of plain text.

## Why the change was made

- The Tauri client expects JSON responses and attempts to parse them.
Returning plain strings like `"Successful..."` or an API key string
causes JSON parsing to fail, resulting in network errors on self-hosted
setups.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
Ludy 2026-01-13 22:18:09 +01:00 committed by GitHub
parent 84ed1d7ecb
commit b266b50bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 582 additions and 79 deletions

View File

@ -32,16 +32,19 @@ public class SettingsController {
@AutoJobPostMapping("/update-enable-analytics")
@Hidden
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
public ResponseEntity<Map<String, Object>> updateApiKey(@RequestParam Boolean enabled)
throws IOException {
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body(
"Setting has already been set, To adjust please edit "
+ InstallationPathConfig.getSettingsPath());
Map.of(
"message",
"Setting has already been set, To adjust please edit "
+ InstallationPathConfig.getSettingsPath()));
}
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
applicationProperties.getSystem().setEnableAnalytics(enabled);
return ResponseEntity.ok("Updated");
return ResponseEntity.ok(Map.of("message", "Updated"));
}
@GetMapping("/get-endpoints-status")

View File

@ -163,12 +163,13 @@ public class AdminSettingsController {
responseCode = "500",
description = "Failed to save settings to configuration file")
})
public ResponseEntity<String> updateSettings(
public ResponseEntity<Map<String, Object>> updateSettings(
@Valid @RequestBody UpdateSettingsRequest request) {
try {
Map<String, Object> settings = request.getSettings();
if (settings == null || settings.isEmpty()) {
return ResponseEntity.badRequest().body("No settings provided to update");
return ResponseEntity.badRequest()
.body(Map.of("error", "No settings provided to update"));
}
int updatedCount = 0;
@ -178,7 +179,11 @@ public class AdminSettingsController {
if (!isValidSettingKey(key)) {
return ResponseEntity.badRequest()
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(key));
.body(
Map.of(
"error",
"Invalid setting key format: "
+ HtmlUtils.htmlEscape(key)));
}
log.info("Admin updating setting: {} = {}", key, value);
@ -191,22 +196,26 @@ public class AdminSettingsController {
}
return ResponseEntity.ok(
String.format(
"Successfully updated %d setting(s). Changes will take effect on"
+ " application restart.",
updatedCount));
Map.of(
"message",
String.format(
"Successfully updated %d setting(s). Changes will take effect on"
+ " application restart.",
updatedCount)));
} catch (IOException e) {
log.error("Failed to save settings to file: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", GENERIC_FILE_ERROR));
} catch (IllegalArgumentException e) {
log.error("Invalid setting key or value: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SETTING);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", GENERIC_INVALID_SETTING));
} catch (Exception e) {
log.error("Unexpected error while updating settings: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(GENERIC_SERVER_ERROR);
.body(Map.of("error", GENERIC_SERVER_ERROR));
}
}
@ -283,20 +292,23 @@ public class AdminSettingsController {
description = "Access denied - Admin role required"),
@ApiResponse(responseCode = "500", description = "Failed to save settings")
})
public ResponseEntity<String> updateSettingsSection(
public ResponseEntity<Map<String, Object>> updateSettingsSection(
@PathVariable String sectionName, @Valid @RequestBody Map<String, Object> sectionData) {
try {
if (sectionData == null || sectionData.isEmpty()) {
return ResponseEntity.badRequest().body("No section data provided to update");
return ResponseEntity.badRequest()
.body(Map.of("error", "No section data provided to update"));
}
if (!isValidSectionName(sectionName)) {
return ResponseEntity.badRequest()
.body(
"Invalid section name: "
+ HtmlUtils.htmlEscape(sectionName)
+ ". Valid sections: "
+ String.join(", ", VALID_SECTION_NAMES));
Map.of(
"error",
"Invalid section name: "
+ HtmlUtils.htmlEscape(sectionName)
+ ". Valid sections: "
+ String.join(", ", VALID_SECTION_NAMES)));
}
// Auto-enable premium features if license key is provided
@ -317,7 +329,11 @@ public class AdminSettingsController {
if (!isValidSettingKey(fullKey)) {
return ResponseEntity.badRequest()
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(fullKey));
.body(
Map.of(
"error",
"Invalid setting key format: "
+ HtmlUtils.htmlEscape(fullKey)));
}
log.info("Admin updating section setting: {} = {}", fullKey, value);
@ -331,21 +347,25 @@ public class AdminSettingsController {
String escapedSectionName = HtmlUtils.htmlEscape(sectionName);
return ResponseEntity.ok(
String.format(
"Successfully updated %d setting(s) in section '%s'. Changes will take"
+ " effect on application restart.",
updatedCount, escapedSectionName));
Map.of(
"message",
String.format(
"Successfully updated %d setting(s) in section '%s'. Changes will take"
+ " effect on application restart.",
updatedCount, escapedSectionName)));
} catch (IOException e) {
log.error("Failed to save section settings to file: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", GENERIC_FILE_ERROR));
} catch (IllegalArgumentException e) {
log.error("Invalid section data: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SECTION);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", GENERIC_INVALID_SECTION));
} catch (Exception e) {
log.error("Unexpected error while updating section settings: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(GENERIC_SERVER_ERROR);
.body(Map.of("error", GENERIC_SERVER_ERROR));
}
}
@ -453,7 +473,7 @@ public class AdminSettingsController {
description = "Access denied - Admin role required"),
@ApiResponse(responseCode = "500", description = "Failed to initiate restart")
})
public ResponseEntity<String> restartApplication() {
public ResponseEntity<Map<String, Object>> restartApplication() {
try {
log.warn("Admin initiated application restart");
@ -465,13 +485,18 @@ public class AdminSettingsController {
log.error("Cannot restart: not running from JAR (likely development mode)");
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(
"Restart not available in development mode. Please restart the application manually.");
Map.of(
"error",
"Restart not available in development mode. Please restart the application manually."));
}
if (helperJar == null || !Files.isRegularFile(helperJar)) {
log.error("Cannot restart: restart-helper.jar not found at expected location");
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("Restart helper not found. Please restart the application manually.");
.body(
Map.of(
"error",
"Restart helper not found. Cannot perform application restart."));
}
// Get current application arguments
@ -526,12 +551,17 @@ public class AdminSettingsController {
.start();
return ResponseEntity.ok(
"Application restart initiated. The server will be back online shortly.");
Map.of(
"message",
"Application restart initiated. The server will be back online shortly."));
} catch (Exception e) {
log.error("Failed to initiate restart: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to initiate application restart: " + e.getMessage());
.body(
Map.of(
"error",
"Failed to initiate application restart: " + e.getMessage()));
}
}

View File

@ -741,31 +741,35 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) {
public ResponseEntity<Map<String, String>> getApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "User not authenticated."));
}
String username = principal.getName();
String apiKey = userService.getApiKeyForUser(username);
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "API key not found for user."));
}
return ResponseEntity.ok(apiKey);
return ResponseEntity.ok(Map.of("apiKey", apiKey));
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/update-api-key")
public ResponseEntity<String> updateApiKey(Principal principal) {
public ResponseEntity<Map<String, String>> updateApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "User not authenticated."));
}
String username = principal.getName();
User user = userService.refreshApiKeyForUser(username);
String apiKey = user.getApiKey();
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "API key not found for user."));
}
return ResponseEntity.ok(apiKey);
return ResponseEntity.ok(Map.of("apiKey", apiKey));
}
/**

View File

@ -128,6 +128,9 @@ undoQuotaError = "Rückgängig nicht möglich: unzureichender Speicherplatz"
undoStorageError = "Rückgängig abgeschlossen, aber einige Dateien konnten nicht im Speicher gespeichert werden"
undoSuccess = "Vorgang erfolgreich rückgängig gemacht"
unsupported = "Nicht unterstützt"
discardRedactions = "Discard & Leave"
pendingRedactions = "You have unapplied redactions that will be lost."
pendingRedactionsTitle = "Unapplied Redactions"
[toolPanel]
placeholder = "Wählen Sie ein Tool, um zu starten"
@ -434,6 +437,13 @@ currentVersion = "Aktuelle Version"
latestVersion = "Neueste Version"
checkForUpdates = "Nach Updates suchen"
viewDetails = "Details anzeigen"
serverNeedsUpdate = "Server needs to be updated by administrator"
[settings.general.versionInfo]
description = "Desktop and server version details"
desktop = "Desktop Version"
server = "Server Version"
title = "Version Information"
[settings.hotkeys]
title = "Tastenkürzel"
@ -453,6 +463,25 @@ noShortcut = "Kein Kürzel festgelegt"
mac = "Fügen Sie ⌘ (Command), ⌥ (Option) oder einen anderen Modifikator in Ihr Kürzel ein."
windows = "Fügen Sie Strg, Alt oder einen anderen Modifikator in Ihr Kürzel ein."
[settings.security]
description = "Update your password to keep your account secure."
title = "Security"
[settings.security.password]
confirm = "Confirm new password"
confirmPlaceholder = "Re-enter your new password"
current = "Current password"
currentPlaceholder = "Enter your current password"
error = "Unable to update password. Please verify your current password and try again."
mismatch = "New passwords do not match."
new = "New password"
newPlaceholder = "Enter a new password"
required = "All fields are required."
ssoDisabled = "Password changes are managed by your identity provider."
subtitle = "Change your password. You will be logged out after updating."
success = "Password updated successfully. Please sign in again."
update = "Update password"
[update]
modalTitle = "Update verfügbar"
current = "Aktuelle Version"
@ -493,6 +522,11 @@ oldPassword = "Aktuelles Passwort"
newPassword = "Neues Passwort"
confirmNewPassword = "Neues Passwort bestätigen"
submit = "Änderung speichern"
changeUsername = "Update your username. You will be logged out after updating."
credsUpdated = "Account updated"
description = "Changes saved. Please log in again."
error = "Unable to update username. Please verify your password and try again."
ssoManaged = "Your account is managed by your identity provider."
[account]
title = "Kontoeinstellungen"
@ -514,6 +548,8 @@ property = "Eigenschaft"
webBrowserSettings = "Webbrowser-Einstellung"
syncToBrowser = "Synchronisiere Konto -> Browser"
syncToAccount = "Synchronisiere Konto <- Browser"
changeUsernameDescription = "Update your username. You will be logged out after updating."
newUsernamePlaceholder = "Enter your new username"
[adminUserSettings]
title = "Benutzerkontrolle"
@ -926,10 +962,16 @@ tags = "text,anmerkung,beschriftung"
title = "Text hinzufügen"
desc = "Beliebigen Text überall in Ihrem PDF hinzufügen"
[home.annotate]
desc = "Highlight, draw, add notes and shapes in the viewer"
tags = "annotate,highlight,draw"
title = "Annotate"
[landing]
addFiles = "Dateien hinzufügen"
uploadFromComputer = "Vom Computer hochladen"
openFromComputer = "Vom Computer öffnen"
mobileUpload = "Upload from Mobile"
[viewPdf]
tags = "anzeigen,lesen,kommentieren,text,bild"
@ -1245,6 +1287,21 @@ cbzOptions = "Optionen: CBZ zu PDF"
optimizeForEbook = "PDF für E-Book-Reader optimieren (verwendet Ghostscript)"
cbzOutputOptions = "Optionen: PDF zu CBZ"
cbzDpi = "DPI für Bildrendering"
cbrDpi = "DPI for image rendering"
cbrOptions = "CBR Options"
cbrOutputOptions = "PDF to CBR Options"
[convert.ebookOptions]
ebookOptions = "eBook to PDF Options"
ebookOptionsDesc = "Options for converting eBooks to PDF"
embedAllFonts = "Embed all fonts"
embedAllFontsDesc = "Embed all fonts from the eBook into the generated PDF"
includePageNumbers = "Include page numbers"
includePageNumbersDesc = "Add page numbers to the generated PDF"
includeTableOfContents = "Include table of contents"
includeTableOfContentsDesc = "Add a generated table of contents to the resulting PDF"
optimizeForEbookPdf = "Optimize for ebook readers"
optimizeForEbookPdfDesc = "Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)"
[imageToPdf]
tags = "konvertierung,img,jpg,bild,foto"
@ -1362,6 +1419,11 @@ add = "Anhang hinzufügen"
remove = "Anhang entfernen"
embed = "Anhang einbetten"
submit = "Anhänge hinzufügen"
convertToPdfA3b = "Convert to PDF/A-3b"
convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments"
convertToPdfA3bTooltip = "PDF/A-3b is an archival format ensuring long-term preservation. It allows embedding arbitrary file formats as attachments. Conversion requires Ghostscript and may take longer for large files."
convertToPdfA3bTooltipHeader = "About PDF/A-3b Conversion"
convertToPdfA3bTooltipTitle = "What it does"
[watermark]
title = "Wasserzeichen hinzufügen"
@ -2306,6 +2368,10 @@ saved = "Gespeichert"
label = "Unterschriftsbild hochladen"
placeholder = "Bilddatei auswählen"
hint = "Laden Sie ein PNG- oder JPG-Bild Ihrer Unterschrift hoch"
backgroundRemovalFailedMessage = "Could not remove the background from the image. Using original image instead."
backgroundRemovalFailedTitle = "Background removal failed"
processing = "Processing image..."
removeBackground = "Remove white background (make transparent)"
[sign.instructions]
title = "So fügen Sie eine Unterschrift hinzu"
@ -2376,6 +2442,11 @@ bullet2 = "Links funktionieren beim Anklicken weiterhin"
bullet3 = "Kommentare und Notizen bleiben sichtbar"
bullet4 = "Lesezeichen helfen weiterhin bei der Navigation"
[flatten.renderDpi]
help = "Leave blank to use the system default. Higher DPI sharpens output but increases processing time and file size."
label = "Rendering DPI (optional, recommended 150 DPI)"
placeholder = "e.g. 150"
[repair]
tags = "reparieren,wiederherstellen,korrigieren,wiederherstellen"
title = "Reparieren"
@ -2925,6 +2996,7 @@ header = "PDF zuschneiden"
submit = "Abschicken"
noFileSelected = "Wählen Sie eine PDF-Datei aus, um mit dem Zuschneiden zu beginnen"
reset = "Auf vollständiges PDF zurücksetzen"
autoCrop = "Auto-crop whitespace"
[crop.preview]
title = "Zuschneidebereich-Auswahl"
@ -3158,6 +3230,7 @@ automaticDesc = "Text basierend auf Suchbegriffen schwärzen"
manual = "Manuell"
manualDesc = "Klicken und ziehen zum Schwärzen bestimmter Bereiche"
manualComingSoon = "Manuelle Schwärzung kommt bald"
automaticDisabledTooltip = "Select files in the file manager to redact multiple files at once"
[redact.auto]
header = "Auto-Schwärzung"
@ -3225,6 +3298,24 @@ text = "Nur vollständige Wörter abgleichen, keine Teilübereinstimmungen. 'Joh
title = "In PDF-Bild konvertieren"
text = "Konvertiert das PDF nach der Schwärzung in ein bildbasiertes PDF. Dies stellt sicher, dass Text hinter Schwärzungskästen vollständig entfernt und nicht wiederherstellbar ist."
[redact.tooltip.manual.apply]
bullet1 = "Mark as many areas as needed before applying"
bullet2 = "All pending redactions are applied at once"
bullet3 = "Redactions cannot be undone after applying"
text = "After marking content, click 'Apply' to permanently redact all marked areas. The pending count shows how many redactions are ready to be applied."
title = "Apply Redactions"
[redact.tooltip.manual.header]
title = "Manual Redaction Controls"
[redact.tooltip.manual.markArea]
text = "Draw rectangular areas on the PDF to mark regions for redaction. Useful for redacting images, signatures, or irregular shapes."
title = "Mark Area Tool"
[redact.tooltip.manual.markText]
text = "Select text directly on the PDF to mark it for redaction. Click and drag to highlight specific text that you want to redact."
title = "Mark Text Tool"
[redact.manual]
header = "Manuelle Schwärzung"
textBasedRedaction = "Textbasierte Schwärzung"
@ -3246,6 +3337,15 @@ showLayers = "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzusta
colourPicker = "Farbwähler"
findCurrentOutlineItem = "Aktuelles Gliederungselement finden"
applyChanges = "Änderungen anwenden"
apply = "Apply"
applyWarning = "⚠️ Permanent application, cannot be undone and the data underneath will be deleted"
controlsTitle = "Manual Redaction Controls"
instructions = "Select text or draw areas on the PDF to mark content for redaction."
markArea = "Mark Area"
markText = "Mark Text"
noMarks = "No redaction marks. Use the tools above to mark content for redaction."
pendingLabel = "Pending:"
title = "Redaction Tools"
[redact.manual.pageRedactionNumbers]
title = "Seiten"
@ -3342,6 +3442,19 @@ placeholder = "Anzahl horizontaler Teiler eingeben"
label = "Vertikale Teiler"
placeholder = "Anzahl vertikaler Teiler eingeben"
[split-by-sections.customPages]
label = "Custom Page Numbers"
placeholder = "e.g. 2,4,6"
[split-by-sections.splitMode]
custom = "Custom pages"
description = "Choose how to split the pages"
label = "Split Mode"
splitAll = "Split all pages"
splitAllExceptFirst = "Split all except first"
splitAllExceptFirstAndLast = "Split all except first and last"
splitAllExceptLast = "Split all except last"
[AddStampRequest]
tags = "stempeln,bild hinzufügen,bild zentrieren,wasserzeichen,pdf,einbetten,anpassen"
header = "PDF Stempel"
@ -3720,6 +3833,10 @@ bullet2 = "Höhere Werte reduzieren die Dateigröße"
title = "Graustufen"
text = "Wählen Sie diese Option, um alle Bilder in Schwarz-Weiß zu konvertieren, was die Dateigröße erheblich reduzieren kann, insbesondere bei gescannten PDFs oder bildreichen Dokumenten."
[compress.tooltip.lineArt]
text = "Convert pages to high-contrast black and white using ImageMagick. Use detail level to control how much content becomes black, and edge emphasis to control how aggressively edges are detected."
title = "Line Art"
[compress.error]
failed = "Ein Fehler ist beim Komprimieren der PDF aufgetreten."
@ -3732,6 +3849,24 @@ failed = "Ein Fehler ist beim Komprimieren der PDF aufgetreten."
_value = "Kompressionseinstellungen"
1 = "1-3 PDF-Komprimierung, </br> 4-6 Leichte Bildkomprimierung, </br> 7-9 Intensive Bildkomprimierung verringert die Bildqualität dramatisch"
[compress.compressionLevel]
range1to3 = "Lower values preserve quality but result in larger files"
range4to6 = "Medium compression with moderate quality reduction"
range7to9 = "Higher values reduce file size significantly but may reduce image clarity"
[compress.lineArt]
description = "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction."
detailLevel = "Detail level"
edgeEmphasis = "Edge emphasis"
edgeHigh = "Strong"
edgeLow = "Gentle"
edgeMedium = "Balanced"
label = "Convert images to line art"
unavailable = "ImageMagick is not installed or enabled on this server"
[compress.linearize]
label = "Linearize PDF for fast web viewing"
[decrypt]
passwordPrompt = "Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:"
cancelled = "Vorgang für PDF abgebrochen: {0}"
@ -3983,11 +4118,14 @@ rotateRight = "Nach rechts drehen"
toggleSidebar = "Seitenleiste umschalten"
exportSelected = "Ausgewählte Seiten exportieren"
toggleAnnotations = "Anmerkungen ein-/ausblenden"
annotationMode = "Anmerkungsmodus umschalten"
print = "PDF drucken"
draw = "Zeichnen"
save = "Speichern"
saveChanges = "Änderungen speichern"
annotations = "Annotations"
applyRedactionsFirst = "Apply redactions first"
exitRedaction = "Exit Redaction Mode"
redact = "Redact"
[search]
title = "PDF durchsuchen"
@ -4038,12 +4176,20 @@ settings = "Optionen"
adminSettings = "Admin Optionen"
allTools = "Werkzeuge"
reader = "Reader"
showMeAround = "Show me around"
tours = "Tours"
[quickAccess.helpMenu]
toolsTour = "Tool-Tour"
toolsTourDesc = "Erfahren Sie, was die Tools können"
adminTour = "Admin-Tour"
adminTourDesc = "Entdecken Sie Admin-Einstellungen & Funktionen"
whatsNewTour = "See what's new in V2"
whatsNewTourDesc = "Tour the updated layout"
[quickAccess.toursTooltip]
admin = "Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour."
user = "Watch walkthroughs here: Tools tour and the New V2 layout tour."
[admin]
error = "Fehler"
@ -4069,6 +4215,8 @@ loginRequired = "Der Anmeldemodus muss aktiviert sein, um Admin-Einstellungen zu
restarting = "Server wird neu gestartet"
restartingMessage = "Der Server wird neu gestartet. Bitte einen Moment warten..."
restartError = "Server konnte nicht neu gestartet werden. Bitte manuell neu starten."
error = "Failed to save settings"
success = "Settings saved successfully"
[admin.settings.unsavedChanges]
title = "Ungespeicherte Änderungen"
@ -4185,6 +4333,10 @@ description = "Pfad zur WeasyPrint-Ausführungsdatei für HTML-zu-PDF-Konvertier
label = "Unoconvert-Ausführbare Datei"
description = "Pfad zu LibreOffice unoconvert für Dokumentkonvertierungen (leer lassen für Standard: /opt/venv/bin/unoconvert)"
[admin.settings.general.frontendUrl]
description = "Base URL for frontend (e.g., https://pdf.example.com). Used for email invite links and mobile QR code uploads. Leave empty to use backend URL."
label = "Frontend URL"
[admin.settings.security]
title = "Sicherheit"
description = "Authentifizierung, Anmeldeverhalten und Sicherheitsrichtlinien konfigurieren."
@ -4321,6 +4473,19 @@ connect = "Verbinden"
disconnect = "Trennen"
disconnected = "Anbieter erfolgreich getrennt"
disconnectError = "Anbieter konnte nicht getrennt werden"
imageResolutionFull = "Full (Original Size)"
imageResolutionReduced = "Reduced (Max 1200px)"
mobileScannerConvertToPdf = "Convert Images to PDF"
mobileScannerConvertToPdfDesc = "Automatically convert uploaded images to PDF format. If disabled, images will be kept as-is."
mobileScannerImageResolution = "Image Resolution"
mobileScannerImageResolutionDesc = "Resolution of uploaded images. \"Reduced\" scales images to max 1200px to reduce file size."
mobileScannerPageFormat = "Page Format"
mobileScannerPageFormatDesc = "PDF page size for converted images. \"Keep\" uses original image dimensions."
mobileScannerStretchToFit = "Stretch to Fit"
mobileScannerStretchToFitDesc = "Stretch images to fill the entire page. If disabled, images are centered with preserved aspect ratio."
pageFormatA4 = "A4 (210×297mm)"
pageFormatKeep = "Keep (Original Dimensions)"
pageFormatLetter = "Letter (8.5×11in)"
[admin.settings.connections.ssoAutoLogin]
label = "SSO-Autoanmeldung"
@ -4389,6 +4554,26 @@ description = "Benutzerkonten bei der ersten SAML2-Anmeldung automatisch erstell
label = "Registrierung blockieren"
description = "Neue Benutzerregistrierung über SAML2 verhindern"
[admin.settings.connections.mobileScanner]
description = "Allow users to upload files from mobile devices by scanning a QR code"
enable = "Enable QR Code Upload"
imageResolutionFull = "Full (Original Size)"
imageResolutionReduced = "Reduced (Max 1200px)"
label = "Mobile Phone Upload"
link = "Configure in System Settings"
mobileScannerConvertToPdf = "Convert Images to PDF"
mobileScannerConvertToPdfDesc = "Automatically convert uploaded images to PDF format. If disabled, images will be kept as-is."
mobileScannerImageResolution = "Image Resolution"
mobileScannerImageResolutionDesc = "Resolution of uploaded images. \"Reduced\" scales images to max 1200px to reduce file size."
mobileScannerPageFormat = "Page Format"
mobileScannerPageFormatDesc = "PDF page size for converted images. \"Keep\" uses original image dimensions."
mobileScannerStretchToFit = "Stretch to Fit"
mobileScannerStretchToFitDesc = "Stretch images to fill the entire page. If disabled, images are centered with preserved aspect ratio."
note = "Note: Requires Frontend URL to be configured. "
pageFormatA4 = "A4 (210×297mm)"
pageFormatKeep = "Keep (Original Dimensions)"
pageFormatLetter = "Letter (8.5×11in)"
[admin.settings.database]
title = "Datenbank"
description = "Benutzerdefinierte Datenbankverbindungseinstellungen für Enterprise-Bereitstellungen konfigurieren."
@ -4570,6 +4755,10 @@ description = "Admins erlauben, Benutzer per E-Mail mit automatisch generierten
label = "Frontend-URL"
description = "Basis-URL für das Frontend (z. B. https://pdf.example.com). Wird zum Erzeugen von Einladungslinks in E-Mails verwendet. Leer lassen, um Backend-URL zu verwenden."
[admin.settings.mail.frontendUrlNote]
link = "Configure in System Settings"
note = "Note: Requires Frontend URL to be configured. "
[admin.settings.legal]
title = "Rechtliche Dokumente"
description = "Links zu rechtlichen Dokumenten und Richtlinien konfigurieren."
@ -4685,6 +4874,9 @@ description = "Einzelne zu deaktivierende Endpunkte auswählen"
label = "Deaktivierte Endpunktgruppen"
description = "Zu deaktivierende Endpunktgruppen auswählen"
[admin.settings.badge]
clickToUpgrade = "Click to view plan details"
[fileUpload]
selectFile = "Datei auswählen"
selectFiles = "Dateien auswählen"
@ -4784,6 +4976,9 @@ showAll = "Alle anzeigen"
sortByDate = "Nach Datum sortieren"
sortByName = "Nach Name sortieren"
sortBySize = "Nach Größe sortieren"
mobileShort = "Mobile"
mobileUpload = "Mobile Upload"
mobileUploadNotAvailable = "Mobile upload not enabled"
[storage]
temporaryNotice = "Dateien werden temporär in Ihrem Browser gespeichert und können automatisch gelöscht werden"
@ -5069,6 +5264,7 @@ loading = "Laden..."
back = "Zurück"
continue = "Weiter"
error = "Fehler"
save = "Save"
[config.overview]
title = "Anwendungskonfiguration"
@ -5131,6 +5327,15 @@ impact = "Alle Anwendungen oder Dienste, die derzeit diese Schlüssel verwenden,
confirmPrompt = "Sind Sie sicher, dass Sie fortfahren möchten?"
confirmCta = "Schlüssel aktualisieren"
[config.apiKeys.alert]
apiKeyErrorTitle = "API-Schlüssel-Fehler"
failedToCreateApiKey = "API-Schlüssel konnte nicht erstellt werden."
failedToRetrieveApiKey = "API-Schlüssel konnte nicht aus der Antwort abgerufen werden."
failedToFetchApiKey = "API-Schlüssel konnte nicht abgerufen werden."
apiKeyRefreshed = "API-Schlüssel aktualisiert"
apiKeyRefreshedBody = "Ihr API-Schlüssel wurde erfolgreich aktualisiert."
failedToRefreshApiKey = "API-Schlüssel konnte nicht aktualisiert werden."
[AddAttachmentsRequest]
attachments = "Anhänge auswählen"
info = "Wählen Sie Dateien aus, die Sie Ihrer PDF anhängen möchten. Diese Dateien werden eingebettet und über das Anhangs-Panel der PDF zugänglich sein."
@ -5279,6 +5484,20 @@ userBody = "Laden Sie Teammitglieder ein, weisen Sie Rollen zu und halten Sie Ih
[onboarding.securityCheck]
message = "Die Anwendung hat kürzlich bedeutende Änderungen erfahren. Möglicherweise ist Aufmerksamkeit Ihres Server-Admins erforderlich. Bitte bestätigen Sie Ihre Rolle, um fortzufahren."
[onboarding.tourOverview]
body = "Stirling PDF V2 ships with dozens of tools and a refreshed layout. Take a quick tour to see what changed and where to find the features you need."
title = "Tour Overview"
[onboarding.whatsNew]
activeFilesView = "Use Active Files to see everything you have open and pick what to work on."
fileUpload = "Use the <strong>Files</strong> button to upload or pick a recent PDF. We will load a sample so you can see the workspace."
leftPanel = "The left <strong>Tools</strong> panel lists everything you can do. Browse categories or search to find a tool quickly."
pageEditorView = "Switch to the Page Editor to reorder, rotate, or delete pages."
quickAccess = "Start at the <strong>Quick Access</strong> rail to jump between Reader, Automate, your files, and all the tours."
rightRail = "The <strong>Right Rail</strong> holds quick actions to select files, change theme or language, and download results."
topBar = "The top bar lets you swap between <strong>Viewer</strong>, <strong>Page Editor</strong>, and <strong>Active Files</strong>."
wrapUp = "That is what is new in V2. Open the <strong>Tours</strong> menu anytime to replay this, the Tools tour, or the Admin tour."
[adminOnboarding]
welcome = "Willkommen zur <strong>Admin-Tour</strong>! Entdecken wir die leistungsstarken Enterprise-Funktionen und Einstellungen für Systemadministratoren."
configButton = "Klicken Sie auf die Schaltfläche <strong>Config</strong>, um alle Systemeinstellungen und Administrationskontrollen aufzurufen."
@ -5568,6 +5787,28 @@ contactSales = "Vertrieb kontaktieren"
contactToUpgrade = "Kontaktieren Sie uns, um Ihren Plan zu upgraden oder anzupassen"
maxUsers = "Max. Benutzer"
upTo = "Bis zu"
activateLicense = "Activate Your License"
checkoutInstructions = "Complete your purchase in the Stripe tab. After payment, return here and refresh the page to activate your license. You will also receive an email with your license key."
checkoutOpened = "Checkout Opened"
getLicense = "Get Server License"
monthlyBilling = "Monthly Billing"
selectPeriod = "Select Billing Period"
upgradeToEnterprise = "Upgrade to Enterprise"
yearlyBilling = "Yearly Billing"
[plan.static.billingPortal]
message = "You will need to verify your email address in the Stripe billing portal. Check your email for a login link."
title = "Email Verification Required"
[plan.static.licenseActivation]
activate = "Activate License"
checkoutOpened = "Checkout Opened in New Tab"
doLater = "I'll do this later"
enterKey = "Enter your license key below to activate your plan:"
instructions = "Complete your purchase in the Stripe tab. Once your payment is complete, you will receive an email with your license key."
keyDescription = "Paste the license key from your email"
success = "License Activated!"
successMessage = "Your license has been successfully activated. You can now close this window."
[plan.period]
month = "Monat"
@ -5771,6 +6012,8 @@ notAvailable = "Audit-System nicht verfügbar"
notAvailableMessage = "Das Audit-System ist nicht konfiguriert oder nicht verfügbar."
disabled = "Audit-Protokollierung ist deaktiviert"
disabledMessage = "Aktivieren Sie die Audit-Protokollierung in Ihrer Anwendungskonfiguration, um Systemereignisse nachzuverfolgen."
enterpriseRequired = "Enterprise License Required"
enterpriseRequiredMessage = "The audit logging system is an enterprise feature. Please upgrade to an enterprise license to access audit logs and analytics."
[audit.error]
title = "Fehler beim Laden des Audit-Systems"
@ -5945,6 +6188,7 @@ emptyUrl = "Bitte eine Server-URL eingeben"
unreachable = "Verbindung zum Server konnte nicht hergestellt werden"
testFailed = "Verbindungstest fehlgeschlagen"
configFetch = "Serverkonfiguration konnte nicht abgerufen werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut."
invalidUrl = "Invalid URL format. Please enter a valid URL like https://your-server.com"
[setup.server.error.securityDisabled]
title = "Anmeldung nicht aktiviert"
@ -5968,6 +6212,7 @@ instructions = "So aktivieren Sie die Anmeldung auf Ihrem Stirling PDF-Server:"
instructionsEnvVar = "Setzen Sie die Umgebungsvariable:"
instructionsOrYml = "Oder in der settings.yml:"
instructionsRestart = "Starten Sie anschließend Ihren Server neu, damit die Änderungen wirksam werden."
sso = "Single Sign-On"
[setup.login.username]
label = "Benutzername"
@ -6025,6 +6270,8 @@ reset = "Änderungen zurücksetzen"
downloadJson = "JSON herunterladen"
generatePdf = "PDF generieren"
saveChanges = "Änderungen speichern"
applyChanges = "Apply Changes"
downloadCopy = "Download Copy"
[pdfTextEditor.options.autoScaleText]
title = "Text automatisch in Rahmen einpassen"
@ -6043,6 +6290,9 @@ descriptionInline = "Tipp: Halten Sie Strg (Cmd) oder Umschalt, um mehrere Textf
title = "Bearbeiteten Text auf ein einzelnes PDF-Element fixieren"
description = "Wenn aktiviert, exportiert der Editor jedes bearbeitete Textfeld als ein PDF-Textelement, um überlappende Glyphen oder gemischte Schriften zu vermeiden."
[pdfTextEditor.options.advanced]
title = "Advanced Settings"
[pdfTextEditor.manual]
mergeTooltip = "Ausgewählte Felder zusammenführen"
merge = "Auswahl zusammenführen"
@ -6118,6 +6368,21 @@ subset = "Subset"
invalidJson = "JSON-Datei kann nicht gelesen werden. Stellen Sie sicher, dass sie vom PDF-zu-JSON-Tool erzeugt wurde."
pdfConversion = "Das bearbeitete JSON kann nicht zurück in ein PDF konvertiert werden."
[pdfTextEditor.tooltip.alpha]
text = "This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing."
title = "Alpha Viewer"
[pdfTextEditor.tooltip.header]
title = "Preview Limitations"
[pdfTextEditor.tooltip.previewVariance]
text = "Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible."
title = "Preview Variance"
[pdfTextEditor.tooltip.textFocus]
text = "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here."
title = "Text and Image Focus"
[auth]
sessionExpired = "Sitzung abgelaufen"
pleaseLoginAgain = "Bitte melden Sie sich erneut an."
@ -6164,3 +6429,134 @@ title = "Ergebnisse: Text hinzufügen"
[addText.error]
failed = "Beim Hinzufügen von Text zum PDF ist ein Fehler aufgetreten."
[annotation]
applyChanges = "Apply Changes"
backgroundColor = "Background colour"
borderOff = "Border: Off"
borderOn = "Border: On"
chooseColor = "Choose colour"
circle = "Circle"
clearBackground = "Remove background"
color = "Colour"
contents = "Text"
desc = "Use highlight, pen, text, and notes. Changes stay live—no flattening required."
drawing = "Drawing"
editCircle = "Edit Circle"
editInk = "Edit Pen"
editLine = "Edit Line"
editNote = "Edit Note"
editPolygon = "Edit Polygon"
editSelectDescription = "Click an existing annotation to edit its colour, opacity, text, or size."
editSelected = "Edit Annotation"
editSquare = "Edit Square"
editStampHint = "To change the image, delete this stamp and add a new one."
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
editText = "Edit Text Box"
editTextMarkup = "Edit Text Markup"
ellipse = "Ellipse"
exit = "Exit annotation mode"
fillColor = "Fill Colour"
fillOpacity = "Fill Opacity"
fontSize = "Font size"
freehandHighlighter = "Freehand Highlighter"
highlight = "Highlight"
imagePreview = "Preview"
inkHighlighter = "Freehand Highlighter"
line = "Line"
noBackground = "No background"
note = "Note"
noteIcon = "Note Icon"
notesStamps = "Notes & Stamps"
opacity = "Opacity"
pen = "Pen"
polygon = "Polygon"
rectangle = "Rectangle"
redo = "Redo"
saveChanges = "Save Changes"
saveFailed = "Unable to save copy"
saveReady = "Download ready"
savingCopy = "Preparing download..."
select = "Select"
selectAndMove = "Select and Edit"
settings = "Settings"
shapes = "Shapes"
square = "Square"
squiggly = "Squiggly"
stamp = "Add Image"
stampSettings = "Stamp Settings"
strikeout = "Strikeout"
strokeColor = "Stroke Colour"
strokeOpacity = "Stroke Opacity"
strokeWidth = "Width"
text = "Text box"
textAlignment = "Text Alignment"
textMarkup = "Text Markup"
title = "Annotate"
underline = "Underline"
undo = "Undo"
unsupportedType = "This annotation type is not fully supported for editing."
[footer]
discord = "Discord"
issues = "GitHub"
[mobileScanner]
addToBatch = "Add to Batch"
back = "Back"
batchImages = "Batch"
camera = "Camera"
cameraAccessDenied = "Camera access denied. Please enable camera access."
cameraDescription = "Scan documents using your device camera with automatic edge detection"
capture = "Capture Photo"
chooseMethod = "Choose Upload Method"
chooseMethodDescription = "Select how you want to scan and upload documents"
clearBatch = "Clear"
connected = "Connected"
connecting = "Connecting..."
edgeDetection = "Edge Detection"
fileDescription = "Upload existing photos or documents from your device"
fileUpload = "File Upload"
flash = "Flash"
flashlight = "Flashlight"
httpsRequired = "Camera access requires HTTPS or localhost. Please use HTTPS or access via localhost."
noSession = "Invalid Session"
noSessionMessage = "Please scan a valid QR code to access this page."
preview = "Preview"
processing = "Processing..."
retake = "Retake"
selectFilesPrompt = "Select files to upload"
selectImage = "Select Image"
sessionExpired = "This session has expired. Please refresh and try again."
sessionInvalid = "Session Error"
sessionNotFound = "Session not found. Please refresh and try again."
sessionValidationError = "Unable to verify session. Please try again."
settings = "Settings"
title = "Mobile Scanner"
upload = "Upload"
uploadAll = "Upload All"
uploadFailed = "Upload failed. Please try again."
uploadSuccess = "Upload Successful!"
uploadSuccessMessage = "Your images have been transferred."
uploading = "Uploading..."
validating = "Validating session..."
[mobileUpload]
connected = "Mobile device connected"
description = "Scan to upload photos. Images auto-convert to PDF."
descriptionNoConvert = "Scan to upload photos from your mobile device."
error = "Connection Error"
expiryWarning = "Session Expiring Soon"
expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically."
filesReceived = "{{count}} file(s) received"
instructions = "Scan with your phone camera. Images convert to PDF automatically."
instructionsNoConvert = "Scan with your phone camera to upload files."
pollingError = "Error checking for files"
sessionCreateError = "Failed to create session"
sessionId = "Session ID"
title = "Upload from Mobile"
[textAlign]
center = "Center"
left = "Left"
right = "Right"

View File

@ -5404,6 +5404,15 @@ impact = "Any applications or services currently using these keys will stop work
confirmPrompt = "Are you sure you want to continue?"
confirmCta = "Refresh Keys"
[config.apiKeys.alert]
apiKeyErrorTitle = "API Key Error"
failedToCreateApiKey = "Failed to create API key."
failedToRetrieveApiKey = "Failed to retrieve API key from response."
failedToFetchApiKey = "Failed to fetch API key."
apiKeyRefreshed = "API Key Refreshed"
apiKeyRefreshedBody = "Your API key has been successfully refreshed."
failedToRefreshApiKey = "Failed to refresh API key."
[AddAttachmentsRequest]
attachments = "Select Attachments"
info = "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel."

View File

@ -16,9 +16,12 @@ export function useRestartServer() {
};
const restartServer = async () => {
try {
setRestartModalOpened(false);
setRestartModalOpened(false);
await apiClient.post('/api/v1/admin/settings/restart', undefined, {
suppressErrorToast: true,
})
.then(() => {
alert({
alertType: 'neutral',
title: t('admin.settings.restarting', 'Restarting Server'),
@ -27,14 +30,12 @@ export function useRestartServer() {
'The server is restarting. Please wait a moment...'
),
});
await apiClient.post('/api/v1/admin/settings/restart');
// Wait a moment then reload the page
setTimeout(() => {
window.location.reload();
}, 3000);
} catch (_error) {
})
.catch(async (_error) => {
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
@ -43,7 +44,7 @@ export function useRestartServer() {
'Failed to restart server. Please restart manually.'
),
});
}
})
};
return {

View File

@ -1,5 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import apiClient from "@app/services/apiClient";
import { alert } from "@app/components/toast";
import { useTranslation } from "react-i18next";
export function useApiKey() {
const [apiKey, setApiKey] = useState<string | null>(null);
@ -7,49 +9,107 @@ export function useApiKey() {
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
const { t } = useTranslation();
function failedToCreateAlert() {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToCreateApiKey", "Failed to create API key."),
isPersistentPopup: false,
});
}
const fetchKey = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Backend is POST for get and update
const res = await apiClient.post("/api/v1/user/get-api-key");
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
// If not found, try to create one by calling update endpoint
if (e?.response?.status === 404) {
try {
const createRes = await apiClient.post("/api/v1/user/update-api-key");
const created =
typeof createRes.data === "string"
? createRes.data
: createRes.data?.apiKey;
if (typeof created === "string") setApiKey(created);
} catch (createErr: any) {
setError(createErr);
// Backend is POST for get and update
await apiClient
.post("/api/v1/user/get-api-key", undefined, {
responseType: "json",
})
.then((response) => {
const data = response.data;
const apiKeyValue = typeof data === "string" ? data : data?.apiKey;
if (typeof apiKeyValue === "string") {
setApiKey(apiKeyValue);
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRetrieveApiKey", "Failed to retrieve API key from response."),
isPersistentPopup: false,
});
}
} else {
setError(e);
}
} finally {
setIsLoading(false);
setHasAttempted(true);
}
})
.catch(async (e) => {
// If not found, try to create one by calling update endpoint
if (e?.response?.status === 404) {
await apiClient
.post("/api/v1/user/update-api-key")
.then((createRes) => {
const created = typeof createRes.data === "string" ? createRes.data : createRes.data?.apiKey;
if (typeof created === "string") {
setApiKey(created);
} else {
failedToCreateAlert();
}
})
.catch((createErr) => {
failedToCreateAlert();
setError(createErr);
});
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToFetchApiKey", "Failed to fetch API key."),
isPersistentPopup: false,
});
setError(e);
}
})
.finally(() => {
setIsLoading(false);
setHasAttempted(true);
});
}, []);
const refresh = useCallback(async () => {
setIsRefreshing(true);
setError(null);
try {
const res = await apiClient.post("/api/v1/user/update-api-key");
await apiClient.post("/api/v1/user/update-api-key", undefined, {
responseType: "json",
suppressErrorToast: true,
}).then((res) => {
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
if (typeof value === "string") {
alert({
alertType: "success",
title: t("config.apiKeys.alert.apiKeyRefreshed", "API Key Refreshed"),
body: t("config.apiKeys.alert.apiKeyRefreshedBody", "Your API key has been successfully refreshed."),
isPersistentPopup: false,
});
setApiKey(value);
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
isPersistentPopup: false,
});
}
}).catch((e) => {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
isPersistentPopup: false,
});
setError(e);
} finally {
}).finally(() => {
setIsRefreshing(false);
}
});
}, []);
useEffect(() => {