Compare commits

..
1 Commits
Author SHA1 Message Date
Anthony StirlingandGitHub 672b02da1e Update build.gradle 2025-08-08 14:07:44 +01:00
90 changed files with 294 additions and 3536 deletions
+8 -8
View File
@@ -2,20 +2,20 @@
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
# Backend
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
/app/** @DarioGii
#V1 frontend
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
#V2 frontend
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
#V2 docker
/docker/backend/** @Frooodle @Ludy87 @DarioGii @Ludy87
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
/docker/backend/** @Frooodle @Ludy87 @DarioGii
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96
#GHA (All users)
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96
-2
View File
@@ -27,5 +27,3 @@ project: &project
- gradlew.bat
- launch4jConfig.xml
- settings.gradle
- frontend/**
- docker/**
+1 -1
View File
@@ -182,4 +182,4 @@
description: "Pull request has been deployed to a test environment"
- name: "codex"
color: "ededed"
description: "chatgpt AI generated code"
description: "chatgpt AI generated code"
+37 -37
View File
@@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![61%](https://geps.dev/progress/61) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![62%](https://geps.dev/progress/62) |
| Basque (Euskara) (eu_ES) | ![36%](https://geps.dev/progress/36) |
| Bulgarian (Български) (bg_BG) | ![68%](https://geps.dev/progress/68) |
| Catalan (Català) (ca_CA) | ![68%](https://geps.dev/progress/68) |
| Croatian (Hrvatski) (hr_HR) | ![60%](https://geps.dev/progress/60) |
| Czech (Česky) (cs_CZ) | ![70%](https://geps.dev/progress/70) |
| Danish (Dansk) (da_DK) | ![61%](https://geps.dev/progress/61) |
| Dutch (Nederlands) (nl_NL) | ![60%](https://geps.dev/progress/60) |
| Arabic (العربية) (ar_AR) | ![63%](https://geps.dev/progress/63) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![63%](https://geps.dev/progress/63) |
| Basque (Euskara) (eu_ES) | ![37%](https://geps.dev/progress/37) |
| Bulgarian (Български) (bg_BG) | ![70%](https://geps.dev/progress/70) |
| Catalan (Català) (ca_CA) | ![69%](https://geps.dev/progress/69) |
| Croatian (Hrvatski) (hr_HR) | ![62%](https://geps.dev/progress/62) |
| Czech (Česky) (cs_CZ) | ![71%](https://geps.dev/progress/71) |
| Danish (Dansk) (da_DK) | ![63%](https://geps.dev/progress/63) |
| Dutch (Nederlands) (nl_NL) | ![61%](https://geps.dev/progress/61) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![89%](https://geps.dev/progress/89) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) |
| Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) |
| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) |
| Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) |
| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) |
| Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![63%](https://geps.dev/progress/63) |
| Irish (Gaeilge) (ga_IE) | ![70%](https://geps.dev/progress/70) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) |
| Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) |
| Persian (فارسی) (fa_IR) | ![64%](https://geps.dev/progress/64) |
| Polish (Polski) (pl_PL) | ![72%](https://geps.dev/progress/72) |
| Portuguese (Português) (pt_PT) | ![69%](https://geps.dev/progress/69) |
| Portuguese Brazilian (Português) (pt_BR) | ![76%](https://geps.dev/progress/76) |
| Romanian (Română) (ro_RO) | ![57%](https://geps.dev/progress/57) |
| Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![95%](https://geps.dev/progress/95) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![51%](https://geps.dev/progress/51) |
| Slovenian (Slovenščina) (sl_SI) | ![71%](https://geps.dev/progress/71) |
| Spanish (Español) (es_ES) | ![74%](https://geps.dev/progress/74) |
| Swedish (Svenska) (sv_SE) | ![65%](https://geps.dev/progress/65) |
| Thai (ไทย) (th_TH) | ![59%](https://geps.dev/progress/59) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) |
| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) |
| Turkish (Türkçe) (tr_TR) | ![80%](https://geps.dev/progress/80) |
| Ukrainian (Українська) (uk_UA) | ![71%](https://geps.dev/progress/71) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![57%](https://geps.dev/progress/57) |
| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) |
| Japanese (日本語) (ja_JP) | ![95%](https://geps.dev/progress/95) |
| Korean (한국어) (ko_KR) | ![69%](https://geps.dev/progress/69) |
| Norwegian (Norsk) (no_NB) | ![67%](https://geps.dev/progress/67) |
| Persian (فارسی) (fa_IR) | ![66%](https://geps.dev/progress/66) |
| Polish (Polski) (pl_PL) | ![73%](https://geps.dev/progress/73) |
| Portuguese (Português) (pt_PT) | ![70%](https://geps.dev/progress/70) |
| Portuguese Brazilian (Português) (pt_BR) | ![77%](https://geps.dev/progress/77) |
| Romanian (Română) (ro_RO) | ![59%](https://geps.dev/progress/59) |
| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![97%](https://geps.dev/progress/97) |
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
| Slovakian (Slovensky) (sk_SK) | ![53%](https://geps.dev/progress/53) |
| Slovenian (Slovenščina) (sl_SI) | ![73%](https://geps.dev/progress/73) |
| Spanish (Español) (es_ES) | ![75%](https://geps.dev/progress/75) |
| Swedish (Svenska) (sv_SE) | ![67%](https://geps.dev/progress/67) |
| Thai (ไทย) (th_TH) | ![60%](https://geps.dev/progress/60) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![66%](https://geps.dev/progress/66) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![82%](https://geps.dev/progress/82) |
| Ukrainian (Українська) (uk_UA) | ![72%](https://geps.dev/progress/72) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![58%](https://geps.dev/progress/58) |
| Malayalam (മലയാളം) (ml_IN) | ![75%](https://geps.dev/progress/75) |
## Stirling PDF Enterprise
@@ -197,7 +197,7 @@ public class ApplicationProperties {
@JsonIgnore
public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) {
return new ClassPathResource(idpMetadataUri.substring("classpath:".length()))
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
.getInputStream();
}
try {
@@ -233,7 +233,6 @@ public class ApplicationProperties {
@JsonIgnore
public Resource getPrivateKey() {
if (privateKey == null) return null;
if (privateKey.startsWith("classpath:")) {
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
@@ -10,25 +10,21 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Role {
// System-wide administrator - can manage all organizations
SYSTEM_ADMIN(
"ROLE_SYSTEM_ADMIN",
Integer.MAX_VALUE,
Integer.MAX_VALUE,
"adminUserSettings.systemAdmin"),
// Organization administrator - can manage their organization and all its teams
ORG_ADMIN("ROLE_ORG_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.orgAdmin"),
// Team leader - can manage users in their specific team
TEAM_LEAD("ROLE_TEAM_LEAD", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.teamLead"),
// Legacy admin role - equivalent to SYSTEM_ADMIN for backward compatibility
// Unlimited access
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.admin"),
// Regular user with unlimited access within their team/org
// Unlimited access
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
// 40 API calls Per Day, 40 web calls
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40, "adminUserSettings.apiUser"),
// 20 API calls Per Day, 20 web calls
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20, "adminUserSettings.extraApiUser"),
// 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20, "adminUserSettings.webOnlyUser"),
INTERNAL_API_USER(
"STIRLING-PDF-BACKEND-API-USER",
Integer.MAX_VALUE,
@@ -67,35 +63,4 @@ public enum Role {
}
throw new IllegalArgumentException("No Role defined for id: " + roleId);
}
/** Checks if this role can manage users across all organizations */
public boolean isSystemAdmin() {
return this == SYSTEM_ADMIN || this == ADMIN; // ADMIN for backward compatibility
}
/** Checks if this role can manage an organization and all its teams */
public boolean isOrgAdmin() {
return isSystemAdmin() || this == ORG_ADMIN;
}
/** Checks if this role can manage a specific team */
public boolean isTeamLead() {
return isOrgAdmin() || this == TEAM_LEAD;
}
/** Gets the hierarchy level of this role (higher number = more permissions) */
public int getHierarchyLevel() {
return switch (this) {
case SYSTEM_ADMIN, ADMIN -> 4;
case ORG_ADMIN -> 3;
case TEAM_LEAD -> 2;
case USER -> 1;
default -> 0; // Limited users
};
}
/** Checks if this role has higher or equal authority than another role */
public boolean hasAuthorityOver(Role otherRole) {
return this.getHierarchyLevel() >= otherRole.getHierarchyLevel();
}
}
@@ -5,11 +5,8 @@ import java.awt.image.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.springframework.web.multipart.MultipartFile;
@@ -118,36 +115,7 @@ public class ImageProcessingUtils {
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
throws IOException {
BufferedImage image = null;
String filename = file.getOriginalFilename();
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
// For PSD files, try explicit ImageReader
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
if (readers.hasNext()) {
ImageReader reader = readers.next();
try (ImageInputStream iis = ImageIO.createImageInputStream(file.getInputStream())) {
reader.setInput(iis);
image = reader.read(0);
} finally {
reader.dispose();
}
}
if (image == null) {
throw new IOException(
"Unable to read image from file: "
+ filename
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
}
} else {
// For non-PSD files, use standard ImageIO
image = ImageIO.read(file.getInputStream());
}
if (image == null) {
throw new IOException("Unable to read image from file: " + filename);
}
BufferedImage image = ImageIO.read(file.getInputStream());
double orientation = extractImageOrientation(file.getInputStream());
return applyOrientation(image, orientation);
}
@@ -1,59 +0,0 @@
package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import stirling.software.common.configuration.InstallationPathConfig;
class ApplicationPropertiesDynamicYamlPropertySourceTest {
@Test
void loads_yaml_into_environment() throws Exception {
// YAML-Config in Temp-Datei schreiben
String yaml =
""
+ "ui:\n"
+ " appName: \"My App\"\n"
+ "system:\n"
+ " enableAnalytics: true\n";
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
Files.writeString(tmp, yaml);
// Pfad per statischem Mock liefern
try (MockedStatic<InstallationPathConfig> mocked =
Mockito.mockStatic(InstallationPathConfig.class)) {
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
ConfigurableEnvironment env = new StandardEnvironment();
ApplicationProperties props = new ApplicationProperties();
props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein
assertEquals("My App", env.getProperty("ui.appName"));
assertEquals("true", env.getProperty("system.enableAnalytics"));
}
}
@Test
void throws_when_settings_file_missing() throws Exception {
String missing = "/path/does/not/exist/spdf.yml";
try (MockedStatic<InstallationPathConfig> mocked =
Mockito.mockStatic(InstallationPathConfig.class)) {
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(missing);
ConfigurableEnvironment env = new StandardEnvironment();
ApplicationProperties props = new ApplicationProperties();
assertThrows(IOException.class, () -> props.dynamicYamlPropertySource(env));
}
}
}
@@ -1,248 +0,0 @@
package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.ApplicationProperties.Driver;
import stirling.software.common.model.ApplicationProperties.Premium;
import stirling.software.common.model.ApplicationProperties.Security;
import stirling.software.common.model.exception.UnsupportedProviderException;
class ApplicationPropertiesLogicTest {
@Test
void system_isAnalyticsEnabled_null_false_true() {
ApplicationProperties.System sys = new ApplicationProperties.System();
sys.setEnableAnalytics(null);
assertFalse(sys.isAnalyticsEnabled());
sys.setEnableAnalytics(Boolean.FALSE);
assertFalse(sys.isAnalyticsEnabled());
sys.setEnableAnalytics(Boolean.TRUE);
assertTrue(sys.isAnalyticsEnabled());
}
@Test
void tempFileManagement_defaults_and_overrides() {
ApplicationProperties.TempFileManagement tfm =
new ApplicationProperties.TempFileManagement();
String expectedBase =
java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
+ "/stirling-pdf";
assertEquals(expectedBase, tfm.getBaseTmpDir());
String expectedLibre = expectedBase + "/libreoffice";
assertEquals(expectedLibre, tfm.getLibreofficeDir());
tfm.setBaseTmpDir("/custom/base");
assertEquals("/custom/base", tfm.getBaseTmpDir());
tfm.setLibreofficeDir("/opt/libre");
assertEquals("/opt/libre", tfm.getLibreofficeDir());
}
@Test
void oauth2_scope_parsing_and_validity() {
Security.OAUTH2 oauth2 = new Security.OAUTH2();
oauth2.setIssuer("https://issuer");
oauth2.setClientId("client");
oauth2.setClientSecret("secret");
oauth2.setUseAsUsername("email");
oauth2.setScopes("openid, profile ,email");
assertTrue(oauth2.isSettingsValid());
}
@Test
void security_login_method_flags() {
Security sec = new Security();
sec.getOauth2().setEnabled(true);
sec.getSaml2().setEnabled(true);
assertTrue(sec.isUserPass());
assertTrue(sec.isOauth2Active());
assertTrue(sec.isSaml2Active());
sec.setLoginMethod(Security.LoginMethods.NORMAL.toString());
assertTrue(sec.isUserPass());
assertFalse(sec.isOauth2Active());
assertFalse(sec.isSaml2Active());
}
@Test
void security_isAltLogin_reflects_oauth2_or_saml2() {
Security sec = new Security();
assertFalse(sec.isAltLogin());
sec.getOauth2().setEnabled(true);
sec.getSaml2().setEnabled(false);
assertTrue(sec.isAltLogin());
sec.getOauth2().setEnabled(false);
sec.getSaml2().setEnabled(true);
assertTrue(sec.isAltLogin());
sec.getOauth2().setEnabled(true);
sec.getSaml2().setEnabled(true);
assertTrue(sec.isAltLogin());
}
@Test
void oauth2_client_provider_mapping_and_unsupported() throws UnsupportedProviderException {
Security.OAUTH2.Client client = new Security.OAUTH2.Client();
assertNotNull(client.get("google"));
assertNotNull(client.get("github"));
assertNotNull(client.get("keycloak"));
UnsupportedProviderException ex =
assertThrows(UnsupportedProviderException.class, () -> client.get("unknown"));
assertTrue(ex.getMessage().toLowerCase().contains("not supported"));
}
@Test
void premium_google_drive_getters_return_empty_string_on_null_or_blank() {
Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive();
assertEquals("", gd.getClientId());
assertEquals("", gd.getApiKey());
assertEquals("", gd.getAppId());
gd.setClientId(" id ");
gd.setApiKey(" key ");
gd.setAppId(" app ");
assertEquals(" id ", gd.getClientId());
assertEquals(" key ", gd.getApiKey());
assertEquals(" app ", gd.getAppId());
}
@Test
void ui_getters_return_null_for_blank() {
ApplicationProperties.Ui ui = new ApplicationProperties.Ui();
ui.setAppName(" ");
ui.setHomeDescription("");
ui.setAppNameNavbar(null);
assertNull(ui.getAppName());
assertNull(ui.getHomeDescription());
assertNull(ui.getAppNameNavbar());
ui.setAppName("Stirling-PDF");
ui.setHomeDescription("Home");
ui.setAppNameNavbar("Nav");
assertEquals("Stirling-PDF", ui.getAppName());
assertEquals("Home", ui.getHomeDescription());
assertEquals("Nav", ui.getAppNameNavbar());
}
@Test
void driver_toString_contains_driver_name() {
assertTrue(Driver.H2.toString().contains("h2"));
assertTrue(Driver.POSTGRESQL.toString().contains("postgresql"));
}
@Test
void session_limits_and_timeouts_have_reasonable_defaults() {
ApplicationProperties.ProcessExecutor pe = new ApplicationProperties.ProcessExecutor();
ApplicationProperties.ProcessExecutor.SessionLimit s = pe.getSessionLimit();
assertEquals(2, s.getQpdfSessionLimit());
assertEquals(1, s.getTesseractSessionLimit());
assertEquals(1, s.getLibreOfficeSessionLimit());
assertEquals(1, s.getPdfToHtmlSessionLimit());
assertEquals(8, s.getPythonOpenCvSessionLimit());
assertEquals(16, s.getWeasyPrintSessionLimit());
assertEquals(1, s.getInstallAppSessionLimit());
assertEquals(1, s.getCalibreSessionLimit());
assertEquals(8, s.getGhostscriptSessionLimit());
assertEquals(2, s.getOcrMyPdfSessionLimit());
ApplicationProperties.ProcessExecutor.TimeoutMinutes t = pe.getTimeoutMinutes();
assertEquals(30, t.getTesseractTimeoutMinutes());
assertEquals(30, t.getQpdfTimeoutMinutes());
assertEquals(30, t.getLibreOfficeTimeoutMinutes());
assertEquals(20, t.getPdfToHtmlTimeoutMinutes());
assertEquals(30, t.getPythonOpenCvTimeoutMinutes());
assertEquals(30, t.getWeasyPrintTimeoutMinutes());
assertEquals(60, t.getInstallAppTimeoutMinutes());
assertEquals(30, t.getCalibreTimeoutMinutes());
assertEquals(30, t.getGhostscriptTimeoutMinutes());
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
}
@Deprecated
@Test
void enterprise_metadata_defaults() {
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
ApplicationProperties.EnterpriseEdition.CustomMetadata eMeta = ee.getCustomMetadata();
eMeta.setCreator(" ");
eMeta.setProducer(null);
assertEquals("Stirling-PDF", eMeta.getCreator());
assertEquals("Stirling-PDF", eMeta.getProducer());
}
@Test
void premium_metadata_defaults() {
Premium.ProFeatures pf = new Premium.ProFeatures();
Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata();
pMeta.setCreator("");
pMeta.setProducer("");
assertEquals("Stirling-PDF", pMeta.getCreator());
assertEquals("Stirling-PDF", pMeta.getProducer());
}
@Test
void premium_metadata_awesome() {
Premium.ProFeatures pf = new Premium.ProFeatures();
Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata();
pMeta.setCreator("Awesome PDF Tool");
pMeta.setProducer("Awesome PDF Tool");
assertEquals("Awesome PDF Tool", pMeta.getCreator());
assertEquals("Awesome PDF Tool", pMeta.getProducer());
}
@Test
void string_isValid_handles_null_empty_blank_and_trimmed() {
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
assertFalse(oauth2.isValid((String) null, "issuer"));
assertFalse(oauth2.isValid("", "issuer"));
assertFalse(oauth2.isValid(" ", "issuer"));
assertTrue(oauth2.isValid("x", "issuer"));
assertTrue(oauth2.isValid(" x ", "issuer")); // trimmt intern
}
@Test
void collection_isValid_handles_null_and_empty() {
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
Collection<String> nullColl = null;
Collection<String> empty = List.of();
assertFalse(oauth2.isValid(nullColl, "scopes"));
assertFalse(oauth2.isValid(empty, "scopes"));
}
@Test
void collection_isValid_true_when_non_empty_even_if_element_is_blank() {
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
// Aktuelles Verhalten: prüft NUR !isEmpty(), nicht Inhalt
Collection<String> oneBlank = new ArrayList<>();
oneBlank.add(" ");
assertTrue(
oauth2.isValid(oneBlank, "scopes"),
"Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn Element leer/blank ist");
}
}
@@ -1,80 +0,0 @@
package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
class ApplicationPropertiesSaml2HttpTest {
@Test
void idpMetadataUri_http_is_resolved_via_mockwebserver() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(
new MockResponse()
.setResponseCode(200)
.addHeader("Content-Type", "application/xml")
.setBody("<EntityDescriptor/>"));
server.start();
String url = server.url("/meta").toString();
var s = new ApplicationProperties.Security.SAML2();
s.setIdpMetadataUri(url);
try (InputStream in = s.getIdpMetadataUri()) {
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(body.contains("EntityDescriptor"));
}
}
}
@Test
void idpMetadataUri_invalidUri_triggers_catch_and_throwsIOException() {
// Ungültige URI -> new URI(...) wirft URISyntaxException -> catch -> IOException
var s = new ApplicationProperties.Security.SAML2();
s.setIdpMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##)
assertThrows(IOException.class, s::getIdpMetadataUri);
}
@Test
void spCert_else_branch_returns_FileSystemResource_for_filesystem_path() throws Exception {
var s = new ApplicationProperties.Security.SAML2();
// temporäre Datei simuliert "Filesystem"-Pfad (-> else-Zweig)
Path tmp = Files.createTempFile("spdf-spcert-", ".crt");
Files.writeString(tmp, "CERT");
s.setSpCert(tmp.toString());
Resource r = s.getSpCert();
assertNotNull(r);
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
assertTrue(r.exists(), "Temp file should exist");
}
@Test
void idpCert_else_branch_returns_FileSystemResource_even_if_missing() {
var s = new ApplicationProperties.Security.SAML2();
// bewusst nicht existierender Pfad -> else-Zweig wird trotzdem genommen
String missing = "/this/path/does/not/exist/idp.crt";
s.setIdpCert(missing);
Resource r = s.getIdpCert();
assertNotNull(r);
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
assertFalse(r.exists(), "Resource should not exist for missing file");
}
}
@@ -1,55 +0,0 @@
package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.*;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.Resource;
class ApplicationPropertiesSaml2ResourceTest {
@Test
void idpMetadataUri_classpath_is_resolved() throws Exception {
var s = new ApplicationProperties.Security.SAML2();
s.setIdpMetadataUri("classpath:saml/dummy.txt");
try (InputStream in = s.getIdpMetadataUri()) {
assertNotNull(in, "Classpath InputStream should not be null");
String txt = new String(in.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(txt.contains("ok"));
}
}
@Test
void spCert_idpCert_privateKey_null_classpath_and_filesystem() throws Exception {
var s = new ApplicationProperties.Security.SAML2();
s.setSpCert(null);
s.setIdpCert(null);
s.setPrivateKey(null);
assertNull(s.getSpCert());
assertNull(s.getIdpCert());
assertNull(s.getPrivateKey());
s.setSpCert("classpath:saml/dummy.txt");
s.setIdpCert("classpath:saml/dummy.txt");
s.setPrivateKey("classpath:saml/dummy.txt");
Resource sp = s.getSpCert();
Resource idp = s.getIdpCert();
Resource pk = s.getPrivateKey();
assertTrue(sp.exists());
assertTrue(idp.exists());
assertTrue(pk.exists());
Path tmp = Files.createTempFile("spdf-key-", ".pem");
Files.writeString(tmp, "KEY");
s.setPrivateKey(tmp.toString());
Resource pkFs = s.getPrivateKey();
assertNotNull(pkFs);
assertTrue(pkFs.exists());
}
}
@@ -1 +0,0 @@
ok
+1 -1
View File
@@ -91,7 +91,7 @@ dependencies {
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
@@ -1,7 +1,6 @@
multipart.enabled=true
logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN
#logging.level.root=DEBUG
logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.saml2=TRACE
#logging.level.org.springframework.security=DEBUG
@@ -35,7 +34,6 @@ spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=false
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
server.servlet.session.timeout:30m
# Change the default URL path for OpenAPI JSON
@@ -366,38 +366,6 @@ navbar.sections.popular=المفضل
settings.title=الإعدادات
settings.update=التحديث متاح
settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=إصدار التطبيق:
settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط):
settings.downloadOption.1=فتح في نفس النافذة
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=هل يجب تعطيل/تمكين ال
adminUserSettings.usernameInfo=يمكن أن يحتوي اسم المستخدم فقط على أحرف وأرقام والرموز الخاصة التالية @._+- أو يجب أن يكون عنوان بريد إلكتروني صالح.
adminUserSettings.role=الدور
adminUserSettings.actions=الإجراءات
adminUserSettings.apiUser=مستخدم API محدود
adminUserSettings.extraApiUser=مستخدم API محدود إضافي
adminUserSettings.webOnlyUser=مستخدم الويب فقط
adminUserSettings.demoUser=مستخدم تجريبي (بدون إعدادات مخصصة)
adminUserSettings.internalApiUser=مستخدم API داخلي
adminUserSettings.forceChange=إجبار المستخدم على تغيير كلمة المرور عند تسجيل الدخول
@@ -366,38 +366,6 @@ navbar.sections.popular=Populyar
settings.title=Parametrlər
settings.update=Yeniləmə mövcuddur
settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Proqram Versiyası:
settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün):
settings.downloadOption.1=Eyni pəncərədə açın
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=İstifadəçi aktivləşdirilməli/dea
adminUserSettings.usernameInfo=İstifadəçi adı sadəcə hərflərdən, rəqəmlərdən və @._+- xüsusi simvollarından ibarət ola bilər və ya düzgün email ünvanı olmalıdır.
adminUserSettings.role=Rol
adminUserSettings.actions=Fəaliyyətlər
adminUserSettings.apiUser=Məhdudlaşdırılmış API İstifadəçisi
adminUserSettings.extraApiUser=Əlavə Məhdudlaşdırılmış API İstifadəçisi
adminUserSettings.webOnlyUser=Yalnız Veb İstifadəçisi
adminUserSettings.demoUser=Demo İstifadəçisi (Fərdi parametrlər yoxdur)
adminUserSettings.internalApiUser=Daxili API İstifadəçisi
adminUserSettings.forceChange=İstifadəçini giriş zamanı parolu dəyişməyə məcbur et
@@ -366,38 +366,6 @@ navbar.sections.popular=Популярни
settings.title=Настройки
settings.update=Налична актуализация
settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версия на приложението:
settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран):
settings.downloadOption.1=Отваряне в същия прозорец
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Трябва ли потребител
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
adminUserSettings.role=Роля
adminUserSettings.actions=Действия
adminUserSettings.apiUser=Ограничен API потребител
adminUserSettings.extraApiUser=Допълнителен ограничен API потребител
adminUserSettings.webOnlyUser=Само за уеб-потребител
adminUserSettings.demoUser=Демо потребител (без персонализирани настройки)
adminUserSettings.internalApiUser=Вътрешен API потребител
adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане
@@ -366,38 +366,6 @@ navbar.sections.popular=སྤྱི་མོས།
settings.title=སྒྲིག་འགོད།
settings.update=གསར་སྒྱུར་ཡོད།
settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད།
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=མཉེན་ཆས་པར་གཞི།
settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།):
settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ།
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=སྤྱོད་མཁན་སྤ
adminUserSettings.usernameInfo=སྤྱོད་མཁན་མིང་ནང་ཡི་གེ་དང་ཨང་ཀི། དམིགས་བསལ་མཚོན་རྟགས་ @._+- ཡང་ན་གློག་འཕྲིན་ཁ་བྱང་ཚད་ལྡན་ཞིག་དགོས།
adminUserSettings.role=འགན་འཁུར།
adminUserSettings.actions=བྱ་སྤྱོད།
adminUserSettings.apiUser=ཚད་བཀག་ཅན་གྱི་ API སྤྱོད་མཁན།
adminUserSettings.extraApiUser=ཚད་བཀག་ཅན་གྱི་ API སྤྱོད་མཁན་འཕར་མ།
adminUserSettings.webOnlyUser=དྲ་ཚིགས་ཁོ་ནའི་སྤྱོད་མཁན།
adminUserSettings.demoUser=བརྟག་དཔྱད་སྤྱོད་མཁན། (རང་སྒྲིག་མེད་པ།)
adminUserSettings.internalApiUser=ནང་ཁུལ་ API སྤྱོད་མཁན།
adminUserSettings.forceChange=ནང་འཛུལ་སྐབས་གསང་ཚིག་བསྒྱུར་དགོས་པ་བཟོ་བ།
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Opcions
settings.update=Actualització Disponible
settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versió de l'App:
settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit):
settings.downloadOption.1=Obre en la mateixa finestra
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Vols deshabilitar/habilitar aquest usu
adminUserSettings.usernameInfo=El nom d'usuari només pot contenir lletres, números i els següents caràcters especials: @._+- o ha de ser una adreça de correu electrònic vàlida.
adminUserSettings.role=Rol
adminUserSettings.actions=Accions
adminUserSettings.apiUser=Usuari amb API limitada
adminUserSettings.extraApiUser=Usuari Addicional amb API limitada
adminUserSettings.webOnlyUser=Usuari només WEB
adminUserSettings.demoUser=Usuari de Demo (Sense configuracions personalitzades)
adminUserSettings.internalApiUser=Usuari d'API Interna
adminUserSettings.forceChange=Força l'usuari a canviar la contrasenya en iniciar sessió
@@ -366,38 +366,6 @@ navbar.sections.popular=Oblíbené
settings.title=Nastavení
settings.update=K dispozici je aktualizace
settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verze aplikace:
settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu):
settings.downloadOption.1=Otevřít ve stejném okně
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Má být uživatel deaktivován/aktivo
adminUserSettings.usernameInfo=Uživatelské jméno může obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být platná e-mailová adresa.
adminUserSettings.role=Role
adminUserSettings.actions=Akce
adminUserSettings.apiUser=Omezený API uživatel
adminUserSettings.extraApiUser=Další omezený API uživatel
adminUserSettings.webOnlyUser=Pouze webový uživatel
adminUserSettings.demoUser=Demo uživatel (Bez vlastních nastavení)
adminUserSettings.internalApiUser=Interní API uživatel
adminUserSettings.forceChange=Vynutit změnu hesla při přihlášení
@@ -366,38 +366,6 @@ navbar.sections.popular=Populære
settings.title=Indstillinger
settings.update=Opdatering tilgængelig
settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads):
settings.downloadOption.1=Åbn i samme vindue
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Skal brugeren deaktiveres/aktiveres?
adminUserSettings.usernameInfo=Brugernavn må kun indeholde bogstaver, tal og følgende specialtegn @._+- eller skal være en gyldig e-mailadresse.
adminUserSettings.role=Rolle
adminUserSettings.actions=Handlinger
adminUserSettings.apiUser=Begrænset API-bruger
adminUserSettings.extraApiUser=Yderligere Begrænset API-bruger
adminUserSettings.webOnlyUser=Kun Web-bruger
adminUserSettings.demoUser=Demo-bruger (Ingen brugerdefinerede indstillinger)
adminUserSettings.internalApiUser=Intern API-bruger
adminUserSettings.forceChange=Tving bruger til at ændre adgangskode ved login
@@ -366,38 +366,6 @@ navbar.sections.popular=Beliebt
settings.title=Einstellungen
settings.update=Update verfügbar
settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App-Version:
settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind):
settings.downloadOption.1=Im selben Fenster öffnen
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Soll der Benutzer deaktiviert/aktivier
adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
adminUserSettings.role=Rolle
adminUserSettings.actions=Aktionen
adminUserSettings.apiUser=Eingeschränkter API-Benutzer
adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer
adminUserSettings.webOnlyUser=Nur Web-Benutzer
adminUserSettings.demoUser=Demo-Benutzer (Keine benutzerdefinierten Einstellungen)
adminUserSettings.internalApiUser=Interner API-Benutzer
adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern
@@ -366,38 +366,6 @@ navbar.sections.popular=Δημοφιλή
settings.title=Ρυθμίσεις
settings.update=Διαθέσιμη ενημέρωση
settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Έκδοση εφαρμογής:
settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση):
settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Θέλετε να απενεργοπο
adminUserSettings.usernameInfo=Το όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email.
adminUserSettings.role=Ρόλος
adminUserSettings.actions=Ενέργειες
adminUserSettings.apiUser=Περιορισμένος χρήστης API
adminUserSettings.extraApiUser=Επιπλέον περιορισμένος χρήστης API
adminUserSettings.webOnlyUser=Χρήστης μόνο web
adminUserSettings.demoUser=Δοκιμαστικός χρήστης (Χωρίς προσαρμοσμένες ρυθμίσεις)
adminUserSettings.internalApiUser=Εσωτερικός χρήστης API
adminUserSettings.forceChange=Υποχρεωτική αλλαγή κωδικού κατά τη σύνδεση
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Settings
settings.update=Update available
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
@@ -446,9 +414,6 @@ account.adminNotif=You have admin privileges. Access system settings and user ma
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.systemAdmin=System Administrator
adminUserSettings.orgAdmin=Organisation Administrator
adminUserSettings.teamLead=Team Leader
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
@@ -458,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -601,12 +569,12 @@ rotate.tags=server side
home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDF to Image
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Organise
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Settings
settings.update=Update available
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -598,12 +569,12 @@ rotate.tags=server side
home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDF to Image
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Organize
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
@@ -366,38 +366,6 @@ navbar.sections.popular=Populares
settings.title=Configuración
settings.update=Actualización disponible
settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versión de la aplicación:
settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP):
settings.downloadOption.1=Abrir en la misma ventana
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=¿Se debe habilitar/deshabilitar el us
adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
adminUserSettings.role=Rol
adminUserSettings.actions=Acciones
adminUserSettings.apiUser=Usuario limitado de API
adminUserSettings.extraApiUser=Otro usuario limitado de API
adminUserSettings.webOnlyUser=Usuario solo web
adminUserSettings.demoUser=Usuario Demo (Sin ajustes personalizados)
adminUserSettings.internalApiUser=Usuario interno de API
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Ezarpenak
settings.update=Eguneratze eskuragarria
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Aplikazioaren bertsioa:
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
settings.downloadOption.1=Ireki leiho berean
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.role=Rol
adminUserSettings.actions=Ekintzak
adminUserSettings.apiUser=APIren erabiltzaile mugatua
adminUserSettings.extraApiUser=Additional Limited API User
adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.internalApiUser=Internal API User
adminUserSettings.forceChange=Force user to change password on login
@@ -599,11 +570,11 @@ rotate.tags=server side
home.imageToPdf.title=Irudia PDF bihurtu
home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDFa irudi bihurtu
home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Antolatzailea
home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan
@@ -366,38 +366,6 @@ navbar.sections.popular=محبوب
settings.title=تنظیمات
settings.update=به‌روزرسانی موجود است
settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=نسخه برنامه:
settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده):
settings.downloadOption.1=باز کردن در همان پنجره
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=آیا باید وضعیت کاربر
adminUserSettings.usernameInfo=نام کاربری فقط می‌تواند شامل حروف، اعداد و کاراکترهای خاص @._+- باشد یا باید یک آدرس ایمیل معتبر باشد.
adminUserSettings.role=نقش
adminUserSettings.actions=اقدامات
adminUserSettings.apiUser=کاربر محدود API
adminUserSettings.extraApiUser=کاربر محدود اضافی API
adminUserSettings.webOnlyUser=فقط کاربر وب
adminUserSettings.demoUser=کاربر دمو (بدون تنظیمات سفارشی)
adminUserSettings.internalApiUser=کاربر داخلی API
adminUserSettings.forceChange=مجبور کردن کاربر به تغییر رمز عبور هنگام ورود
@@ -366,38 +366,6 @@ navbar.sections.popular=Populaire
settings.title=Paramètres
settings.update=Mise à jour disponible
settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Version de l'application :
settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) :
settings.downloadOption.1=Ouvrir dans la même fenêtre
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Voulez vous vraiment déactiver/réact
adminUserSettings.usernameInfo=Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
adminUserSettings.role=Rôle
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Utilisateur API limité
adminUserSettings.extraApiUser=Utilisateur limité supplémentaire de l'API
adminUserSettings.webOnlyUser=Utilisateur Web uniquement
adminUserSettings.demoUser=Demo User (Paramètres par défaut)
adminUserSettings.internalApiUser=Utilisateur de l'API interne
adminUserSettings.forceChange=Forcer l'utilisateur à changer son nom d'utilisateur/mot de passe lors de la connexion
@@ -598,7 +569,7 @@ rotate.tags=pivoter,server side,rotate
home.imageToPdf.title=Image en PDF
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF.
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF) en PDF.
imageToPdf.tags=pdf,conversion,img,jpg,image,photo
home.pdfToImage.title=PDF en image
@@ -366,38 +366,6 @@ navbar.sections.popular=Coitianta
settings.title=Socruithe
settings.update=Nuashonrú ar fáil
settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Leagan Aipe:
settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip):
settings.downloadOption.1=Oscail sa bhfuinneog chéanna
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Ar cheart an t-úsáideoir a dhíchuma
adminUserSettings.usernameInfo=Ní féidir ach litreacha, uimhreacha agus na carachtair speisialta seo a leanas @._+- a bheith san ainm úsáideora nó ní mór gur seoladh ríomhphoist bailí é.
adminUserSettings.role=Ról
adminUserSettings.actions=Gníomhartha
adminUserSettings.apiUser=Úsáideoir API Teoranta
adminUserSettings.extraApiUser=Úsáideoir API Teoranta breise
adminUserSettings.webOnlyUser=Úsáideoir Gréasáin Amháin
adminUserSettings.demoUser=Úsáideoir Taispeána (Gan socruithe saincheaptha)
adminUserSettings.internalApiUser=Úsáideoir API Inmheánach
adminUserSettings.forceChange=Cuir iallach ar an úsáideoir pasfhocal a athrú ar logáil isteach
@@ -366,38 +366,6 @@ navbar.sections.popular=लोकप्रिय
settings.title=सेटिंग्स
settings.update=अपडेट उपलब्ध है
settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है।
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=ऐप संस्करण:
settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए):
settings.downloadOption.1=उसी विंडो में खोलें
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=क्या उपयोगकर्
adminUserSettings.usernameInfo=उपयोगकर्ता नाम में केवल अक्षर, संख्याएं और निम्नलिखित विशेष वर्ण @._+- हो सकते हैं या एक वैध ईमेल पता होना चाहिए।
adminUserSettings.role=भूमिका
adminUserSettings.actions=कार्रवाइयां
adminUserSettings.apiUser=सीमित API उपयोगकर्ता
adminUserSettings.extraApiUser=अतिरिक्त सीमित API उपयोगकर्ता
adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता
adminUserSettings.demoUser=डेमो उपयोगकर्ता (कोई कस्टम सेटिंग्स नहीं)
adminUserSettings.internalApiUser=आंतरिक API उपयोगकर्ता
adminUserSettings.forceChange=लॉगिन पर उपयोगकर्ता को पासवर्ड बदलने के लिए मजबूर करें
@@ -366,38 +366,6 @@ navbar.sections.popular=Popularno
settings.title=Postavke
settings.update=Dostupno ažuriranje
settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzija aplikacije:
settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata):
settings.downloadOption.1=Otvori u istom prozoru
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Treba li isključiti/uključiti ovog k
adminUserSettings.usernameInfo=Korisničko ime može sadržavati samo slova, brojke i sljedeće posebne znakove @._+- ili mora biti važeća adresa e-pošte.
adminUserSettings.role=Uloga
adminUserSettings.actions=Akcije
adminUserSettings.apiUser=Korisnik s ograničenim API pristupom
adminUserSettings.extraApiUser=Dodatni korisnik s ograničenim API pristupom
adminUserSettings.webOnlyUser=Web Korisnik
adminUserSettings.demoUser=Demo korisnik (Bez prilagođenih Postavki)
adminUserSettings.internalApiUser=Interni API Korisnik
adminUserSettings.forceChange=Prisiliti korisnika da promijeni lozinku prilikom prijave
@@ -366,38 +366,6 @@ navbar.sections.popular=Népszerű
settings.title=Beállítások
settings.update=Frissítés elérhető
settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Sürgős frissítés érhető el
update.updateAvailable=Frissítés érhető el
update.modalTitle=Frissítés érhető el
update.current=Jelenlegi verzió
update.latest=Legújabb verzió
update.latestStable=Legújabb stabil verzió
update.priority=Fontosság
update.recommendedAction=Ajánlott lépés
update.breakingChangesDetected=⚠️ Jelentős változások észlelve
update.breakingChangesMessage=Ez a frissítés jelentős változásokat tartalmaz. Kérjük, olvassa el az alábbi migrációs útmutatót.
update.migrationGuides=Migrációs útmutatók:
update.viewGuide=Útmutató megtekintése
update.loadingDetailedInfo=Részletes verzióinformációk betöltése folyamatban...
update.close=Bezárás
update.viewAllReleases=Összes kiadás megtekintése
update.downloadLatest=Legújabb verzió letöltése
update.availableUpdates=Elérhető frissítések:
update.unableToLoadDetails=Nem sikerült betölteni a részletes verzióinformációkat.
update.version=Verzió
# Update priority levels
update.priority.urgent=SÜRGETŐ
update.priority.normal=NORMÁL
update.priority.minor=KISEBB
update.priority.low=ALACSONY
# Breaking changes text
update.breakingChanges=Megszakító változások:
update.breakingChangesDefault=Ez a verzió megszakító változásokat tartalmaz
update.migrationGuide=Migrációs útmutató
settings.appVersion=Alkalmazás verziója:
settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén):
settings.downloadOption.1=Megnyitás ugyanabban az ablakban
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Biztosan módosítja a felhasználó
adminUserSettings.usernameInfo=A felhasználónév csak betűket, számokat és a következő speciális karaktereket tartalmazhatja: @._+- vagy érvényes e-mail címnek kell lennie.
adminUserSettings.role=Szerepkör
adminUserSettings.actions=Műveletek
adminUserSettings.apiUser=Korlátozott API felhasználó
adminUserSettings.extraApiUser=További korlátozott API felhasználó
adminUserSettings.webOnlyUser=Csak webes felhasználó
adminUserSettings.demoUser=Demo felhasználó (egyedi beállítások nélkül)
adminUserSettings.internalApiUser=Belső API felhasználó
adminUserSettings.forceChange=Jelszóváltoztatás kikényszerítése bejelentkezéskor
@@ -366,38 +366,6 @@ navbar.sections.popular=Populer
settings.title=Pengaturan
settings.update=Pembaruan tersedia
settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versi Aplikasi:
settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip):
settings.downloadOption.1=Buka di jendela yang sama
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Haruskah pengguna dinonaktifkan/diakti
adminUserSettings.usernameInfo=Nama pengguna hanya boleh mengandung huruf, angka, dan karakter khusus berikut @._+- atau harus berupa alamat email yang valid.
adminUserSettings.role=Peran
adminUserSettings.actions=Tindakan
adminUserSettings.apiUser=Pengguna API Terbatas
adminUserSettings.extraApiUser=Pengguna API Terbatas Tambahan
adminUserSettings.webOnlyUser=Pengguna Khusus Web
adminUserSettings.demoUser=Pengguna Demo (Tanpa pengaturan kustom)
adminUserSettings.internalApiUser=Pengguna API Internal
adminUserSettings.forceChange=Memaksa pengguna untuk mengubah nama pengguna/kata sandi saat masuk
@@ -366,38 +366,6 @@ navbar.sections.popular=Popolare
settings.title=Impostazioni
settings.update=Aggiornamento disponibile
settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Aggiornamento disponibile
update.updateAvailable=Aggiornamento disponibile
update.modalTitle=Aggiornamento disponibile
update.current=Corrente
update.latest=Ultimo
update.latestStable=Ultima versione stabile
update.priority=Priorità
update.recommendedAction=Azione consigliata
update.breakingChangesDetected=⚠️ Rilevate modifiche sostanziali
update.breakingChangesMessage=Questo aggiornamento contiene modifiche sostanziali. Consulta le guide alla migrazione riportate di seguito.
update.migrationGuides=Guide alla migrazione:
update.viewGuide=Visualizza la guida
update.loadingDetailedInfo=Caricamento delle informazioni dettagliate sulla versione...
update.close=Chiudi
update.viewAllReleases=Visualizza tutte le versioni
update.downloadLatest=Scarica l'ultima
update.availableUpdates=Aggiornamenti disponibili:
update.unableToLoadDetails=Impossibile caricare informazioni dettagliate sulla versione.
update.version=Versione
# Update priority levels
update.priority.urgent=URGENTE
update.priority.normal=NORMALE
update.priority.minor=MINORE
update.priority.low=BASSA
# Breaking changes text
update.breakingChanges=Modifiche sostanziali:
update.breakingChangesDefault=Questa versione contiene modifiche sostanziali
update.migrationGuide=Guida alla migrazione
settings.appVersion=Versione App:
settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
settings.downloadOption.1=Apri in questa finestra
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=L'utente dovrebbe essere disabilitato/
adminUserSettings.usernameInfo=Il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- oppure deve essere un indirizzo email valido.
adminUserSettings.role=Ruolo
adminUserSettings.actions=Azioni
adminUserSettings.apiUser=Utente API limitato
adminUserSettings.extraApiUser=API utente limitato aggiuntivo
adminUserSettings.webOnlyUser=Utente solo Web
adminUserSettings.demoUser=Utente demo (nessuna impostazione personalizzata)
adminUserSettings.internalApiUser=API utente interna
adminUserSettings.forceChange=Forza l'utente a cambiare nome utente/password all'accesso
@@ -1693,7 +1664,7 @@ fileChooser.dragAndDrop=Trascina & Rilascia
fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF
fileChooser.dragAndDropImage=Trascina & rilascia il file immagine
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
fileChooser.extractPDF=Estrazione...
fileChooser.extractPDF=Estraendo...
fileChooser.addAttachments=trascina & rilascia gli allegati qui
#release notes
@@ -1889,12 +1860,12 @@ editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deselezi
editTableOfContents.editorTitle=Editor segnalibri
editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari.
editTableOfContents.addBookmark=Aggiungi nuovo segnalibro
editTableOfContents.importBookmarksDefault=Importa
editTableOfContents.importBookmarksFromJsonFile=Carica file JSON
editTableOfContents.importBookmarksFromClipboard=Incolla dagli appunti
editTableOfContents.exportBookmarksDefault=Esporta
editTableOfContents.exportBookmarksAsJson=Scarica come JSON
editTableOfContents.exportBookmarksAsText=Copia come testo
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF.
editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali.
editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione.
@@ -366,38 +366,6 @@ navbar.sections.popular=人気
settings.title=設定
settings.update=利用可能なアップデート
settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Appバージョン:
settings.downloadOption.title=ダウンロードオプション(zip以外の単一ファイル):
settings.downloadOption.1=同じウィンドウで開く
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=ユーザーを無効/有効にする
adminUserSettings.usernameInfo=ユーザー名には、文字、数字、および次の特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
adminUserSettings.role=ロール
adminUserSettings.actions=アクション
adminUserSettings.apiUser=限定されたAPIユーザー
adminUserSettings.extraApiUser=追加の制限付きAPIユーザー
adminUserSettings.webOnlyUser=ウェブ専用ユーザー
adminUserSettings.demoUser=デモユーザー (カスタム設定なし)
adminUserSettings.internalApiUser=内部APIユーザー
adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する
@@ -599,11 +570,11 @@ rotate.tags=server side
home.imageToPdf.title=画像をPDFに変換
home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDFを画像に変換
home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=整理
home.pdfOrganiser.desc=ページの削除/並べ替えします。
@@ -366,38 +366,6 @@ navbar.sections.popular=인기
settings.title=설정
settings.update=업데이트 가능
settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=앱 버전:
settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용):
settings.downloadOption.1=같은 창에서 열기
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=사용자를 비활성화/활성화하
adminUserSettings.usernameInfo=사용자 이름은 문자, 숫자 및 @._+- 특수문자만 포함하거나 유효한 이메일 주소여야 합니다.
adminUserSettings.role=역할
adminUserSettings.actions=작업
adminUserSettings.apiUser=제한된 API 사용자
adminUserSettings.extraApiUser=추가 제한된 API 사용자
adminUserSettings.webOnlyUser=웹 전용 사용자
adminUserSettings.demoUser=데모 사용자 (사용자 지정 설정 없음)
adminUserSettings.internalApiUser=내부 API 사용자
adminUserSettings.forceChange=로그인 시 사용자 비밀번호 변경 강제
@@ -366,38 +366,6 @@ navbar.sections.popular=ജനപ്രിയം
settings.title=ക്രമീകരണങ്ങൾ
settings.update=അപ്ഡേറ്റ് ലഭ്യമാണ്
settings.updateAvailable={0} നിലവിൽ ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പാണ്. ഒരു പുതിയ പതിപ്പ് ({1}) ലഭ്യമാണ്.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=ആപ്പ് പതിപ്പ്:
settings.downloadOption.title=ഡൗൺലോഡ് ഓപ്ഷൻ തിരഞ്ഞെടുക്കുക (സിംഗിൾ ഫയൽ നോൺ-സിപ്പ് ഡൗൺലോഡുകൾക്ക്):
settings.downloadOption.1=ഒരേ വിൻഡോയിൽ തുറക്കുക
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=ഉപയോക്താവിനെ
adminUserSettings.usernameInfo=ഉപയോക്തൃനാമത്തിൽ അക്ഷരങ്ങൾ, അക്കങ്ങൾ, താഴെ പറയുന്ന പ്രത്യേക പ്രതീകങ്ങൾ @._+- എന്നിവ മാത്രമേ ഉണ്ടാകാവൂ അല്ലെങ്കിൽ സാധുവായ ഒരു ഇമെയിൽ വിലാസം ആയിരിക്കണം.
adminUserSettings.role=റോൾ
adminUserSettings.actions=പ്രവർത്തനങ്ങൾ
adminUserSettings.apiUser=പരിമിതമായ API ഉപയോക്താവ്
adminUserSettings.extraApiUser=അധിക പരിമിതമായ API ഉപയോക്താവ്
adminUserSettings.webOnlyUser=വെബ് മാത്രം ഉപയോക്താവ്
adminUserSettings.demoUser=ഡെമോ ഉപയോക്താവ് (ഇഷ്ടാനുസൃത ക്രമീകരണങ്ങളില്ല)
adminUserSettings.internalApiUser=ആന്തരിക API ഉപയോക്താവ്
adminUserSettings.forceChange=ലോഗിൻ ചെയ്യുമ്പോൾ പാസ്വേഡ് മാറ്റാൻ ഉപയോക്താവിനെ നിർബന്ധിക്കുക
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Instellingen
settings.update=Update beschikbaar
settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App versie:
settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip):
settings.downloadOption.1=Open in hetzelfde venster
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Gebruikersnaam kan alleen letters, nummers en de volgende speciale tekens @._+- bevatten of moet een geldig emailadres zijn.
adminUserSettings.role=Rol
adminUserSettings.actions=Acties
adminUserSettings.apiUser=Beperkte API gebruiker
adminUserSettings.extraApiUser=Extra beperkte API gebruiker
adminUserSettings.webOnlyUser=Alleen web gebruiker
adminUserSettings.demoUser=Demogebruiker (geen aangepaste instellingen)
adminUserSettings.internalApiUser=Interne API gebruiker
adminUserSettings.forceChange=Forceer gebruiker om gebruikersnaam/wachtwoord te wijzigen bij inloggen
@@ -366,38 +366,6 @@ navbar.sections.popular=Populært
settings.title=Innstillinger
settings.update=Oppdatering tilgjengelig
settings.updateAvailable={0} er den nåværende installerte versjonen. En ny versjon ({1}) er tilgjengelig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Versjon:
settings.downloadOption.title=Velg nedlastingsalternativ (For enkeltfil ikke-zip nedlastinger):
settings.downloadOption.1=Åpne i samme vindu
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Skal brukeren deaktiveres/aktiveres?
adminUserSettings.usernameInfo=Brukernavn kan bare inneholde bokstaver, tall og følgende spesialtegn @._+- eller må være en gyldig e-postadresse.
adminUserSettings.role=Rolle
adminUserSettings.actions=Handlinger
adminUserSettings.apiUser=Begrenset API Bruker
adminUserSettings.extraApiUser=Ekstra Begrenset API Bruker
adminUserSettings.webOnlyUser=Kun Web Bruker
adminUserSettings.demoUser=Demo Bruker (Ingen tilpassede innstillinger)
adminUserSettings.internalApiUser=Intern API Bruker
adminUserSettings.forceChange=Tving bruker til å endre passord ved innlogging
@@ -366,38 +366,6 @@ navbar.sections.popular=Popularne
settings.title=Ustawienia
settings.update=Dostępna aktualizacja
settings.updateAvailable=Wersja {0} jest obecenia zainstalowana, dostępna jest nowa wersja ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Wersja aplikacji:
settings.downloadOption.title=Wybierz opcję pobierania (w przypadku pobierania pojedynczych plików innych niż ZIP):
settings.downloadOption.1=Otwórz w tym samym oknie
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Czy użytkownik powinien zostać wył
adminUserSettings.usernameInfo=Niewłaściwa nazwa użytkownika - musi zawierać litery, cyfry i @._+- LUB być adresem email.
adminUserSettings.role=Rola
adminUserSettings.actions=Akcje
adminUserSettings.apiUser=Ograniczony Użytkownik API
adminUserSettings.extraApiUser=Dodatkowy ograniczony Użytkownik API
adminUserSettings.webOnlyUser=Użytkownik tylko WEB
adminUserSettings.demoUser=Użytkownik DEMO
adminUserSettings.internalApiUser=Wewnętrzny użytkownik API
adminUserSettings.forceChange=Wymuś zmianę hasło po zalogowaniu
@@ -366,38 +366,6 @@ navbar.sections.popular=Populares
settings.title=Configurações
settings.update=Atualização disponível
settings.updateAvailable={0} é a versão atualmente instalada. Uma nova versão ({1}) está disponível.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versão do Aplicativo:
settings.downloadOption.title=Escolha a opção de download (para download de arquivo único, não compactados):
settings.downloadOption.1=Abrir na mesma janela
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=O usuário deve ser desabilitado/habil
adminUserSettings.usernameInfo=Nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um e-mail válido.
adminUserSettings.role=Função
adminUserSettings.actions=Ações
adminUserSettings.apiUser=Usuário de API limitado
adminUserSettings.extraApiUser=Usuário de API limitado adicional
adminUserSettings.webOnlyUser=Usuário web apenas
adminUserSettings.demoUser=Usuário demo (Sem configurações personalizadas)
adminUserSettings.internalApiUser=Usuário de API interno
adminUserSettings.forceChange=Forçar usuário a trocar a senha ao iniciar sessão
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Definições
settings.update=Atualização disponível
settings.updateAvailable={0} é a versão atual instalada. Uma nova versão ({1}) está disponível.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versão da Aplicação:
settings.downloadOption.title=Escolha a opção de download (Para downloads de ficheiro único não zipado):
settings.downloadOption.1=Abrir na mesma janela
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Deve o utilizador ser desativado/ativa
adminUserSettings.usernameInfo=O nome de utilizador só pode conter letras, números e os seguintes caracteres especiais @._+- ou deve ser um endereço de email válido.
adminUserSettings.role=Função
adminUserSettings.actions=Ações
adminUserSettings.apiUser=Utilizador API Limitado
adminUserSettings.extraApiUser=Utilizador API Limitado Adicional
adminUserSettings.webOnlyUser=Utilizador Apenas Web
adminUserSettings.demoUser=Utilizador Demo (Sem Definições Personalizadas)
adminUserSettings.internalApiUser=Utilizador API Interno
adminUserSettings.forceChange=Forçar utilizador a alterar palavra-passe no login
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Setări
settings.update=Actualizare disponibilă
settings.updateAvailable={0} este versiunea instalată curent. O nouă versiune ({1}) este disponibilă.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versiune aplicație:
settings.downloadOption.title=Alege opțiunea de descărcare (pentru descărcarea unui singur fișier non-zip):
settings.downloadOption.1=Deschide în aceeași fereastră
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Ar trebui dezactivat/activat utilizato
adminUserSettings.usernameInfo=Numele de utilizator poate conține doar litere, numere și următoarele caractere speciale @._+- sau trebuie să fie o adresă de email validă.
adminUserSettings.role=Rol
adminUserSettings.actions=Acțiuni
adminUserSettings.apiUser=Utilizator API Limitat
adminUserSettings.extraApiUser=Utilizator API Limitat Suplimentar
adminUserSettings.webOnlyUser=Utilizator Doar Web
adminUserSettings.demoUser=Utilizator Demo (Fără setări personalizate)
adminUserSettings.internalApiUser=Utilizator API Intern
adminUserSettings.forceChange=Forțează utilizatorul să schimbe parola la conectare
@@ -366,38 +366,6 @@ navbar.sections.popular=Популярное
settings.title=Настройки
settings.update=Доступно обновление
settings.updateAvailable=Текущая установленная версия - {0}. Доступна новая версия ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версия приложения:
settings.downloadOption.title=Выберите вариант загрузки (для одиночных файлов без архивации):
settings.downloadOption.1=Открыть в том же окне
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Отключить/включить п
adminUserSettings.usernameInfo=Имя пользователя может содержать только буквы, цифры и следующие специальные символы @._+- или должно быть действительным адресом электронной почты.
adminUserSettings.role=Роль
adminUserSettings.actions=Действия
adminUserSettings.apiUser=Ограниченный пользователь API
adminUserSettings.extraApiUser=Дополнительный ограниченный пользователь API
adminUserSettings.webOnlyUser=Только веб-пользователь
adminUserSettings.demoUser=Демо-пользователь (без настраиваемых параметров)
adminUserSettings.internalApiUser=Внутренний пользователь API
adminUserSettings.forceChange=Требовать смену пароля при входе
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Nastavenia
settings.update=Dostupná aktualizácia
settings.updateAvailable={0} je aktuálne nainštalovaná verzia. Nová verzia ({1}) je dostupná.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzia aplikácie:
settings.downloadOption.title=Vyberte možnosť sťahovania (Pre jednotlivé neskomprimované súbory):
settings.downloadOption.1=Otvoriť v rovnakom okne
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Používateľské meno musí obsahovať iba písmená a čísla, žiadne medzery alebo špeciálne znaky.
adminUserSettings.role=Rola
adminUserSettings.actions=Akcie
adminUserSettings.apiUser=Obmedzený API používateľ
adminUserSettings.extraApiUser=Ďalší obmedzený API používateľ
adminUserSettings.webOnlyUser=Používateľ iba pre web
adminUserSettings.demoUser=Demo používateľ (Bez vlastných nastavení)
adminUserSettings.internalApiUser=Interný API používateľ
adminUserSettings.forceChange=Donútiť používateľa zmeniť heslo pri prihlásení
@@ -366,38 +366,6 @@ navbar.sections.popular=Priljubljeno
settings.title=Nastavitve
settings.update=Na voljo je posodobitev
settings.updateAvailable={0} je trenutno nameščena različica. Na voljo je nova različica ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Različica aplikacije:
settings.downloadOption.title=Izberi možnost prenosa (za prenose ene datoteke brez zip):
settings.downloadOption.1=Odpri v istem oknu
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Ali naj bo uporabnik onemogočen/omogo
adminUserSettings.usernameInfo=Uporabniško ime lahko vsebuje samo črke, številke in naslednje posebne znake @._+- ali mora biti veljaven e-poštni naslov.
adminUserSettings.role=Vloga
adminUserSettings.actions=Dejanja
adminUserSettings.apiUser=Omejen uporabnik API-ja
adminUserSettings.extraApiUser=Dodatni omejeni uporabnik API-ja
adminUserSettings.webOnlyUser=Samo spletni uporabnik
adminUserSettings.demoUser=Demo uporabnik (brez nastavitev po meri)
adminUserSettings.internalApiUser=Notranji uporabnik API-ja
adminUserSettings.forceChange=Prisili uporabnika, da spremeni geslo ob prijavi
@@ -366,38 +366,6 @@ navbar.sections.popular=Popularno
settings.title=Podešavanja
settings.update=Dostupno ažuriranje
settings.updateAvailable={0} je trenutno instalirana verzija. Nova verzija ({1}) je dostupna.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzija aplikacije:
settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih fajlova bez zip formata):
settings.downloadOption.1=Otvori u istom prozoru
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Da onemogućim/omogućim korisnika?
adminUserSettings.usernameInfo=Korisničko ime može sadržati samo slova, brojeve i specijalne karaktere @._+- ili mora biti validna email adresa.
adminUserSettings.role=Uloga
adminUserSettings.actions=Akcije
adminUserSettings.apiUser=Korisnik s ograničenim API pristupom
adminUserSettings.extraApiUser=Dodatni ograničeni API korisnik
adminUserSettings.webOnlyUser=Korisnik samo za web
adminUserSettings.demoUser=Demo korisnik (Bez prilagođenih podešavanja)
adminUserSettings.internalApiUser=Interni API korisnik
adminUserSettings.forceChange=Prisili korisnika da promeni korisničko ime/lozinku pri prijavi
@@ -366,38 +366,6 @@ navbar.sections.popular=Populära
settings.title=Inställningar
settings.update=Uppdatering tillgänglig
settings.updateAvailable={0} är den aktuella installerade versionen. En ny version ({1}) finns tillgänglig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Appversion:
settings.downloadOption.title=Välj nedladdningsalternativ (för nedladdning av en fil utan zip):
settings.downloadOption.1=Öppnas i samma fönster
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Ska användaren inaktiveras/aktiveras?
adminUserSettings.usernameInfo=Användarnamn kan endast innehålla bokstäver, siffror och följande specialtecken @._+- eller måste vara en giltig e-postadress.
adminUserSettings.role=Roll
adminUserSettings.actions=Åtgärder
adminUserSettings.apiUser=Begränsad API-användare
adminUserSettings.extraApiUser=Ytterligare begränsad API-användare
adminUserSettings.webOnlyUser=Endast webbanvändare
adminUserSettings.demoUser=Demoanvändare (Inga anpassade inställningar)
adminUserSettings.internalApiUser=Intern API-användare
adminUserSettings.forceChange=Tvinga användare att ändra lösenord vid inloggning
@@ -366,38 +366,6 @@ navbar.sections.popular=ยอดนิยม
settings.title=การตั้งค่า
settings.update=มีการอัปเดต
settings.updateAvailable={0} คือเวอร์ชันที่ติดตั้งในปัจจุบัน มีเวอร์ชันใหม่ ({1}) พร้อมให้บริการ
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=เวอร์ชันแอป:
settings.downloadOption.title=เลือกตัวเลือกการดาวน์โหลด (สำหรับการดาวน์โหลดไฟล์เดียวที่ไม่ใช่ zip):
settings.downloadOption.1=เปิดในหน้าต่างเดียวกัน
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=ผู้ใช้นี้ควร
adminUserSettings.usernameInfo=ชื่อผู้ใช้สามารถประกอบด้วยตัวอักษร ตัวเลข และอักขระพิเศษต่อไปนี้ @._+- หรือจะต้องเป็นที่อยู่อีเมลที่ถูกต้อง
adminUserSettings.role=บทบาท
adminUserSettings.actions=การดำเนินการ
adminUserSettings.apiUser=ผู้ใช้ API จำกัด
adminUserSettings.extraApiUser=ผู้ใช้ API เพิ่มเติม
adminUserSettings.webOnlyUser=ผู้ใช้เว็บเท่านั้น
adminUserSettings.demoUser=ผู้ใช้ทดลอง (ไม่มีการตั้งค่าปรับแต่ง)
adminUserSettings.internalApiUser=ผู้ใช้ API ภายใน
adminUserSettings.forceChange=บังคับให้ผู้ใช้เปลี่ยนรหัสผ่านในการเข้าสู่ระบบ
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Ayarlar
settings.update=Güncelleme mevcut
settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Uygulama Sürümü:
settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için):
settings.downloadOption.1=Aynı pencerede aç
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Kullanıcı devre dışı bırakılmal
adminUserSettings.usernameInfo=Kullanıcı adı yalnızca harf, rakam ve aşağıdaki özel karakterleri @._+- içerebilir veya geçerli bir e-posta adresi olmalıdır.
adminUserSettings.role=Rol
adminUserSettings.actions=Eylemler
adminUserSettings.apiUser=Sınırlı API Kullanıcısı
adminUserSettings.extraApiUser=Ek Sınırlı API Kullanıcısı
adminUserSettings.webOnlyUser=Sadece Web Kullanıcısı
adminUserSettings.demoUser=Demo Kullanıcısı (Özel ayar yok)
adminUserSettings.internalApiUser=Dahili API Kullanıcısı
adminUserSettings.forceChange=Kullanıcının girişte kullanıcı adı/şifre değiştirmesini zorla
@@ -366,38 +366,6 @@ navbar.sections.popular=Популярне
settings.title=Налаштування
settings.update=Доступне оновлення
settings.updateAvailable=Зараз встановлена версія {0}. Нова версія ({1}) доступна.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версія додатку:
settings.downloadOption.title=Виберіть варіант завантаження (для завантаження одного файлу без zip):
settings.downloadOption.1=Відкрити в тому ж вікні
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Чи потрібно вимкнути
adminUserSettings.usernameInfo=Ім’я користувача може містити лише літери, цифри та наступні спеціальні символи @._+- або має бути дійсною електронною адресою.
adminUserSettings.role=Роль
adminUserSettings.actions=Дії
adminUserSettings.apiUser=Обмежений користувач API
adminUserSettings.extraApiUser=Додатковий обмежений користувач API
adminUserSettings.webOnlyUser=Тільки веб-користувач
adminUserSettings.demoUser=Демо-користувач (без налаштованих параметрів)
adminUserSettings.internalApiUser=Внутрішній користувач API
adminUserSettings.forceChange=Примусити користувача змінити пароль при вході в систему
@@ -366,38 +366,6 @@ navbar.sections.popular=Popular
settings.title=Cài đặt
settings.update=Có bản cập nhật
settings.updateAvailable={0} là phiên bản hiện tại đã cài đặt. Một phiên bản mới ({1}) đã có sẵn.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Phiên bản ứng dụng:
settings.downloadOption.title=Chọn tùy chọn tải xuống (Đối với tải xuống tệp đơn không nén):
settings.downloadOption.1=Mở trong cùng cửa sổ
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Tên người dùng chỉ có thể chứa chữ cái, số và các ký tự đặc biệt sau @._+- hoặc phải là một địa chỉ email hợp lệ.
adminUserSettings.role=Vai trò
adminUserSettings.actions=Hành động
adminUserSettings.apiUser=Người dùng API giới hạn
adminUserSettings.extraApiUser=Người dùng API giới hạn bổ sung
adminUserSettings.webOnlyUser=Chỉ người dùng web
adminUserSettings.demoUser=Người dùng demo (Không có cài đặt tùy chỉnh)
adminUserSettings.internalApiUser=Người dùng API nội bộ
adminUserSettings.forceChange=Buộc người dùng thay đổi mật khẩu khi đăng nhập
@@ -366,38 +366,6 @@ navbar.sections.popular=热门
settings.title=设置
settings.update=有可用的更新
settings.updateAvailable=当前版本为 {0},新版本 ({1}) 可用。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=应用程序版本:
settings.downloadOption.title=选择下载选项(单个文件非压缩文件):
settings.downloadOption.1=在同一窗口打开
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=是否应禁用/启用该用户?
adminUserSettings.usernameInfo=用户名只能包含字母、数字和以下特殊字符@._+-,或者必须是有效的电子邮件地址。
adminUserSettings.role=角色
adminUserSettings.actions=操作
adminUserSettings.apiUser=受限制的 API 用户
adminUserSettings.extraApiUser=额外受限制的 API 用户
adminUserSettings.webOnlyUser=仅限 Web 用户
adminUserSettings.demoUser=演示用户(无自定义设置)
adminUserSettings.internalApiUser=内部 API 用户
adminUserSettings.forceChange=强制用户在登录时更改用户名/密码
@@ -366,38 +366,6 @@ navbar.sections.popular=熱門功能
settings.title=設定
settings.update=有更新可用
settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=應用程式版本:
settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載):
settings.downloadOption.1=在同一視窗中開啟
@@ -455,6 +423,9 @@ adminUserSettings.confirmChangeUserStatus=是否要停用/啟用此使用者?
adminUserSettings.usernameInfo=使用者名稱只能包含字母、數字和以下特殊字元 @._+- 或必須是有效的電子郵件地址。
adminUserSettings.role=角色
adminUserSettings.actions=操作
adminUserSettings.apiUser=受限制的 API 使用者
adminUserSettings.extraApiUser=額外受限制的 API 使用者
adminUserSettings.webOnlyUser=僅網頁版使用者
adminUserSettings.demoUser=示範使用者(無自訂設定)
adminUserSettings.internalApiUser=內部 API 使用者
adminUserSettings.forceChange=強制使用者在登入時變更密碼
@@ -336,12 +336,6 @@
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-psd",
"moduleVersion": "3.12.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
"moduleVersion": "3.12.0",
@@ -629,21 +623,21 @@
{
"moduleName": "io.swagger.core.v3:swagger-annotations-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations",
"moduleVersion": "2.2.35",
"moduleVersion": "2.2.34",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.swagger.core.v3:swagger-core-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core",
"moduleVersion": "2.2.35",
"moduleVersion": "2.2.34",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.swagger.core.v3:swagger-models-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models",
"moduleVersion": "2.2.35",
"moduleVersion": "2.2.34",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@@ -986,13 +980,13 @@
},
{
"moduleName": "org.commonmark:commonmark",
"moduleVersion": "0.25.1",
"moduleVersion": "0.25.0",
"moduleLicense": "BSD-2-Clause",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},
{
"moduleName": "org.commonmark:commonmark-ext-gfm-tables",
"moduleVersion": "0.25.1",
"moduleVersion": "0.25.0",
"moduleLicense": "BSD-2-Clause",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},
@@ -16,96 +16,21 @@ function compareVersions(version1, version2) {
return 0;
}
function getDownloadUrl() {
// Only show download for non-Docker installations
if (machineType === 'Docker' || machineType === 'Kubernetes') {
return null;
}
const baseUrl = 'https://files.stirlingpdf.com/';
// Determine file based on machine type and security
if (machineType === 'Server-jar') {
return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar');
}
// Client installations
if (machineType.startsWith('Client-')) {
const os = machineType.replace('Client-', ''); // win, mac, unix
const type = activeSecurity ? '-server-security' : '-server';
if (os === 'unix') {
return baseUrl + os + type + '.jar';
} else if (os === 'win') {
return baseUrl + os + '-installer.exe';
} else if (os === 'mac') {
return baseUrl + os + '-installer.dmg';
}
}
return null;
}
// Function to get translated priority text
function getTranslatedPriority(priority) {
switch(priority?.toLowerCase()) {
case 'urgent': return updatePriorityUrgent;
case 'normal': return updatePriorityNormal;
case 'minor': return updatePriorityMinor;
case 'low': return updatePriorityLow;
default: return priority?.toUpperCase() || 'NORMAL';
}
}
async function getUpdateSummary() {
// Map Java License enum to API types
let type = 'normal';
if (licenseType === 'PRO') {
type = 'pro';
} else if (licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`;
console.log("Fetching update summary from:", url);
async function getLatestReleaseVersion() {
const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest";
try {
const response = await fetch(url);
console.log("Response status:", response.status);
if (response.status === 200) {
const data = await response.json();
return data;
return data.tag_name ? data.tag_name.substring(1) : "";
} else {
console.error("Failed to fetch update summary from Supabase:", response.status);
return null;
// If the status is not 200, try to get the version from build.gradle
return await getCurrentVersionFromBypass();
}
} catch (error) {
console.error("Failed to fetch update summary from Supabase:", error);
return null;
}
}
async function getFullUpdateInfo() {
// Map Java License enum to API types
let type = 'normal';
if (licenseType === 'PRO') {
type = 'pro';
} else if (licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`;
console.log("Fetching full update info from:", url);
try {
const response = await fetch(url);
console.log("Full update response status:", response.status);
if (response.status === 200) {
const data = await response.json();
return data;
} else {
console.error("Failed to fetch full update info from Supabase:", response.status);
return null;
}
} catch (error) {
console.error("Failed to fetch full update info from Supabase:", error);
return null;
console.error("Failed to fetch latest version from GitHub:", error);
// If an error occurs, try to get the version from build.gradle
return await getCurrentVersionFromBypass();
}
}
@@ -135,7 +60,6 @@ async function checkForUpdate() {
var updateLinkLegacy = document.getElementById("update-link-legacy") || null;
if (updateBtn !== null) {
updateBtn.style.display = "none";
updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary");
}
if (updateLink !== null) {
updateLink.style.display = "none";
@@ -147,47 +71,19 @@ async function checkForUpdate() {
}
}
const updateSummary = await getUpdateSummary();
if (!updateSummary) {
console.log("No update summary available");
return;
}
console.log("updateSummary=", updateSummary);
const latestVersion = await getLatestReleaseVersion();
console.log("latestVersion=" + latestVersion);
console.log("currentVersion=" + currentVersion);
console.log("latestVersion=" + updateSummary.latest_version);
if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) {
const priority = updateSummary.max_priority || 'normal';
console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion));
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
if (updateBtn != null) {
// Style button based on priority
if (priority === 'urgent') {
updateBtn.classList.add("btn-danger");
updateBtn.innerHTML = urgentUpdateAvailable;
} else if (priority === 'normal') {
updateBtn.classList.add("btn-warning");
updateBtn.innerHTML = updateAvailableText;
} else {
updateBtn.classList.add("btn-outline-primary");
updateBtn.innerHTML = updateAvailableText;
}
// Store summary for initial display
updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary));
updateBtn.style.display = "block";
// Add click handler for update details modal
updateBtn.onclick = function(e) {
e.preventDefault();
showUpdateModal();
};
document.getElementById("update-btn").style.display = "block";
}
if (updateLink !== null) {
document.getElementById("update-link").style.display = "flex";
}
if (updateLinkLegacy !== null) {
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + updateSummary.latest_version + '</b>');
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
if (updateLinkLegacy.classList.contains("visually-hidden")) {
updateLinkLegacy.classList.remove("visually-hidden");
}
@@ -203,188 +99,6 @@ async function checkForUpdate() {
}
}
async function showUpdateModal() {
// Close settings modal if open
const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if (settingsModal) {
settingsModal.hide();
}
// Get summary data from button
const updateBtn = document.getElementById("update-btn");
const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary'));
// Utility function to escape HTML special characters
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\//g, '&#x2F;');
}
// Create initial modal with loading state
const initialModalHtml = `
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document" style="max-height: 80vh;">
<div class="modal-content" style="max-height: 80vh;">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">${updateModalTitle}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body" id="updateModalBody" style="max-height: 60vh; overflow-y: auto;">
<div class="update-summary mb-4">
<div class="row mb-3">
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
<small class="text-muted">${updateCurrent}</small><br>
<strong>${escapeHtml(currentVersion)}</strong>
</div>
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
<small class="text-muted">${updateLatest}</small><br>
<strong class="text-primary">${escapeHtml(summaryData.latest_version)}</strong>
</div>
${summaryData.latest_stable_version ? `
<div class="col-4 text-center">
<small class="text-muted">${updateLatestStable}</small><br>
<strong class="text-success">${escapeHtml(summaryData.latest_stable_version)}</strong>
</div>
` : ''}
</div>
<div class="alert ${summaryData.max_priority === 'urgent' ? 'alert-danger' : 'alert-warning'}" role="alert">
<strong>${updatePriority}:</strong> ${getTranslatedPriority(summaryData.max_priority)}
${summaryData.recommended_action ? `<br><strong>${updateRecommendedAction}:</strong> ${escapeHtml(summaryData.recommended_action)}` : ''}
</div>
</div>
${summaryData.any_breaking ? `
<div class="alert alert-warning" role="alert">
<h6><strong>${updateBreakingChangesDetected}</strong></h6>
<p>${updateBreakingChangesMessage}</p>
</div>
` : ''}
${summaryData.migration_guides && summaryData.migration_guides.length > 0 ? `
<div class="migration-guides mb-4">
<h6>${updateMigrationGuides}</h6>
<ul class="list-group">
${summaryData.migration_guides.map(guide => `
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>${updateVersion} ${escapeHtml(guide.version)}:</strong> ${escapeHtml(guide.notes)}
</div>
<a href="${escapeHtml(guide.url)}" target="_blank" class="btn btn-sm btn-outline-primary">${updateViewGuide}</a>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="text-center">
<div class="spinner-border text-primary" role="status" id="loadingSpinner">
<span class="visually-hidden">${updateLoadingDetailedInfo}</span>
</div>
<p class="mt-2">${updateLoadingDetailedInfo}</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${updateClose}</button>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" class="btn btn-outline-primary">${updateViewAllReleases}</a>
${getDownloadUrl() ? `<a href="${escapeHtml(getDownloadUrl())}" class="btn btn-success" target="_blank">${updateDownloadLatest}</a>` : ''}
</div>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.getElementById('updateModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', initialModalHtml);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
modal.show();
// Fetch full update info
const fullUpdateInfo = await getFullUpdateInfo();
// Update modal with full information
const modalBody = document.getElementById('updateModalBody');
if (fullUpdateInfo && fullUpdateInfo.new_versions) {
const storedMode = localStorage.getItem("dark-mode");
const isDarkMode = storedMode === "on" ||
(storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
const darkClasses = isDarkMode ? {
accordionItem: 'bg-dark border-secondary text-light',
accordionButton: 'bg-dark text-light border-secondary',
accordionBody: 'bg-dark text-light'
} : {
accordionItem: '',
accordionButton: '',
accordionBody: ''
};
const detailedVersionsHtml = `
<div class="detailed-versions mt-4">
<h6>${updateAvailableUpdates}</h6>
<div class="accordion" id="versionsAccordion">
${fullUpdateInfo.new_versions.map((version, index) => `
<div class="accordion-item" style="border-color: var(--md-sys-color-outline);">
<h2 class="accordion-header" id="heading${index}">
<button class="accordion-button ${index === 0 ? '' : 'collapsed'}" style="color: var(--md-sys-color-on-surface); background-color:
var(--md-sys-color-surface);" type="button" data-bs-toggle="collapse"
data-bs-target="#collapse${index}" aria-expanded="${index === 0 ? 'true' : 'false'}" aria-controls="collapse${index}">
<div class="d-flex justify-content-between w-100 me-3">
<span><strong>${updateVersion} ${version.version}</strong></span>
<span class="badge ${version.priority === 'urgent' ? 'bg-danger' : version.priority === 'normal' ? 'bg-warning' : 'bg-secondary'}">${getTranslatedPriority(version.priority)}</span>
</div>
</button>
</h2>
<div id="collapse${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
aria-labelledby="heading${index}" data-bs-parent="#versionsAccordion">
<div class="accordion-body" style="color: var(--md-sys-color-on-surface); background-color:
var(--md-sys-color-surface-bright);">
<h6>${version.announcement.title}</h6>
<p>${version.announcement.message}</p>
${version.compatibility.breaking_changes ? `
<div class="alert alert-warning alert-sm" role="alert">
<small><strong>⚠️ ${updateBreakingChanges}</strong> ${version.compatibility.breaking_description || updateBreakingChangesDefault}</small>
${version.compatibility.migration_guide_url ? `<br><a href="${version.compatibility.migration_guide_url}" target="_blank" class="btn btn-sm btn-outline-warning mt-1">${updateMigrationGuide}</a>` : ''}
</div>
` : ''}
</div>
</div>
</div>
`).join('')}
</div>
</div>
`;
// Remove loading spinner and add detailed info
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.parentElement.remove();
}
modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml);
} else {
// Remove loading spinner if failed to load
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.parentElement.innerHTML = `<p class="text-muted">${updateUnableToLoadDetails}</p>`;
}
}
}
document.addEventListener("DOMContentLoaded", (event) => {
checkForUpdate();
});
@@ -32,7 +32,7 @@
</div>
<!-- Admin Settings Banner (for admins only) -->
<div th:if="${isSystemAdmin}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
<div style="display: flex; align-items: center; gap: 1rem;">
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
@@ -22,7 +22,7 @@
<form id="imageToPDFForm" method="post" enctype="multipart/form-data"
th:action="@{'/api/v1/convert/img/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*,.psd', inputText=#{imgPrompt})}">
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*', inputText=#{imgPrompt})}">
</div>
<div class="mb-3">
<label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label>
@@ -11,44 +11,9 @@
</script>
<script th:inline="javascript">
const currentVersion = /*[[${@appVersion}]]*/ '';
const licenseType = /*[[${@license}]]*/ '';
const machineType = /*[[${@machineType}]]*/ '';
const activeSecurity = /*[[${@activeSecurity}]]*/ false;
const noFavourites = /*[[#{noFavourites}]]*/ '';
console.log(noFavourites);
const updateAvailable = /*[[#{settings.updateAvailable}]]*/ '';
// Update notification i18n constants
const urgentUpdateAvailable = /*[[#{update.urgentUpdateAvailable}]]*/ '🚨 Update Available';
const updateAvailableText = /*[[#{update.updateAvailable}]]*/ 'Update Available';
const updateModalTitle = /*[[#{update.modalTitle}]]*/ 'Update Available';
const updateCurrent = /*[[#{update.current}]]*/ 'Current';
const updateLatest = /*[[#{update.latest}]]*/ 'Latest';
const updateLatestStable = /*[[#{update.latestStable}]]*/ 'Latest Stable';
const updatePriority = /*[[#{update.priority}]]*/ 'Priority';
const updateRecommendedAction = /*[[#{update.recommendedAction}]]*/ 'Recommended Action';
const updateBreakingChangesDetected = /*[[#{update.breakingChangesDetected}]]*/ '⚠️ Breaking Changes Detected';
const updateBreakingChangesMessage = /*[[#{update.breakingChangesMessage}]]*/ 'This update contains breaking changes. Please review the migration guides below.';
const updateMigrationGuides = /*[[#{update.migrationGuides}]]*/ 'Migration Guides:';
const updateViewGuide = /*[[#{update.viewGuide}]]*/ 'View Guide';
const updateLoadingDetailedInfo = /*[[#{update.loadingDetailedInfo}]]*/ 'Loading detailed version information...';
const updateClose = /*[[#{update.close}]]*/ 'Close';
const updateViewAllReleases = /*[[#{update.viewAllReleases}]]*/ 'View All Releases';
const updateDownloadLatest = /*[[#{update.downloadLatest}]]*/ 'Download Latest';
const updateAvailableUpdates = /*[[#{update.availableUpdates}]]*/ 'Available Updates:';
const updateUnableToLoadDetails = /*[[#{update.unableToLoadDetails}]]*/ 'Unable to load detailed version information.';
const updateVersion = /*[[#{update.version}]]*/ 'Version';
// Update priority levels
const updatePriorityUrgent = /*[[#{update.priority.urgent}]]*/ 'URGENT';
const updatePriorityNormal = /*[[#{update.priority.normal}]]*/ 'NORMAL';
const updatePriorityMinor = /*[[#{update.priority.minor}]]*/ 'MINOR';
const updatePriorityLow = /*[[#{update.priority.low}]]*/ 'LOW';
// Breaking changes text
const updateBreakingChanges = /*[[#{update.breakingChanges}]]*/ 'Breaking Changes:';
const updateBreakingChangesDefault = /*[[#{update.breakingChangesDefault}]]*/ 'This version contains breaking changes';
const updateMigrationGuide = /*[[#{update.migrationGuide}]]*/ 'Migration Guide';
</script>
<script th:src="@{'/js/homecard.js'}"></script>
<script th:src="@{'/js/githubVersion.js'}"></script>
@@ -33,7 +33,7 @@ public class AdminJobController {
* @return Job statistics
*/
@GetMapping("/api/v1/admin/job/stats")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<JobStats> getJobStats() {
JobStats stats = taskManager.getJobStats();
log.info(
@@ -49,7 +49,7 @@ public class AdminJobController {
* @return Queue statistics
*/
@GetMapping("/api/v1/admin/job/queue/stats")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<?> getQueueStats() {
Map<String, Object> queueStats = jobQueue.getQueueStats();
log.info("Admin requested queue stats: {} queued jobs", queueStats.get("queuedJobs"));
@@ -62,7 +62,7 @@ public class AdminJobController {
* @return A response indicating how many jobs were cleaned up
*/
@PostMapping("/api/v1/admin/job/cleanup")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResponseEntity<?> cleanupOldJobs() {
int beforeCount = taskManager.getJobStats().getTotalJobs();
taskManager.cleanupOldJobs();
@@ -48,7 +48,7 @@ import stirling.software.proprietary.security.config.EnterpriseEndpoint;
@Slf4j
@Controller
@RequestMapping("/audit")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ADMIN')")
@RequiredArgsConstructor
@EnterpriseEndpoint
public class AuditDashboardController {
@@ -1,45 +0,0 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "organizations")
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class Organization implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "org_id")
private Long id;
@Column(name = "name", unique = true, nullable = false)
private String name;
@Column(name = "description")
private String description;
@OneToMany(mappedBy = "organization", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Team> teams = new HashSet<>();
public void addTeam(Team team) {
teams.add(team);
team.setOrganization(this);
}
public void removeTeam(Team team) {
teams.remove(team);
team.setOrganization(null);
}
}
@@ -11,7 +11,7 @@ import lombok.*;
import stirling.software.proprietary.security.model.User;
@Entity
@Table(name = "teams", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "org_id"}))
@Table(name = "teams")
@NoArgsConstructor
@Getter
@Setter
@@ -26,15 +26,9 @@ public class Team implements Serializable {
@Column(name = "team_id")
private Long id;
@Column(name = "name", nullable = false)
@Column(name = "name", unique = true, nullable = false)
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(
name = "org_id",
nullable = true) // Nullable for backward compatibility during migration
private Organization organization;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<User> users = new HashSet<>();
@@ -1,15 +0,0 @@
package stirling.software.proprietary.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationWithTeamCountDTO {
private Long id;
private String name;
private String description;
private Long teamCount;
}
@@ -15,12 +15,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.DatabaseServiceInterface;
import stirling.software.proprietary.security.service.OrganizationService;
import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.service.UserService;
@@ -31,8 +28,6 @@ public class InitialSecuritySetup {
private final UserService userService;
private final TeamService teamService;
private final OrganizationService organizationService;
private final TeamRepository teamRepository;
private final ApplicationProperties applicationProperties;
private final DatabaseServiceInterface databaseService;
@@ -49,9 +44,6 @@ public class InitialSecuritySetup {
}
userService.migrateOauth2ToSSO();
migrateAdminRolesToSystemAdmin();
migrateDeprecatedRolesToUser();
assignTeamsToDefaultOrganizationIfMissing();
assignUsersToDefaultTeamIfMissing();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
@@ -60,49 +52,6 @@ public class InitialSecuritySetup {
}
}
private void assignTeamsToDefaultOrganizationIfMissing() {
// Find teams without organizations (legacy teams from before org feature)
List<Team> teamsWithoutOrg = teamRepository.findTeamsWithoutOrganization();
if (teamsWithoutOrg.isEmpty()) {
log.debug("No teams without organizations found - migration not needed");
return;
}
log.info(
"Found {} teams without organizations. Starting migration...",
teamsWithoutOrg.size());
// Ensure default organizations exist
Organization defaultOrg = organizationService.getOrCreateDefaultOrganization();
Organization internalOrg = organizationService.getOrCreateInternalOrganization();
int migratedCount = 0;
for (Team team : teamsWithoutOrg) {
try {
// Assign internal team to internal org, all others to default org
if (TeamService.INTERNAL_TEAM_NAME.equals(team.getName())) {
team.setOrganization(internalOrg);
log.debug("Assigned team '{}' to internal organization", team.getName());
} else {
team.setOrganization(defaultOrg);
log.debug("Assigned team '{}' to default organization", team.getName());
}
teamRepository.save(team);
migratedCount++;
} catch (Exception e) {
log.error(
"Failed to migrate team '{}' to organization: {}",
team.getName(),
e.getMessage());
}
}
if (migratedCount > 0) {
log.info("Successfully migrated {} teams to organizations", migratedCount);
}
}
private void assignUsersToDefaultTeamIfMissing() {
Team defaultTeam = teamService.getOrCreateDefaultTeam();
Team internalTeam = teamService.getOrCreateInternalTeam();
@@ -137,7 +86,7 @@ public class InitialSecuritySetup {
Team team = teamService.getOrCreateDefaultTeam();
userService.saveUser(
initialUsername, initialPassword, team, Role.SYSTEM_ADMIN.getRoleId(), false);
initialUsername, initialPassword, team, Role.ADMIN.getRoleId(), false);
log.info("Admin user created: {}", initialUsername);
} else {
createDefaultAdminUser();
@@ -151,7 +100,7 @@ public class InitialSecuritySetup {
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
Team team = teamService.getOrCreateDefaultTeam();
userService.saveUser(
defaultUsername, defaultPassword, team, Role.SYSTEM_ADMIN.getRoleId(), true);
defaultUsername, defaultPassword, team, Role.ADMIN.getRoleId(), true);
log.info("Default admin user created: {}", defaultUsername);
}
}
@@ -185,89 +134,4 @@ public class InitialSecuritySetup {
}
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
}
private void migrateAdminRolesToSystemAdmin() {
List<User> adminUsers = userService.findByRole(Role.ADMIN.getRoleId());
if (adminUsers.isEmpty()) {
log.debug("No ROLE_ADMIN users found - migration not needed");
return;
}
log.info("Found {} ROLE_ADMIN users. Converting to SYSTEM_ADMIN...", adminUsers.size());
int migratedCount = 0;
for (User user : adminUsers) {
try {
user.setUserRole(Role.SYSTEM_ADMIN);
userService.saveUser(user);
log.debug(
"Converted user '{}' from ROLE_ADMIN to SYSTEM_ADMIN", user.getUsername());
migratedCount++;
} catch (Exception e) {
log.error(
"Failed to migrate user '{}' from ROLE_ADMIN to SYSTEM_ADMIN: {}",
user.getUsername(),
e.getMessage());
}
}
if (migratedCount > 0) {
log.info(
"Successfully migrated {} users from ROLE_ADMIN to SYSTEM_ADMIN",
migratedCount);
}
}
private void migrateDeprecatedRolesToUser() {
String[] deprecatedRoles = {
"ROLE_WEB_ONLY_USER", "ROLE_EXTRA_LIMITED_API_USER", "ROLE_LIMITED_API_USER"
};
int totalMigrated = 0;
for (String deprecatedRole : deprecatedRoles) {
List<User> usersWithDeprecatedRole = userService.findByRole(deprecatedRole);
if (!usersWithDeprecatedRole.isEmpty()) {
log.info(
"Found {} users with role {}. Converting to USER...",
usersWithDeprecatedRole.size(),
deprecatedRole);
int migratedCount = 0;
for (User user : usersWithDeprecatedRole) {
try {
user.setUserRole(Role.USER);
userService.saveUser(user);
log.debug(
"Converted user '{}' from {} to USER",
user.getUsername(),
deprecatedRole);
migratedCount++;
} catch (Exception e) {
log.error(
"Failed to migrate user '{}' from {} to USER: {}",
user.getUsername(),
deprecatedRole,
e.getMessage());
}
}
if (migratedCount > 0) {
log.info(
"Successfully migrated {} users from {} to USER",
migratedCount,
deprecatedRole);
totalMigrated += migratedCount;
}
}
}
if (totalMigrated == 0) {
log.debug("No users with deprecated roles found - migration not needed");
} else {
log.info("Total users migrated from deprecated roles to USER: {}", totalMigrated);
}
}
}
@@ -7,7 +7,6 @@ import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -204,7 +203,7 @@ public class AccountWebController {
return "login";
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/usage")
public String showUsage() {
if (!runningEE) {
@@ -213,26 +212,13 @@ public class AccountWebController {
return "usage";
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/adminSettings")
public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAllWithTeam();
Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails();
// Filter role details to only show SYSTEM_ADMIN, USER, and DEMO_USER in UI
Map<String, String> filteredRoleDetails = new LinkedHashMap<>();
String[] allowedRoles = {
Role.SYSTEM_ADMIN.getRoleId(), Role.USER.getRoleId(), Role.DEMO_USER.getRoleId()
};
for (String roleId : allowedRoles) {
if (roleDetails.containsKey(roleId)) {
filteredRoleDetails.put(roleId, roleDetails.get(roleId));
}
}
roleDetails = filteredRoleDetails;
// Map to store session information and user activity status
Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>();
@@ -440,7 +426,6 @@ public class AccountWebController {
model.addAttribute("username", username);
model.addAttribute("messageType", messageType);
model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("isSystemAdmin", user.get().isSystemAdmin());
model.addAttribute("settings", settingsJson);
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
model.addAttribute("currentPage", "account");
@@ -8,7 +8,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -75,7 +74,6 @@ public class SecurityConfiguration {
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final Environment environment;
public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository,
@@ -93,8 +91,7 @@ public class SecurityConfiguration {
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
Environment environment) {
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
@@ -109,7 +106,6 @@ public class SecurityConfiguration {
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
this.environment = environment;
}
@Bean
@@ -133,49 +129,33 @@ public class SecurityConfiguration {
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf -> {
var csrfConfig =
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF
// (return false)
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
csrf ->
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
});
// Only ignore CSRF for H2 console if H2 console is enabled
if (isH2ConsoleEnabled()) {
csrfConfig = csrfConfig.ignoringRequestMatchers("/h2-console/**");
}
csrfConfig
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler);
});
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
})
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
// Allow H2 console frames only if H2 console is enabled
if (isH2ConsoleEnabled()) {
http.headers(
headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()));
}
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement(
@@ -235,9 +215,6 @@ public class SecurityConfiguration {
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| (isH2ConsoleEnabled()
&& trimmedUri.startsWith(
"/h2-console/"))
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
@@ -346,8 +323,4 @@ public class SecurityConfiguration {
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(persistentLoginRepository);
}
private boolean isH2ConsoleEnabled() {
return environment.getProperty("spring.h2.console.enabled", Boolean.class, false);
}
}
@@ -43,7 +43,7 @@ import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequ
@Tag(name = "Admin Settings", description = "Admin-only Settings Management APIs")
@RequestMapping("/api/v1/admin/settings")
@RequiredArgsConstructor
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@Slf4j
public class AdminSettingsController {
@@ -33,7 +33,7 @@ import stirling.software.proprietary.security.service.DatabaseService;
@Slf4j
@Controller
@RequestMapping("/api/v1/database")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@Conditional(H2SQLCondition.class)
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
@RequiredArgsConstructor
@@ -1,216 +0,0 @@
package stirling.software.proprietary.security.controller.api;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.config.PremiumEndpoint;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
@RestController
@RequestMapping("/api/v1/org-admin")
@Tag(name = "Organization Admin", description = "Organization Admin Management APIs")
@Slf4j
@RequiredArgsConstructor
@PremiumEndpoint
@PreAuthorize(
"@roleBasedAuthorizationService.canManageOrgUsers() or @roleBasedAuthorizationService.canManageOrgTeams()")
public class OrgAdminController {
private final TeamRepository teamRepository;
private final UserRepository userRepository;
private final RoleBasedAuthorizationService authorizationService;
/** Get all teams in the org admin's organization */
@GetMapping("/teams")
@PreAuthorize("@roleBasedAuthorizationService.canManageOrgTeams()")
public ResponseEntity<List<Team>> getOrganizationTeams() {
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getOrganization() == null) {
return ResponseEntity.badRequest().build();
}
List<Team> teams =
teamRepository.findByOrganizationId(currentUser.getOrganization().getId());
return ResponseEntity.ok(teams);
}
/** Get all users in the org admin's organization */
@GetMapping("/users")
public ResponseEntity<List<User>> getOrganizationUsers() {
if (!authorizationService.canManageOrgUsers()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getOrganization() == null) {
return ResponseEntity.badRequest().build();
}
// Get all users in teams belonging to this organization
List<Team> orgTeams =
teamRepository.findByOrganizationId(currentUser.getOrganization().getId());
List<User> orgUsers =
orgTeams.stream().flatMap(team -> team.getUsers().stream()).distinct().toList();
return ResponseEntity.ok(orgUsers);
}
/** Assign a user to a team within the organization */
@PostMapping("/assign-user-to-team")
@Transactional
public ResponseEntity<?> assignUserToTeam(
@RequestParam("userId") Long userId, @RequestParam("teamId") Long teamId) {
if (!authorizationService.canManageOrgUsers()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to manage organization users");
}
Optional<User> userOpt = userRepository.findById(userId);
Optional<Team> teamOpt = teamRepository.findById(teamId);
if (userOpt.isEmpty() || teamOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
Team team = teamOpt.get();
if (!authorizationService.canAddUserToTeam(userId, team)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to add user to this team");
}
// Assign user to team
user.setTeam(team);
userRepository.save(user);
return ResponseEntity.ok().body("User assigned to team successfully");
}
/** Promote a user to team lead */
@PostMapping("/promote-to-team-lead")
@Transactional
public ResponseEntity<?> promoteToTeamLead(@RequestParam("userId") Long userId) {
if (!authorizationService.canManageUser(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to manage this user");
}
if (!authorizationService.canAssignRole(Role.TEAM_LEAD)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to assign team lead role");
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// User must be in a team to become a team lead
if (user.getTeam() == null) {
return ResponseEntity.badRequest()
.body("User must be assigned to a team before becoming a team lead");
}
user.setUserRole(Role.TEAM_LEAD);
userRepository.save(user);
return ResponseEntity.ok().body("User promoted to team lead successfully");
}
/** Demote a team lead to regular user */
@PostMapping("/demote-from-team-lead")
@Transactional
public ResponseEntity<?> demoteFromTeamLead(@RequestParam("userId") Long userId) {
if (!authorizationService.canManageUser(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to manage this user");
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
user.setUserRole(Role.USER);
userRepository.save(user);
return ResponseEntity.ok().body("User demoted from team lead successfully");
}
/** Create a new team in the organization */
@PostMapping("/create-team")
@Transactional
public ResponseEntity<?> createTeam(@RequestParam("teamName") String teamName) {
if (!authorizationService.canManageOrgTeams()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to create teams");
}
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getOrganization() == null) {
return ResponseEntity.badRequest()
.body("Org admin must be assigned to an organization");
}
Organization organization = currentUser.getOrganization();
// Check if team name already exists in the organization
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(
teamName, organization.getId())) {
return ResponseEntity.badRequest()
.body("Team with name '" + teamName + "' already exists in this organization");
}
Team newTeam = new Team();
newTeam.setName(teamName);
newTeam.setOrganization(organization);
Team savedTeam = teamRepository.save(newTeam);
return ResponseEntity.ok(savedTeam);
}
/** Remove a user from the organization (removes from their team) */
@PostMapping("/remove-user")
@Transactional
public ResponseEntity<?> removeUserFromOrganization(@RequestParam("userId") Long userId) {
if (!authorizationService.canRemoveUserFromTeam(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to remove this user");
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
user.setTeam(null);
user.setUserRole(Role.USER); // Reset to basic user role
userRepository.save(user);
return ResponseEntity.ok().body("User removed from organization successfully");
}
}
@@ -1,99 +0,0 @@
package stirling.software.proprietary.security.controller.api;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO;
import stirling.software.proprietary.security.repository.OrganizationRepository;
import stirling.software.proprietary.security.service.OrganizationService;
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
@RestController
@RequestMapping("/api/v1/organizations")
@RequiredArgsConstructor
@PreAuthorize("@roleBasedAuthorizationService.canManageOrganizations()")
public class OrganizationController {
private final OrganizationRepository organizationRepository;
private final OrganizationService organizationService;
private final RoleBasedAuthorizationService authorizationService;
@GetMapping
public ResponseEntity<List<OrganizationWithTeamCountDTO>> getAllOrganizations() {
List<OrganizationWithTeamCountDTO> organizations =
organizationRepository.findAllOrganizationsWithTeamCount();
return ResponseEntity.ok(organizations);
}
@GetMapping("/{id}")
@PreAuthorize(
"@roleBasedAuthorizationService.canViewOrganization(@organizationRepository.findById(#id).orElse(null))")
public ResponseEntity<Organization> getOrganization(@PathVariable Long id) {
Optional<Organization> organizationOpt = organizationRepository.findById(id);
if (organizationOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
Organization organization = organizationOpt.get();
return ResponseEntity.ok(organization);
}
@PostMapping
public ResponseEntity<?> createOrganization(@RequestBody Organization organization) {
if (organizationRepository.existsByNameIgnoreCase(organization.getName())) {
return ResponseEntity.badRequest()
.body("Organization with name '" + organization.getName() + "' already exists");
}
Organization savedOrganization = organizationRepository.save(organization);
return ResponseEntity.status(HttpStatus.CREATED).body(savedOrganization);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateOrganization(
@PathVariable Long id, @RequestBody Organization organization) {
Optional<Organization> existingOrganization = organizationRepository.findById(id);
if (existingOrganization.isEmpty()) {
return ResponseEntity.notFound().build();
}
if (organizationRepository.existsByNameIgnoreCase(organization.getName())
&& !existingOrganization.get().getName().equalsIgnoreCase(organization.getName())) {
return ResponseEntity.badRequest()
.body("Organization with name '" + organization.getName() + "' already exists");
}
organization.setId(id);
Organization savedOrganization = organizationRepository.save(organization);
return ResponseEntity.ok(savedOrganization);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteOrganization(@PathVariable Long id) {
Optional<Organization> organization = organizationRepository.findById(id);
if (organization.isEmpty()) {
return ResponseEntity.notFound().build();
}
// Prevent deletion of default organizations
if (OrganizationService.DEFAULT_ORG_NAME.equals(organization.get().getName())
|| OrganizationService.INTERNAL_ORG_NAME.equals(organization.get().getName())) {
return ResponseEntity.badRequest().body("Cannot delete system organizations");
}
if (!organization.get().getTeams().isEmpty()) {
return ResponseEntity.badRequest()
.body("Cannot delete organization with existing teams");
}
organizationRepository.deleteById(id);
return ResponseEntity.ok().build();
}
}
@@ -14,16 +14,11 @@ import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.config.PremiumEndpoint;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.OrganizationRepository;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.OrganizationService;
import stirling.software.proprietary.security.service.OrganizationValidationService;
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
import stirling.software.proprietary.security.service.TeamService;
@Controller
@@ -32,50 +27,35 @@ import stirling.software.proprietary.security.service.TeamService;
@Slf4j
@RequiredArgsConstructor
@PremiumEndpoint
@PreAuthorize("@roleBasedAuthorizationService.canManageOrgTeams()")
public class TeamController {
private final TeamRepository teamRepository;
private final UserRepository userRepository;
private final OrganizationRepository organizationRepository;
private final OrganizationService organizationService;
private final OrganizationValidationService organizationValidationService;
private final RoleBasedAuthorizationService authorizationService;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/create")
public RedirectView createTeam(
@RequestParam("name") String name,
@RequestParam("organizationId") Long organizationId) {
Organization organization = organizationService.getOrCreateDefaultOrganization();
if (organizationId != null) {
organization = organizationRepository.findById(organizationId).orElse(organization);
}
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(name, organization.getId())) {
public RedirectView createTeam(@RequestParam("name") String name) {
if (teamRepository.existsByNameIgnoreCase(name)) {
return new RedirectView("/teams?messageType=teamExists");
}
Team team = new Team();
team.setName(name);
team.setOrganization(organization);
teamRepository.save(team);
return new RedirectView("/teams?messageType=teamCreated");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/rename")
@PreAuthorize(
"@roleBasedAuthorizationService.canManageTeam(@teamRepository.findById(#teamId).orElse(null))")
public RedirectView renameTeam(
@RequestParam("teamId") Long teamId, @RequestParam("newName") String newName) {
Optional<Team> existing = teamRepository.findById(teamId);
if (existing.isEmpty()) {
return new RedirectView("/teams?messageType=teamNotFound");
}
Team team = existing.get();
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(
newName, team.getOrganization().getId())) {
if (teamRepository.existsByNameIgnoreCase(newName)) {
return new RedirectView("/teams?messageType=teamNameExists");
}
Team team = existing.get();
// Prevent renaming the Internal team
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
@@ -87,10 +67,9 @@ public class TeamController {
return new RedirectView("/teams?messageType=teamRenamed");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/delete")
@Transactional
@PreAuthorize(
"@roleBasedAuthorizationService.canManageTeam(@teamRepository.findById(#teamId).orElse(null))")
public RedirectView deleteTeam(@RequestParam("teamId") Long teamId) {
Optional<Team> teamOpt = teamRepository.findById(teamId);
if (teamOpt.isEmpty()) {
@@ -113,10 +92,9 @@ public class TeamController {
return new RedirectView("/teams?messageType=teamDeleted");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/addUser")
@Transactional
@PreAuthorize(
"@roleBasedAuthorizationService.canAddUserToTeam(#userId, @teamRepository.findById(#teamId).orElse(null))")
public RedirectView addUserToTeam(
@RequestParam("teamId") Long teamId, @RequestParam("userId") Long userId) {
@@ -143,13 +121,6 @@ public class TeamController {
return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers");
}
// Ensure user and team are in the same organization (or user has no org yet)
if (user.getOrganization() != null
&& !organizationValidationService.isTeamInOrganization(
team, user.getOrganization())) {
return new RedirectView("/teams/" + teamId + "?error=userNotInSameOrganization");
}
// Assign user to team
user.setTeam(team);
userRepository.save(user);
@@ -1,169 +0,0 @@
package stirling.software.proprietary.security.controller.api;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.config.PremiumEndpoint;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
@RestController
@RequestMapping("/api/v1/team-lead")
@Tag(name = "Team Lead", description = "Team Lead Management APIs")
@Slf4j
@RequiredArgsConstructor
@PremiumEndpoint
@PreAuthorize("@roleBasedAuthorizationService.canManageTeamUsers()")
public class TeamLeadController {
private final TeamRepository teamRepository;
private final UserRepository userRepository;
private final RoleBasedAuthorizationService authorizationService;
/** Get team members that the current team lead can manage */
@GetMapping("/my-team-members")
public ResponseEntity<List<User>> getMyTeamMembers() {
if (!authorizationService.canManageTeamUsers()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getTeam() == null) {
return ResponseEntity.badRequest().build();
}
List<User> teamMembers = userRepository.findByTeam(currentUser.getTeam());
return ResponseEntity.ok(teamMembers);
}
/** Add a user to the team lead's team */
@PostMapping("/add-member")
@Transactional
public ResponseEntity<?> addMemberToMyTeam(@RequestParam("userId") Long userId) {
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getTeam() == null) {
return ResponseEntity.badRequest().body("Team lead must be assigned to a team");
}
if (!authorizationService.canAddUserToTeam(userId, currentUser.getTeam())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to add users to this team");
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
// Check if user is already in a team
if (user.getTeam() != null) {
return ResponseEntity.badRequest()
.body("User is already assigned to team: " + user.getTeam().getName());
}
// Assign user to team
user.setTeam(currentUser.getTeam());
userRepository.save(user);
return ResponseEntity.ok().body("User added to team successfully");
}
/** Remove a user from the team lead's team */
@PostMapping("/remove-member")
@Transactional
public ResponseEntity<?> removeMemberFromMyTeam(@RequestParam("userId") Long userId) {
if (!authorizationService.canRemoveUserFromTeam(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to remove this user");
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
User currentUser = authorizationService.getCurrentUser();
// Prevent team leads from removing themselves
if (currentUser != null && currentUser.getId().equals(userId)) {
return ResponseEntity.badRequest()
.body("Team leads cannot remove themselves from the team");
}
// Remove user from team
user.setTeam(null);
userRepository.save(user);
return ResponseEntity.ok().body("User removed from team successfully");
}
/** Update a team member's role (team leads can only assign USER role) */
@PostMapping("/update-member-role")
@Transactional
public ResponseEntity<?> updateMemberRole(
@RequestParam("userId") Long userId, @RequestParam("role") String roleString) {
if (!authorizationService.canManageUser(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Not authorized to manage this user");
}
try {
Role newRole = Role.fromString(roleString);
// Team leads can only assign USER role
if (!authorizationService.canAssignRole(newRole)) {
return ResponseEntity.badRequest()
.body("Not authorized to assign role: " + newRole.getRoleName());
}
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = userOpt.get();
user.setUserRole(newRole);
userRepository.save(user);
return ResponseEntity.ok().body("User role updated successfully");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid role: " + roleString);
}
}
/** Get team information for the current team lead */
@GetMapping("/my-team")
public ResponseEntity<Team> getMyTeam() {
User currentUser = authorizationService.getCurrentUser();
if (currentUser == null || currentUser.getTeam() == null) {
return ResponseEntity.badRequest().build();
}
if (!authorizationService.canManageTeam(currentUser.getTeam())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return ResponseEntity.ok(currentUser.getTeam());
}
}
@@ -207,7 +207,7 @@ public class UserController {
return "redirect:/account";
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser")
public RedirectView saveUser(
@RequestParam(name = "username", required = true) String username,
@@ -279,7 +279,7 @@ public class UserController {
true);
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeRole")
@Transactional
public RedirectView changeRole(
@@ -342,7 +342,7 @@ public class UserController {
true);
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeUserEnabled/{username}")
public RedirectView changeUserEnabled(
@PathVariable("username") String username,
@@ -392,7 +392,7 @@ public class UserController {
true);
}
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}")
public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) {
@@ -24,7 +24,7 @@ public class DatabaseWebController {
private final DatabaseService databaseService;
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/database")
public String database(HttpServletRequest request, Model model, Authentication authentication) {
String error = request.getParameter("error");
@@ -36,7 +36,7 @@ public class TeamWebController {
private final UserRepository userRepository;
@GetMapping
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String listTeams(HttpServletRequest request, Model model) {
// Get teams with user counts using a DTO projection
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
@@ -87,7 +87,7 @@ public class TeamWebController {
}
@GetMapping("/{id}")
@PreAuthorize("@roleBasedAuthorizationService.canManageAllUsers()")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String viewTeamDetails(
HttpServletRequest request, @PathVariable("id") Long id, Model model) {
// Get the team
@@ -24,6 +24,9 @@ public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
@Query("SELECT u FROM User u WHERE u.team IS NULL")
List<User> findAllWithoutTeam();
@Query(value = "SELECT u FROM User u LEFT JOIN FETCH u.team")
List<User> findAllWithTeam();
@@ -33,11 +36,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
long countByTeam(Team team);
List<User> findByTeam(Team team);
@Query("SELECT u FROM User u WHERE u.team IS NULL")
List<User> findUsersWithoutTeam();
@Query("SELECT u FROM User u JOIN u.authorities a WHERE a.authority = :role")
List<User> findByRole(@Param("role") String role);
List<User> findAllByTeam(Team team);
}
@@ -55,11 +55,7 @@ public class User implements Serializable {
@Column(name = "authenticationtype")
private String authenticationType;
@OneToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.ALL,
mappedBy = "user",
orphanRemoval = true)
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@ManyToOne(fetch = FetchType.EAGER)
@@ -106,88 +102,4 @@ public class User implements Serializable {
public boolean hasPassword() {
return this.password != null && !this.password.isEmpty();
}
public stirling.software.proprietary.model.Organization getOrganization() {
return this.team != null ? this.team.getOrganization() : null;
}
// Role-based permission methods
public Role getUserRole() {
String roleString = getRolesAsString();
if (roleString == null || roleString.isEmpty()) return Role.USER;
try {
return Role.fromString(roleString);
} catch (IllegalArgumentException e) {
return Role.USER; // Default fallback
}
}
public boolean isSystemAdmin() {
Role role = getUserRole();
return role.isSystemAdmin();
}
public boolean isOrgAdmin() {
Role role = getUserRole();
return role.isOrgAdmin();
}
public boolean isTeamLead() {
Role role = getUserRole();
return role.isTeamLead();
}
public boolean canManageUser(User otherUser) {
// System admins can manage anyone
if (isSystemAdmin()) return true;
// Org admins can manage users in their organization
if (isOrgAdmin()) {
stirling.software.proprietary.model.Organization thisOrg = getOrganization();
stirling.software.proprietary.model.Organization otherOrg = otherUser.getOrganization();
return thisOrg != null && otherOrg != null && thisOrg.getId().equals(otherOrg.getId());
}
// Team leads can manage users in their team
if (isTeamLead()) {
return this.team != null
&& otherUser.team != null
&& this.team.getId().equals(otherUser.team.getId());
}
return false;
}
public boolean canManageTeam(stirling.software.proprietary.model.Team targetTeam) {
if (targetTeam == null) return false;
// System admins can manage any team
if (isSystemAdmin()) return true;
// Org admins can manage teams in their organization
if (isOrgAdmin()) {
stirling.software.proprietary.model.Organization thisOrg = getOrganization();
stirling.software.proprietary.model.Organization teamOrg = targetTeam.getOrganization();
return thisOrg != null && teamOrg != null && thisOrg.getId().equals(teamOrg.getId());
}
// Team leads can only manage their own team
if (isTeamLead()) {
return this.team != null && this.team.getId().equals(targetTeam.getId());
}
return false;
}
public void setUserRole(Role role) {
// Clear existing authorities
this.authorities.clear();
// Add new authority
Authority authority = new Authority();
authority.setAuthority(role.getRoleId());
authority.setUser(this);
this.authorities.add(authority);
}
}
@@ -1,23 +0,0 @@
package stirling.software.proprietary.security.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO;
@Repository
public interface OrganizationRepository extends JpaRepository<Organization, Long> {
Optional<Organization> findByName(String name);
@Query(
"SELECT new stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO(o.id, o.name, o.description, COUNT(t)) "
+ "FROM Organization o LEFT JOIN o.teams t GROUP BY o.id, o.name, o.description")
List<OrganizationWithTeamCountDTO> findAllOrganizationsWithTeamCount();
boolean existsByNameIgnoreCase(String name);
}
@@ -14,24 +14,10 @@ import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
public interface TeamRepository extends JpaRepository<Team, Long> {
Optional<Team> findByName(String name);
Optional<Team> findByNameAndOrganizationId(String name, Long organizationId);
List<Team> findByOrganizationId(Long organizationId);
@Query(
"SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) "
+ "FROM Team t LEFT JOIN t.users u WHERE t.organization.id = :organizationId GROUP BY t.id, t.name")
List<TeamWithUserCountDTO> findAllTeamsWithUserCountByOrganizationId(Long organizationId);
@Query(
"SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) "
+ "FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
boolean existsByNameIgnoreCase(String name);
boolean existsByNameIgnoreCaseAndOrganizationId(String name, Long organizationId);
@Query("SELECT t FROM Team t WHERE t.organization IS NULL")
List<Team> findTeamsWithoutOrganization();
}
@@ -37,7 +37,7 @@ class AppUpdateAuthService implements ShowAdminInterface {
}
Optional<User> user = userRepository.findByUsername(authentication.getName());
if (user.isPresent() && showUpdateOnlyAdmin) {
return user.get().isSystemAdmin();
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
}
return showUpdate;
}
@@ -1,43 +0,0 @@
package stirling.software.proprietary.security.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.security.repository.OrganizationRepository;
@Service
@RequiredArgsConstructor
public class OrganizationService {
private final OrganizationRepository organizationRepository;
public static final String DEFAULT_ORG_NAME = "Default Organization";
public static final String INTERNAL_ORG_NAME = "Internal Organization";
public Organization getOrCreateDefaultOrganization() {
return organizationRepository
.findByName(DEFAULT_ORG_NAME)
.orElseGet(
() -> {
Organization defaultOrg = new Organization();
defaultOrg.setName(DEFAULT_ORG_NAME);
defaultOrg.setDescription("Default organization for initial setup");
return organizationRepository.save(defaultOrg);
});
}
public Organization getOrCreateInternalOrganization() {
return organizationRepository
.findByName(INTERNAL_ORG_NAME)
.orElseGet(
() -> {
Organization internalOrg = new Organization();
internalOrg.setName(INTERNAL_ORG_NAME);
internalOrg.setDescription(
"Internal organization for system operations");
return organizationRepository.save(internalOrg);
});
}
}
@@ -1,79 +0,0 @@
package stirling.software.proprietary.security.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
@Service
@RequiredArgsConstructor
public class OrganizationValidationService {
private final TeamRepository teamRepository;
/**
* Validates that a user has access to a specific team. Users can only access teams within their
* own organization.
*/
public boolean canUserAccessTeam(User user, Team team) {
if (user == null || team == null) {
return false;
}
Organization userOrg = user.getOrganization();
Organization teamOrg = team.getOrganization();
if (userOrg == null || teamOrg == null) {
return false;
}
return userOrg.getId().equals(teamOrg.getId());
}
/** Validates that a user has access to a specific team by ID. */
public boolean canUserAccessTeam(User user, Long teamId) {
if (user == null || teamId == null) {
return false;
}
Organization userOrg = user.getOrganization();
if (userOrg == null) {
return false;
}
return teamRepository
.findById(teamId)
.map(team -> userOrg.getId().equals(team.getOrganization().getId()))
.orElse(false);
}
/** Validates that two users belong to the same organization. */
public boolean areUsersInSameOrganization(User user1, User user2) {
if (user1 == null || user2 == null) {
return false;
}
Organization org1 = user1.getOrganization();
Organization org2 = user2.getOrganization();
if (org1 == null || org2 == null) {
return false;
}
return org1.getId().equals(org2.getId());
}
/** Validates that a team belongs to a specific organization. */
public boolean isTeamInOrganization(Team team, Organization organization) {
if (team == null || organization == null) {
return false;
}
Organization teamOrg = team.getOrganization();
return teamOrg != null && teamOrg.getId().equals(organization.getId());
}
}
@@ -1,128 +0,0 @@
package stirling.software.proprietary.security.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
@Service
@RequiredArgsConstructor
public class RoleBasedAuthorizationService {
private final UserRepository userRepository;
/** Gets the current authenticated user */
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
String username = authentication.getName();
return userRepository.findByUsername(username).orElse(null);
}
/** Checks if current user can manage users across all organizations (System Admin) */
public boolean canManageAllUsers() {
User currentUser = getCurrentUser();
return currentUser != null && currentUser.isSystemAdmin();
}
/** Checks if current user can manage users within their organization (Org Admin or above) */
public boolean canManageOrgUsers() {
User currentUser = getCurrentUser();
return currentUser != null && currentUser.isOrgAdmin();
}
/** Checks if current user can manage team members (Team Lead or above) */
public boolean canManageTeamUsers() {
User currentUser = getCurrentUser();
return currentUser != null && currentUser.isTeamLead();
}
/** Checks if current user can manage a specific user */
public boolean canManageUser(Long userId) {
User currentUser = getCurrentUser();
if (currentUser == null) return false;
User targetUser = userRepository.findById(userId).orElse(null);
if (targetUser == null) return false;
return currentUser.canManageUser(targetUser);
}
/** Checks if current user can manage a specific team */
public boolean canManageTeam(Team team) {
User currentUser = getCurrentUser();
if (currentUser == null || team == null) return false;
return currentUser.canManageTeam(team);
}
/** Checks if current user can manage teams within their organization */
public boolean canManageOrgTeams() {
User currentUser = getCurrentUser();
return currentUser != null && currentUser.isOrgAdmin();
}
/** Checks if current user can create/manage organizations (System Admin only) */
public boolean canManageOrganizations() {
return canManageAllUsers();
}
/** Checks if current user can assign roles */
public boolean canAssignRole(Role targetRole) {
User currentUser = getCurrentUser();
if (currentUser == null) return false;
// Users can only assign roles that are lower than or equal to their own
return currentUser.getUserRole().hasAuthorityOver(targetRole);
}
/** Checks if current user can remove a user from their team/organization */
public boolean canRemoveUserFromTeam(Long userId) {
return canManageUser(userId);
}
/** Checks if current user can add a user to a specific team */
public boolean canAddUserToTeam(Long userId, Team team) {
User currentUser = getCurrentUser();
if (currentUser == null || team == null) return false;
// Must be able to manage both the user and the target team
return canManageUser(userId) && currentUser.canManageTeam(team);
}
/** Gets the highest role the current user can assign to others */
public Role getMaxAssignableRole() {
User currentUser = getCurrentUser();
if (currentUser == null) return Role.USER;
return switch (currentUser.getUserRole()) {
case SYSTEM_ADMIN, ADMIN -> Role.ORG_ADMIN; // System admins can create org admins
case ORG_ADMIN -> Role.TEAM_LEAD; // Org admins can create team leads
case TEAM_LEAD -> Role.USER; // Team leads can only create regular users
default -> Role.USER;
};
}
/** Checks if current user can view organization details */
public boolean canViewOrganization(Organization organization) {
User currentUser = getCurrentUser();
if (currentUser == null || organization == null) return false;
// System admins can view any org
if (currentUser.isSystemAdmin()) return true;
// Org admins and team leads can view their own organization
Organization userOrg = currentUser.getOrganization();
return userOrg != null && userOrg.getId().equals(organization.getId());
}
}
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.repository.TeamRepository;
@@ -13,46 +12,29 @@ import stirling.software.proprietary.security.repository.TeamRepository;
public class TeamService {
private final TeamRepository teamRepository;
private final OrganizationService organizationService;
public static final String DEFAULT_TEAM_NAME = "Default";
public static final String INTERNAL_TEAM_NAME = "Internal";
public Team getOrCreateDefaultTeam() {
Organization defaultOrg = organizationService.getOrCreateDefaultOrganization();
return teamRepository
.findByNameAndOrganizationId(DEFAULT_TEAM_NAME, defaultOrg.getId())
.findByName(DEFAULT_TEAM_NAME)
.orElseGet(
() -> {
Team defaultTeam = new Team();
defaultTeam.setName(DEFAULT_TEAM_NAME);
defaultTeam.setOrganization(defaultOrg);
return teamRepository.save(defaultTeam);
});
}
public Team getOrCreateInternalTeam() {
Organization internalOrg = organizationService.getOrCreateInternalOrganization();
return teamRepository
.findByNameAndOrganizationId(INTERNAL_TEAM_NAME, internalOrg.getId())
.findByName(INTERNAL_TEAM_NAME)
.orElseGet(
() -> {
Team internalTeam = new Team();
internalTeam.setName(INTERNAL_TEAM_NAME);
internalTeam.setOrganization(internalOrg);
return teamRepository.save(internalTeam);
});
}
public Team getOrCreateTeamForOrganization(String teamName, Organization organization) {
return teamRepository
.findByNameAndOrganizationId(teamName, organization.getId())
.orElseGet(
() -> {
Team team = new Team();
team.setName(teamName);
team.setOrganization(organization);
return teamRepository.save(team);
});
}
}
@@ -328,10 +328,6 @@ public class UserService implements UserServiceInterface {
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
}
public List<User> findByRole(String role) {
return userRepository.findByRole(role);
}
public Authority findRole(User user) {
return authorityRepository.findByUserId(user.getId());
}
@@ -624,14 +620,10 @@ public class UserService implements UserServiceInterface {
}
public List<User> getUsersWithoutTeam() {
return userRepository.findUsersWithoutTeam();
return userRepository.findAllWithoutTeam();
}
public void saveAll(List<User> users) {
userRepository.saveAll(users);
}
public User saveUser(User user) {
return userRepository.save(user);
}
}
@@ -12,7 +12,6 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.proprietary.model.Organization;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.repository.TeamRepository;
@@ -21,23 +20,14 @@ class TeamServiceTest {
@Mock private TeamRepository teamRepository;
@Mock private OrganizationService organizationService;
@InjectMocks private TeamService teamService;
@Test
void getDefaultTeam() {
var organization = new Organization();
organization.setId(1L);
organization.setName("Default Organization");
var team = new Team();
team.setName("Marleyans");
team.setOrganization(organization);
when(organizationService.getOrCreateDefaultOrganization()).thenReturn(organization);
when(teamRepository.findByNameAndOrganizationId(
TeamService.DEFAULT_TEAM_NAME, organization.getId()))
when(teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME))
.thenReturn(Optional.of(team));
Team result = teamService.getOrCreateDefaultTeam();
@@ -47,19 +37,12 @@ class TeamServiceTest {
@Test
void createDefaultTeam_whenRepositoryIsEmpty() {
var organization = new Organization();
organization.setId(1L);
organization.setName("Default Organization");
String teamName = "Default";
var defaultTeam = new Team();
defaultTeam.setId(1L);
defaultTeam.setName(teamName);
defaultTeam.setOrganization(organization);
when(organizationService.getOrCreateDefaultOrganization()).thenReturn(organization);
when(teamRepository.findByNameAndOrganizationId(teamName, organization.getId()))
.thenReturn(Optional.empty());
when(teamRepository.findByName(teamName)).thenReturn(Optional.empty());
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
Team result = teamService.getOrCreateDefaultTeam();
@@ -69,17 +52,10 @@ class TeamServiceTest {
@Test
void getInternalTeam() {
var organization = new Organization();
organization.setId(2L);
organization.setName("Internal Organization");
var team = new Team();
team.setName("Eldians");
team.setOrganization(organization);
when(organizationService.getOrCreateInternalOrganization()).thenReturn(organization);
when(teamRepository.findByNameAndOrganizationId(
TeamService.INTERNAL_TEAM_NAME, organization.getId()))
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
.thenReturn(Optional.of(team));
Team result = teamService.getOrCreateInternalTeam();
@@ -89,21 +65,15 @@ class TeamServiceTest {
@Test
void createInternalTeam_whenRepositoryIsEmpty() {
var organization = new Organization();
organization.setId(2L);
organization.setName("Internal Organization");
String teamName = "Internal";
Team internalTeam = new Team();
internalTeam.setId(2L);
internalTeam.setName(teamName);
internalTeam.setOrganization(organization);
when(organizationService.getOrCreateInternalOrganization()).thenReturn(organization);
when(teamRepository.findByNameAndOrganizationId(
TeamService.INTERNAL_TEAM_NAME, organization.getId()))
.thenReturn(Optional.empty());
when(teamRepository.findByName(teamName)).thenReturn(Optional.empty());
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
.thenReturn(Optional.empty());
Team result = teamService.getOrCreateInternalTeam();
@@ -286,7 +286,7 @@ class UserServiceTest {
String username = "testuser";
String password = "password123";
Long teamId = 1L;
String customRole = Role.USER.getRoleId();
String customRole = Role.LIMITED_API_USER.getRoleId();
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
+1 -18
View File
@@ -69,7 +69,7 @@ allprojects {
tasks.register('writeVersion', WriteProperties) {
outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
println "Writing version.properties to ${outputFile.path}"
comment = "${new Date()}"
comment "${new Date()}"
property 'version', project.provider { project.version.toString() }
}
@@ -128,9 +128,6 @@ subprojects {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.withType(JavaCompile).configureEach {
@@ -156,17 +153,6 @@ subprojects {
}
}
jacocoTestCoverageVerification {
dependsOn jacocoTestReport
violationRules {
rule {
limit {
minimum = 0.0
}
}
}
}
tasks.named("processResources") {
dependsOn(rootProject.tasks.writeVersion)
}
@@ -583,9 +569,6 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.named("test") {