diff --git a/.gitignore b/.gitignore index 661ca292..86a25aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -116,7 +116,7 @@ watchedFolders/ *.zip *.tar.gz *.rar - +*.db /build /.vscode \ No newline at end of file diff --git a/README.md b/README.md index 9b7365f3..a0a0dd50 100644 --- a/README.md +++ b/README.md @@ -185,21 +185,30 @@ For those wanting to use Stirling-PDFs backend API to link with their own custom [here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) +## Login authentication (CURRENTLY ALPHA TAG ONLY) +### Prerequisites: +- User must have the folder ./configs volumed within docker so that it is retained during updates. +- The environment variable 'login.enabled' must be set to true +- The environment variables "INITIAL_USERNAME" and "INITIAL_PASSWORD" must also be populated (only required on first boot to create initial user, ignored after.) + +Once the above has been done, on restart a new stirling-pdf-DB.mv.db will show if everything worked. + +When you login to Stirling PDF you will be redirected to /login page to login with those credentials. After login everything should function as normal + +To access your account settings go to Account settings in the settings cog menu (top right in navbar) this Account settings menu is also where you find your API key. + +To add new users go to bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future + +For API usage you must provide a header with 'X-API-Key' and the associated API key for that user. + + ## FAQ -### Q1: Can you add authentication in Stirling PDF? -There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead we recommended you use trusted and secure authentication software like Authentik or Authelia. - -### Q2: What are your planned features? -- Crop +### Q1: What are your planned features? - Progress bar/Tracking - Full custom logic pipelines to combine multiple operations together. - Folder support with auto scanning to perform operations on - Redact sections of pages -- Add page numbers -- Auto rename (Renames file based on file title text) -- URL to PDF -- Change contrast -### Q3: Why is my application downloading .htm files? +### Q2: Why is my application downloading .htm files? This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files. diff --git a/build.gradle b/build.gradle index 5e95460f..bff497c0 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'stirling.software' -version = '0.12.3' +version = '0.13.0' sourceCompatibility = '17' repositories { @@ -48,7 +48,12 @@ dependencies { implementation 'org.yaml:snakeyaml:2.1' implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2' + implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.2' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + implementation "com.h2database:h2" + // https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4' implementation 'commons-io:commons-io:2.13.0' @@ -65,6 +70,8 @@ dependencies { implementation group: 'com.google.zxing', name: 'core', version: '3.5.1' // https://mvnrepository.com/artifact/org.commonmark/commonmark implementation 'org.commonmark:commonmark:0.21.0' + // https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core + implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0' developmentOnly("org.springframework.boot:spring-boot-devtools") diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index f9512295..5955c339 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -10,11 +10,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import jakarta.annotation.PostConstruct; import stirling.software.SPDF.utils.GeneralUtils; - +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @SpringBootApplication +@EnableWebSecurity() +@EnableGlobalMethodSecurity(prePostEnabled = true) //@EnableScheduling public class SPdfApplication { diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 430bcae8..29290a32 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -1,42 +1,63 @@ -package stirling.software.SPDF.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AppConfig { - @Bean(name = "appName") - public String appName() { - String appName = System.getProperty("APP_HOME_NAME"); - if (appName == null) - appName = System.getenv("APP_HOME_NAME"); - return (appName != null) ? appName : "Stirling PDF"; - } - - @Bean(name = "appVersion") - public String appVersion() { - String version = getClass().getPackage().getImplementationVersion(); - return (version != null) ? version : "0.0.0"; - } - - @Bean(name = "homeText") - public String homeText() { - String homeText = System.getProperty("APP_HOME_DESCRIPTION"); - if (homeText == null) - homeText = System.getenv("APP_HOME_DESCRIPTION"); - return (homeText != null) ? homeText : "null"; - } - - @Bean(name = "navBarText") - public String navBarText() { - String navBarText = System.getProperty("APP_NAVBAR_NAME"); - if (navBarText == null) - navBarText = System.getenv("APP_NAVBAR_NAME"); - if (navBarText == null) - navBarText = System.getProperty("APP_HOME_NAME"); - if (navBarText == null) - navBarText = System.getenv("APP_HOME_NAME"); - - return (navBarText != null) ? navBarText : "Stirling PDF"; - } +package stirling.software.SPDF.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + + + @Bean(name = "rateLimit") + public boolean rateLimit() { + String appName = System.getProperty("rateLimit"); + if (appName == null) + appName = System.getenv("rateLimit"); + System.out.println("rateLimit=" + appName); + return (appName != null) ? Boolean.valueOf(appName) : false; + } + + @Bean(name = "loginEnabled") + public boolean loginEnabled() { + String appName = System.getProperty("login.enabled"); + if (appName == null) + appName = System.getenv("login.enabled"); + System.out.println("loginEnabled=" + appName); + return (appName != null) ? Boolean.valueOf(appName) : false; + } + + @Bean(name = "appName") + public String appName() { + String appName = System.getProperty("APP_HOME_NAME"); + if (appName == null) + appName = System.getenv("APP_HOME_NAME"); + return (appName != null) ? appName : "Stirling PDF"; + } + + @Bean(name = "appVersion") + public String appVersion() { + String version = getClass().getPackage().getImplementationVersion(); + return (version != null) ? version : "0.0.0"; + } + + @Bean(name = "homeText") + public String homeText() { + String homeText = System.getProperty("APP_HOME_DESCRIPTION"); + if (homeText == null) + homeText = System.getenv("APP_HOME_DESCRIPTION"); + return (homeText != null) ? homeText : "null"; + } + + @Bean(name = "navBarText") + public String navBarText() { + String navBarText = System.getProperty("APP_NAVBAR_NAME"); + if (navBarText == null) + navBarText = System.getenv("APP_NAVBAR_NAME"); + if (navBarText == null) + navBarText = System.getProperty("APP_HOME_NAME"); + if (navBarText == null) + navBarText = System.getenv("APP_HOME_NAME"); + + return (navBarText != null) ? navBarText : "Stirling PDF"; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/Beans.java b/src/main/java/stirling/software/SPDF/config/Beans.java index f8ff302e..c22c42fe 100644 --- a/src/main/java/stirling/software/SPDF/config/Beans.java +++ b/src/main/java/stirling/software/SPDF/config/Beans.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.config; +import java.time.Duration; import java.util.Locale; import org.springframework.context.annotation.Bean; @@ -10,9 +11,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; + @Configuration public class Beans implements WebMvcConfigurer { - + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor()); diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index efa084b3..430ef5ea 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -1,79 +1,83 @@ -package stirling.software.SPDF.config; - -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.Arrays; -import java.util.List; -import java.util.HashMap; -import java.util.Map; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -public class CleanUrlInterceptor implements HandlerInterceptor { - - private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints"); - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - - Map parameters = new HashMap<>(); - - // Keep only the allowed parameters - String[] queryParameters = queryString.split("&"); - for (String param : queryParameters) { - String[] keyValue = param.split("="); - if (keyValue.length != 2) { - continue; - } - if (ALLOWED_PARAMS.contains(keyValue[0])) { - parameters.put(keyValue[0], keyValue[1]); - } - } - - // If there are any parameters that are not allowed - if (parameters.size() != queryParameters.length) { - // Construct new query string - StringBuilder newQueryString = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { - if (newQueryString.length() > 0) { - newQueryString.append("&"); - } - newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); - } - - // Redirect to the URL with only allowed query parameters - String redirectUrl = requestURI + "?" + newQueryString; - response.sendRedirect(redirectUrl); - return false; - } - } - return true; - } - - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) { - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { - } -} +package stirling.software.SPDF.config; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.Arrays; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +public class CleanUrlInterceptor implements HandlerInterceptor { + + private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error"); + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String queryString = request.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + String requestURI = request.getRequestURI(); + + Map parameters = new HashMap<>(); + + // Keep only the allowed parameters + String[] queryParameters = queryString.split("&"); + for (String param : queryParameters) { + String[] keyValue = param.split("="); + System.out.print("astirli " + keyValue[0]); + if (keyValue.length != 2) { + continue; + } + System.out.print("astirli2 " + keyValue[0]); + + if (ALLOWED_PARAMS.contains(keyValue[0])) { + parameters.put(keyValue[0], keyValue[1]); + } + } + + // If there are any parameters that are not allowed + if (parameters.size() != queryParameters.length) { + // Construct new query string + StringBuilder newQueryString = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (newQueryString.length() > 0) { + newQueryString.append("&"); + } + newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); + } + + // Redirect to the URL with only allowed query parameters + String redirectUrl = requestURI + "?" + newQueryString; + response.sendRedirect(redirectUrl); + return false; + } + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) { + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + Exception ex) { + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 5befd511..77191a41 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -1,26 +1,26 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class EndpointInterceptor implements HandlerInterceptor { - - @Autowired - private EndpointConfiguration endpointConfiguration; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String requestURI = request.getRequestURI(); - if (!endpointConfiguration.isEndpointEnabled(requestURI)) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); - return false; - } - return true; - } +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class EndpointInterceptor implements HandlerInterceptor { + + @Autowired + private EndpointConfiguration endpointConfiguration; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String requestURI = request.getRequestURI(); + if (!endpointConfiguration.isEndpointEnabled(requestURI)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); + return false; + } + return true; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java index 25d6d8d6..1cdc99e3 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java @@ -1,24 +1,24 @@ -package stirling.software.SPDF.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.config.MeterFilterReply; - -@Configuration -public class MetricsConfig { - - @Bean - public MeterFilter meterFilter() { - return new MeterFilter() { - @Override - public MeterFilterReply accept(Meter.Id id) { - if (id.getName().equals("http.requests")) { - return MeterFilterReply.NEUTRAL; - } - return MeterFilterReply.DENY; - } - }; - } +package stirling.software.SPDF.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; + +@Configuration +public class MetricsConfig { + + @Bean + public MeterFilter meterFilter() { + return new MeterFilter() { + @Override + public MeterFilterReply accept(Meter.Id id) { + if (id.getName().equals("http.requests")) { + return MeterFilterReply.NEUTRAL; + } + return MeterFilterReply.DENY; + } + }; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 326ba09d..6ee59db7 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -1,48 +1,48 @@ -package stirling.software.SPDF.config; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class MetricsFilter extends OncePerRequestFilter { - - private final MeterRegistry meterRegistry; - - @Autowired - public MetricsFilter(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - String uri = request.getRequestURI(); - - //System.out.println("uri="+uri + ", method=" + request.getMethod() ); - // Ignore static resources - if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { - Counter counter = Counter.builder("http.requests") - .tag("uri", uri) - .tag("method", request.getMethod()) - .register(meterRegistry); - - counter.increment(); - //System.out.println("Counted"); - } - - filterChain.doFilter(request, response); - } - - - -} +package stirling.software.SPDF.config; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class MetricsFilter extends OncePerRequestFilter { + + private final MeterRegistry meterRegistry; + + @Autowired + public MetricsFilter(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); + + //System.out.println("uri="+uri + ", method=" + request.getMethod() ); + // Ignore static resources + if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { + Counter counter = Counter.builder("http.requests") + .tag("uri", uri) + .tag("method", request.getMethod()) + .register(meterRegistry); + + counter.increment(); + //System.out.println("Counted"); + } + + filterChain.doFilter(request, response); + } + + + +} diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index d7495aca..573873da 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,36 +1,36 @@ -package stirling.software.SPDF.config; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; - -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - Properties props = new Properties(); - try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { - props.load(input); - version = props.getProperty("version"); - } catch (IOException ex) { - ex.printStackTrace(); - version = "1.0.0"; // default version if all else fails - } - } - - return new OpenAPI().components(new Components()).info( - new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); - } - - -} +package stirling.software.SPDF.config; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + Properties props = new Properties(); + try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { + props.load(input); + version = props.getProperty("version"); + } catch (IOException ex) { + ex.printStackTrace(); + version = "1.0.0"; // default version if all else fails + } + } + + return new OpenAPI().components(new Components()).info( + new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); + } + + +} diff --git a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java index a25cbbbf..28653d35 100644 --- a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java +++ b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java @@ -1,20 +1,20 @@ -package stirling.software.SPDF.config; - - -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -@Component -public class StartupApplicationListener implements ApplicationListener { - - public static LocalDateTime startTime; - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - startTime = LocalDateTime.now(); - } -} - +package stirling.software.SPDF.config; + + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class StartupApplicationListener implements ApplicationListener { + + public static LocalDateTime startTime; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + startTime = LocalDateTime.now(); + } +} + diff --git a/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java new file mode 100644 index 00000000..3ecb3ead --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java @@ -0,0 +1,125 @@ +package stirling.software.SPDF.config; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.ConsumptionProbe; +import io.github.bucket4j.Refill; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.Role; +@Component +public class UserBasedRateLimitingFilter extends OncePerRequestFilter { + + private final Map apiBuckets = new ConcurrentHashMap<>(); + private final Map webBuckets = new ConcurrentHashMap<>(); + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + @Qualifier("rateLimit") + public boolean rateLimit; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!rateLimit) { + // If rateLimit is not enabled, just pass all requests without rate limiting + filterChain.doFilter(request, response); + return; + } + + String method = request.getMethod(); + if (!"POST".equalsIgnoreCase(method)) { + // If the request is not a POST, just pass it through without rate limiting + filterChain.doFilter(request, response); + return; + } + + String identifier = null; + + // Check for API key in the request headers + String apiKey = request.getHeader("X-API-Key"); + if (apiKey != null && !apiKey.trim().isEmpty()) { + identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames + } else { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + identifier = userDetails.getUsername(); + } + } + + // If neither API key nor an authenticated user is present, use IP address + if (identifier == null) { + identifier = request.getRemoteAddr(); + } + + Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); + + if (request.getHeader("X-API-Key") != null) { + // It's an API call + processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); + } else { + // It's a Web UI call + processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); + } + } + + private Role getRoleFromAuthentication(Authentication authentication) { + if (authentication != null && authentication.isAuthenticated()) { + for (GrantedAuthority authority : authentication.getAuthorities()) { + try { + return Role.fromString(authority.getAuthority()); + } catch (IllegalArgumentException ex) { + // Ignore and continue to next authority. + } + } + } + throw new IllegalStateException("User does not have a valid role."); + } + + private void processRequest(int limitPerDay, String identifier, Map buckets, + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); + ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); + + if (probe.isConsumed()) { + response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); + filterChain.doFilter(request, response); + } else { + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); + response.getWriter().write("Rate limit exceeded for POST requests."); + } + } + + private Bucket createUserBucket(int limitPerDay) { + Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); + return Bucket.builder().addLimit(limit).build(); + } +} + + + diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 10a88e97..dca3f91c 100644 --- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,27 +1,27 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - @Autowired - private EndpointInterceptor endpointInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(endpointInterceptor); - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - // Handler for external static resources - registry.addResourceHandler("/**") - .addResourceLocations("file:customFiles/static/", "classpath:/static/") - .setCachePeriod(0); // Optional: disable caching - } -} +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private EndpointInterceptor endpointInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(endpointInterceptor); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Handler for external static resources + registry.addResourceHandler("/**") + .addResourceLocations("file:customFiles/static/", "classpath:/static/"); + //.setCachePeriod(0); // Optional: disable caching + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java new file mode 100644 index 00000000..6b300920 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -0,0 +1,26 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; + +public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) + throws IOException, ServletException { + if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { + setDefaultFailureUrl("/login?error=badcredentials"); + } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + setDefaultFailureUrl("/login?error=locked"); + } + super.onAuthenticationFailure(request, response, exception); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java new file mode 100644 index 00000000..074316ee --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -0,0 +1,46 @@ +package stirling.software.SPDF.config.security; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.repository.UserRepository; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + user.isEnabled(), + true, true, true, + getAuthorities(user.getAuthorities()) + ); + } + + private Collection getAuthorities(Set authorities) { + return authorities.stream() + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSetup.java new file mode 100644 index 00000000..d1d286dc --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSetup.java @@ -0,0 +1,26 @@ +package stirling.software.SPDF.config.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import stirling.software.SPDF.model.Role; +@Component +public class InitialSetup { + + @Autowired + private UserService userService; + + @PostConstruct + public void init() { + if(!userService.hasUsers()) { + String initialUsername = System.getenv("INITIAL_USERNAME"); + String initialPassword = System.getenv("INITIAL_PASSWORD"); + if(initialUsername != null && initialPassword != null) { + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + } + + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java new file mode 100644 index 00000000..11d63a61 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -0,0 +1,90 @@ +package stirling.software.SPDF.config.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@Configuration +public class SecurityConfiguration { + + @Autowired + private UserDetailsService userDetailsService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Autowired + @Lazy + private UserService userService; + + @Autowired + @Qualifier("loginEnabled") + public boolean loginEnabledValue; + + @Autowired + private UserAuthenticationFilter userAuthenticationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + if(loginEnabledValue) { + + http.csrf().disable(); + http + .formLogin(formLogin -> formLogin + .loginPage("/login") + .defaultSuccessUrl("/") + .failureHandler(new CustomAuthenticationFailureHandler()) + .permitAll() + ) + .logout(logout -> logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login?logout=true") + .invalidateHttpSession(true) // Invalidate session + .deleteCookies("JSESSIONID") + ) + .authorizeHttpRequests(authz -> authz + .requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/")) + .permitAll() + .anyRequest().authenticated() + ) + .userDetailsService(userDetailsService) + .authenticationProvider(authenticationProvider()); + } else { + http + .csrf().disable() + .authorizeHttpRequests(authz -> authz + .anyRequest().permitAll() + ); + } + return http.build(); + } + + + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + + +} + diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java new file mode 100644 index 00000000..d7d40710 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -0,0 +1,120 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.ConsumptionProbe; +import io.github.bucket4j.Refill; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.ApiKeyAuthenticationToken; +@Component +public class UserAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + @Lazy + private UserService userService; + + + @Autowired + @Qualifier("loginEnabled") + public boolean loginEnabledValue; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + if (!loginEnabledValue) { + // If login is not enabled, just pass all requests without authentication + filterChain.doFilter(request, response); + return; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // Check for API key in the request headers if no authentication exists + if (authentication == null || !authentication.isAuthenticated()) { + String apiKey = request.getHeader("X-API-Key"); + if (apiKey != null && !apiKey.trim().isEmpty()) { + try { + // Use API key to authenticate. This requires you to have an authentication provider for API keys. + UserDetails userDetails = userService.loadUserByApiKey(apiKey); + if(userDetails == null) + { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Invalid API Key."); + return; + } + authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (AuthenticationException e) { + // If API key authentication fails, deny the request + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Invalid API Key."); + return; + } + } + } + + // If we still don't have any authentication, deny the request + if (authentication == null || !authentication.isAuthenticated()) { + String method = request.getMethod(); + if ("GET".equalsIgnoreCase(method)) { + response.sendRedirect("/login"); // redirect to the login page + return; + } + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); + return; + } + + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String uri = request.getRequestURI(); + + String[] permitAllPatterns = { + "/login", + "/register", + "/error", + "/images/", + "/public/", + "/css/", + "/js/" + }; + + for (String pattern : permitAllPatterns) { + if (uri.startsWith(pattern) || uri.endsWith(".svg")) { + return true; + } + } + + return false; + } + +} diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java new file mode 100644 index 00000000..5a28e776 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -0,0 +1,178 @@ +package stirling.software.SPDF.config.security; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.Collection; +import java.util.HashMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.repository.UserRepository; +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.Role; +import stirling.software.SPDF.model.User; +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + public Authentication getAuthentication(String apiKey) { + User user = getUserByApiKey(apiKey); + if (user == null) { + throw new UsernameNotFoundException("API key is not valid"); + } + + // Convert the user into an Authentication object + return new UsernamePasswordAuthenticationToken( + user, // principal (typically the user) + null, // credentials (we don't expose the password or API key here) + getAuthorities(user) // user's authorities (roles/permissions) + ); + } + + private Collection getAuthorities(User user) { + // Convert each Authority object into a SimpleGrantedAuthority object. + return user.getAuthorities().stream() + .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); + + + } + + private String generateApiKey() { + String apiKey; + do { + apiKey = UUID.randomUUID().toString(); + } while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness + return apiKey; + } + + public User addApiKeyToUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + user.setApiKey(generateApiKey()); + return userRepository.save(user); + } + + public User refreshApiKeyForUser(String username) { + return addApiKeyToUser(username); // reuse the add API key method for refreshing + } + + public String getApiKeyForUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return user.getApiKey(); + } + + public boolean isValidApiKey(String apiKey) { + return userRepository.findByApiKey(apiKey) != null; + } + + public User getUserByApiKey(String apiKey) { + return userRepository.findByApiKey(apiKey); + } + + public UserDetails loadUserByApiKey(String apiKey) { + User userOptional = userRepository.findByApiKey(apiKey); + if (userOptional != null) { + User user = userOptional; + // Convert your User entity to a UserDetails object with authorities + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), // you might not need this for API key auth + getAuthorities(user) + ); + } + return null; // or throw an exception + } + + + public boolean validateApiKeyForUser(String username, String apiKey) { + Optional userOpt = userRepository.findByUsername(username); + return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); + } + + public void saveUser(String username, String password) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setEnabled(true); + userRepository.save(user); + } + + public void saveUser(String username, String password, String role) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + userRepository.save(user); + } + + public void deleteUser(String username) { + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + userRepository.delete(userOpt.get()); + } + } + + public boolean usernameExists(String username) { + return userRepository.findByUsername(username).isPresent(); + } + + public boolean hasUsers() { + return userRepository.count() > 0; + } + + public void updateUserSettings(String username, Map updates) { + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + User user = userOpt.get(); + Map settingsMap = user.getSettings(); + + if(settingsMap == null) { + settingsMap = new HashMap(); + } + settingsMap.clear(); + settingsMap.putAll(updates); + user.setSettings(settingsMap); + + userRepository.save(user); + } + } + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public void changeUsername(User user, String newUsername) { + user.setUsername(newUsername); + userRepository.save(user); + } + + public void changePassword(User user, String newPassword) { + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + } + + public boolean isPasswordCorrect(User user, String currentPassword) { + return passwordEncoder.matches(currentPassword, user.getPassword()); + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 7639d154..035a1214 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,7 +1,13 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -11,10 +17,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; - +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,55 +33,93 @@ public class MergeController { private static final Logger logger = LoggerFactory.getLogger(MergeController.class); - private PDDocument mergeDocuments(List documents) throws IOException { - // Create a new empty document - PDDocument mergedDoc = new PDDocument(); - // Iterate over the list of documents and add their pages to the merged document - for (PDDocument doc : documents) { - // Get all pages from the current document - PDPageTree pages = doc.getPages(); - // Iterate over the pages and add them to the merged document - for (PDPage page : pages) { - mergedDoc.addPage(page); - } - - +private PDDocument mergeDocuments(List documents) throws IOException { + PDDocument mergedDoc = new PDDocument(); + for (PDDocument doc : documents) { + for (PDPage page : doc.getPages()) { + mergedDoc.addPage(page); } + } + return mergedDoc; +} - // Return the merged document - return mergedDoc; +private Comparator getSortComparator(String sortType) { + switch (sortType) { + case "byFileName": + return Comparator.comparing(MultipartFile::getOriginalFilename); + case "byDateModified": + return (file1, file2) -> { + try { + BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); + } catch (IOException e) { + return 0; // If there's an error, treat them as equal + } + }; + case "byDateCreated": + return (file1, file2) -> { + try { + BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + return attr1.creationTime().compareTo(attr2.creationTime()); + } catch (IOException e) { + return 0; // If there's an error, treat them as equal + } + }; + case "byPDFTitle": + return (file1, file2) -> { + try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); + PDDocument doc2 = PDDocument.load(file2.getInputStream())) { + String title1 = doc1.getDocumentInformation().getTitle(); + String title2 = doc2.getDocumentInformation().getTitle(); + return title1.compareTo(title2); + } catch (IOException e) { + return 0; + } + }; + case "orderProvided": + default: + return (file1, file2) -> 0; // Default is the order provided + } +} + +@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") +@Operation(summary = "Merge multiple PDF files into one", + description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") +public ResponseEntity mergePdfs( + @RequestPart(required = true, value = "fileInput") MultipartFile[] files, + @RequestParam(value = "sortType", defaultValue = "orderProvided") + @Parameter(schema = @Schema(description = "The type of sorting to be applied on the input files before merging.", + allowableValues = { + "orderProvided", + "byFileName", + "byDateModified", + "byDateCreated", + "byPDFTitle" + })) + String sortType) throws IOException { + + Arrays.sort(files, getSortComparator(sortType)); + + List documents = new ArrayList<>(); + for (MultipartFile file : files) { + try (InputStream is = file.getInputStream()) { + documents.add(PDDocument.load(is)); + } } - @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") - @Operation( - summary = "Merge multiple PDF files into one", - description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO" - ) - public ResponseEntity mergePdfs( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF files to be merged into a single file", required = true) - MultipartFile[] files) throws IOException { - // Read the input PDF files into PDDocument objects - List documents = new ArrayList<>(); - - // Loop through the files array and read each file into a PDDocument - for (MultipartFile file : files) { - documents.add(PDDocument.load(file.getInputStream())); - } - - PDDocument mergedDoc = mergeDocuments(documents); - - - // Return the merged PDF as a response + try (PDDocument mergedDoc = mergeDocuments(documents)) { ResponseEntity response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); - - for (PDDocument doc : documents) { - // Close the document after processing - doc.close(); - } - return response; + } finally { + for (PDDocument doc : documents) { + if (doc != null) { + doc.close(); + } + } } +} } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java new file mode 100644 index 00000000..cd1be5a0 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -0,0 +1,163 @@ +package stirling.software.SPDF.controller.api; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.User; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; + +@Controller +public class UserController { + + @Autowired + private UserService userService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @PostMapping("/register") + public String register(@RequestParam String username, @RequestParam String password, Model model) { + if(userService.usernameExists(username)) { + model.addAttribute("error", "Username already exists"); + return "register"; + } + + userService.saveUser(username, password); + return "redirect:/login?registered=true"; + } + + @PostMapping("/change-username") + public ResponseEntity changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + + Optional userOpt = userService.findByUsername(principal.getName()); + + if(userOpt == null || userOpt.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + } + User user = userOpt.get(); + + if(!userService.isPasswordCorrect(user, currentPassword)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + } + + if(userService.usernameExists(newUsername)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists."); + } + + userService.changeUsername(user, newUsername); + + // Logout using Spring's utility + new SecurityContextLogoutHandler().logout(request, response, null); + + + return ResponseEntity.ok("Username updated successfully."); + } + + @PostMapping("/change-password") + public ResponseEntity changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + + Optional userOpt = userService.findByUsername(principal.getName()); + + if(userOpt == null || userOpt.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + } + User user = userOpt.get(); + if(!userService.isPasswordCorrect(user, currentPassword)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + } + + userService.changePassword(user, passwordEncoder.encode(newPassword)); + + // Logout using Spring's utility + new SecurityContextLogoutHandler().logout(request, response, null); + + return ResponseEntity.ok("Password updated successfully."); + } + + @PostMapping("/updateUserSettings") + public String updateUserSettings(HttpServletRequest request, Principal principal) { + Map paramMap = request.getParameterMap(); + Map updates = new HashMap<>(); + + System.out.println("Received parameter map: " + paramMap); + + for (Map.Entry entry : paramMap.entrySet()) { + updates.put(entry.getKey(), entry.getValue()[0]); + } + + System.out.println("Processed updates: " + updates); + + // Assuming you have a method in userService to update the settings for a user + userService.updateUserSettings(principal.getName(), updates); + + return "redirect:/account"; // Redirect to a page of your choice after updating + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/admin/saveUser") + public String saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role) { + userService.saveUser(username, password, role); + return "redirect:/addUsers"; // Redirect to account page after adding the user + } + + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/admin/deleteUser/{username}") + public String deleteUser(@PathVariable String username) { + userService.deleteUser(username); + return "redirect:/addUsers"; + } + + @PostMapping("/get-api-key") + public ResponseEntity getApiKey(Principal principal) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + String username = principal.getName(); + String apiKey = userService.getApiKeyForUser(username); + if (apiKey == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user."); + } + return ResponseEntity.ok(apiKey); + } + + @PostMapping("/update-api-key") + public ResponseEntity updateApiKey(Principal principal) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + } + String username = principal.getName(); + User user = userService.refreshApiKeyForUser(username); + String apiKey = user.getApiKey(); + if (apiKey == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user."); + } + return ResponseEntity.ok(apiKey); + } + + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 909be730..bcc7e37f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -52,13 +52,13 @@ public class PasswordController { @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to which the password should be added", required = true) MultipartFile fileInput, - @RequestParam(value = "", name = "ownerPassword") + @RequestParam(value = "", name = "ownerPassword", required = false, defaultValue = "") @Parameter(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)") String ownerPassword, - @RequestParam( name = "password", required = false) + @RequestParam( name = "password", required = false, defaultValue = "") @Parameter(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)") String password, - @RequestParam( name = "keyLength", required = false) + @RequestParam( name = "keyLength", required = false, defaultValue = "256") @Parameter(description = "The length of the encryption key", schema = @Schema(allowableValues = {"40", "128", "256"})) int keyLength, @RequestParam( name = "canAssembleDocument", required = false) @@ -98,15 +98,15 @@ public class PasswordController { ap.setCanPrint(!canPrint); ap.setCanPrintFaithful(!canPrintFaithful); StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); - - - - spp.setEncryptionKeyLength(keyLength); + if(!"".equals(ownerPassword) || !"".equals(password)) { + spp.setEncryptionKeyLength(keyLength); + } spp.setPermissions(ap); - document.protect(spp); + if("".equals(ownerPassword) && "".equals(password)) + return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf"); return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); } diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 4d6e991a..c415d8fd 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -4,11 +4,13 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -16,18 +18,108 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.repository.UserRepository; @Controller @Tag(name = "General", description = "General APIs") public class GeneralWebController { + + @GetMapping("/login") + public String login(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication != null && authentication.isAuthenticated()) { + return "redirect:/"; + } + + if (request.getParameter("error") != null) { + + model.addAttribute("error", request.getParameter("error")); + } + if (request.getParameter("logout") != null) { + + model.addAttribute("logoutMessage", "You have been logged out."); + } + + return "login"; + } + @Autowired + private UserRepository userRepository; // Assuming you have a repository for user operations + + @Autowired + private UserService userService; // Assuming you have a repository for user operations + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/addUsers") + public String showAddUserForm(Model model) { + List allUsers = userRepository.findAll(); + model.addAttribute("users", allUsers); + return "addUsers"; + } + + + + @GetMapping("/account") + public String account(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/"; + } + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; + + // Retrieve username and other attributes + String username = userDetails.getUsername(); + + // Fetch user details from the database + Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } + + // Convert settings map to JSON string + ObjectMapper objectMapper = new ObjectMapper(); + String settingsJson; + try { + settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); + } catch (JsonProcessingException e) { + // Handle JSON conversion error + e.printStackTrace(); + return "redirect:/error"; // Example redirection in case of error + } + + // Add attributes to the model + model.addAttribute("username", username); + model.addAttribute("role", user.get().getRolesAsString()); + model.addAttribute("settings", settingsJson); + } + } else { + return "redirect:/"; + } + return "account"; + } + + + + @GetMapping("/pipeline") @Hidden public String pipelineForm(Model model) { diff --git a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java new file mode 100644 index 00000000..3276ac0c --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java @@ -0,0 +1,49 @@ +package stirling.software.SPDF.model; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + private Object credentials; + + public ApiKeyAuthenticationToken(String apiKey) { + super(null); + this.principal = null; + this.credentials = apiKey; + setAuthenticated(false); + } + + public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection authorities) { + super(authorities); + this.principal = principal; // principal can be a UserDetails object + this.credentials = apiKey; + super.setAuthenticated(true); // this authentication is trusted + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); + } + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + credentials = null; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Authority.java b/src/main/java/stirling/software/SPDF/model/Authority.java new file mode 100644 index 00000000..8be853ea --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/Authority.java @@ -0,0 +1,64 @@ +package stirling.software.SPDF.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "authorities") +public class Authority { + + public Authority() { + + } + + + public Authority(String authority, User user) { + this.authority = authority; + this.user = user; + user.getAuthorities().add(this); + } + + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "authority") + private String authority; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAuthority() { + return authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + +} diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java new file mode 100644 index 00000000..1b775de0 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -0,0 +1,50 @@ +package stirling.software.SPDF.model; +public enum Role { + + // Unlimited access + ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE), + + // Unlimited access + USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE), + + // 40 API calls Per Day, 40 web calls + LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40), + + // 20 API calls Per Day, 20 web calls + EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20), + + // 0 API calls per day and 20 web calls + WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20); + + private final String roleId; + private final int apiCallsPerDay; + private final int webCallsPerDay; + + Role(String roleId, int apiCallsPerDay, int webCallsPerDay) { + this.roleId = roleId; + this.apiCallsPerDay = apiCallsPerDay; + this.webCallsPerDay = webCallsPerDay; + } + + public String getRoleId() { + return roleId; + } + + public int getApiCallsPerDay() { + return apiCallsPerDay; + } + + public int getWebCallsPerDay() { + return webCallsPerDay; + } + + public static Role fromString(String roleId) { + for (Role role : Role.values()) { + if (role.getRoleId().equalsIgnoreCase(roleId)) { + return role; + } + } + throw new IllegalArgumentException("No Role defined for id: " + roleId); + } + +} diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java new file mode 100644 index 00000000..c2b80e83 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -0,0 +1,124 @@ +package stirling.software.SPDF.model; + +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MapKeyColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.JoinColumn; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; + + @Column(name = "username", unique = true) + private String username; + + @Column(name = "password") + private String password; + + @Column(name = "apiKey") + private String apiKey; + + @Column(name = "enabled") + private boolean enabled; + + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") + private Set authorities = new HashSet<>(); + + @ElementCollection + @MapKeyColumn(name = "setting_key") + @Column(name = "setting_value") + @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) + private Map settings = new HashMap<>(); // Key-value pairs of settings. + + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public Map getSettings() { + return settings; + } + + public void setSettings(Map settings) { + this.settings = settings; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + public void addAuthorities(Set authorities) { + this.authorities.addAll(authorities); + } + public void addAuthority(Authority authorities) { + this.authorities.add(authorities); + } + + public String getRolesAsString() { + return this.authorities.stream() + .map(Authority::getAuthority) + .collect(Collectors.joining(", ")); + } + + +} diff --git a/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java new file mode 100644 index 00000000..62f546b8 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java @@ -0,0 +1,12 @@ +package stirling.software.SPDF.repository; + +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; + +import stirling.software.SPDF.model.Authority; + +public interface AuthorityRepository extends JpaRepository { + //Set findByUsername(String username); + Set findByUser_Username(String username); +} diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java new file mode 100644 index 00000000..744953d7 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -0,0 +1,13 @@ +package stirling.software.SPDF.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import stirling.software.SPDF.model.User; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + User findByApiKey(String apiKey); +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c4118342..20638f7d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,7 +17,10 @@ server.error.include-exception=true server.error.include-message=always #logging.level.org.springframework.web=DEBUG +#logging.level.org.springframework=DEBUG +#logging.level.org.springframework.security=DEBUG +#login.enabled=true server.servlet.session.tracking-modes=cookie server.servlet.context-path=${APP_ROOT_PATH:/} @@ -32,4 +35,12 @@ spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000} spring.resources.static-locations=file:customFiles/static/ #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ -#spring.thymeleaf.cache=false \ No newline at end of file +#spring.thymeleaf.cache=false + + +spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.h2.console.enabled=true +spring.jpa.hibernate.ddl-auto=update diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 53a0fa44..7f9cd9cb 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=\u0641\u062A\u062D \u0641\u064A \u0646\u0641\u0633 \u0 settings.downloadOption.2=\u0641\u062A\u062D \u0641\u064A \u0646\u0627\u0641\u0630\u0629 \u062C\u062F\u064A\u062F\u0629 settings.downloadOption.3=\u062A\u0646\u0632\u064A\u0644 \u0627\u0644\u0645\u0644\u0641 settings.zipThreshold=\u0645\u0644\u0641\u0627\u062A \u0645\u0636\u063A\u0648\u0637\u0629 \u0639\u0646\u062F \u062A\u062C\u0627\u0648\u0632 \u0639\u062F\u062F \u0627\u0644\u0645\u0644\u0641\u0627\u062A \u0627\u0644\u062A\u064A \u062A\u0645 \u062A\u0646\u0632\u064A\u0644\u0647\u0627 +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=إضافة صورة #merge merge.title=دمج merge.header=دمج ملفات PDF متعددة (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=دمج @@ -626,17 +676,14 @@ watermark.selectText.4=دوران (0-360): watermark.selectText.5=widthSpacer (مسافة بين كل علامة مائية أفقيًا): watermark.selectText.6=heightSpacer (مسافة بين كل علامة مائية عموديًا): watermark.selectText.7=\u0627\u0644\u062A\u0639\u062A\u064A\u0645 (0\u066A - 100\u066A): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=إضافة علامة مائية -#remove-watermark -remove-watermark.title=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629 -remove-watermark.header=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629 -remove-watermark.selectText.1=\u062D\u062F\u062F PDF \u0644\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629 \u0645\u0646: -remove-watermark.selectText.2=\u0646\u0635 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629: -remove-watermark.submit=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629 - - #Change permissions permissions.title=تغيير الأذونات permissions.header=تغيير الأذونات diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index 6d653a9b..68d8b466 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Obre mateixa finestra settings.downloadOption.2=Obre mateixa finestra settings.downloadOption.3=Descarrega Arxiu settings.zipThreshold=Comprimiu els fitxers quan el nombre de fitxers baixats superi +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Afegir Imatge #merge merge.title=Fusiona merge.header=Fusiona múltiples PDFs (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Fusiona @@ -626,17 +676,14 @@ watermark.selectText.4=Rotació (0-360): watermark.selectText.5=separació d'amplada (Espai horitzontal entre cada Marca d'Aigua): watermark.selectText.6=separació d'alçada (Espai vertical entre cada Marca d'Aigua): watermark.selectText.7=Opacitat (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Afegir Marca d'Aigua -#remove-watermark -remove-watermark.title=Elimina Marca d'Aigua -remove-watermark.header=Elimina Marca d'Aigua -remove-watermark.selectText.1=Seleciona PDF per eliminar Marca d'Aigua: -remove-watermark.selectText.2=Text de la Marca d'Aigua: -remove-watermark.submit=Elimina Marca d'Aigua - - #Change permissions permissions.title=Canviar Permissos permissions.header=Canviar Permissos diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 1b5691bf..35043038 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Im selben Fenster öffnen settings.downloadOption.2=In neuem Fenster öffnen settings.downloadOption.3=Datei herunterladen settings.zipThreshold=Dateien komprimieren, wenn die Anzahl der heruntergeladenen Dateien überschritten wird +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Bild hinzufügen #merge merge.title=Zusammenführen merge.header=Mehrere PDFs zusammenführen (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Zusammenführen @@ -626,17 +676,14 @@ watermark.selectText.4=Drehung (0-360): watermark.selectText.5=breiteSpacer (horizontaler Abstand zwischen den einzelnen Wasserzeichen): watermark.selectText.6=höheSpacer (vertikaler Abstand zwischen den einzelnen Wasserzeichen): watermark.selectText.7=Deckkraft (0% - 100 %): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Wasserzeichen hinzufügen -#remove-watermark -remove-watermark.title=Wasserzeichen entfernen -remove-watermark.header=Wasserzeichen entfernen -remove-watermark.selectText.1=PDF auswählen, um Wasserzeichen zu entfernen von: -remove-watermark.selectText.2=Wasserzeichentext: -remove-watermark.submit=Wasserzeichen entfernen - - #Change permissions permissions.title=Berechtigungen ändern permissions.header=Berechtigungen ändern diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 977260a5..58fc107f 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -31,7 +31,11 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +58,41 @@ settings.downloadOption.1=Open in same window settings.downloadOption.2=Open in new window settings.downloadOption.3=Download file settings.zipThreshold=Zip files when the number of downloaded files exceeds +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -520,6 +559,8 @@ addImage.submit=Add image #merge merge.title=Merge merge.header=Merge multiple PDFs (2+) +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Merge @@ -578,9 +619,9 @@ imageToPDF.selectText.5=Convert to separate PDFs pdfToImage.title=PDF to Image pdfToImage.header=PDF to Image pdfToImage.selectText=Image Format -pdfToImage.singleOrMultiple=Image result type -pdfToImage.single=Single Big Image -pdfToImage.multi=Multiple Images +pdfToImage.singleOrMultiple=Page to Image result type +pdfToImage.single=Single Big Image Combing all pages +pdfToImage.multi=Multiple Images, one image per page pdfToImage.colorType=Colour type pdfToImage.color=Colour pdfToImage.grey=Greyscale @@ -620,17 +661,11 @@ watermark.selectText.4=Rotation (0-360): watermark.selectText.5=widthSpacer (Space between each watermark horizontally): watermark.selectText.6=heightSpacer (Space between each watermark vertically): watermark.selectText.7=Opacity (0% - 100%): +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Add Watermark -#remove-watermark -remove-watermark.title=Remove Watermark -remove-watermark.header=Remove Watermark -remove-watermark.selectText.1=Select PDF to remove watermark from: -remove-watermark.selectText.2=Watermark Text: -remove-watermark.submit=Remove Watermark - - #Change permissions permissions.title=Change Permissions permissions.header=Change Permissions diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index b1b05e24..778629bf 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -1,7 +1,7 @@ ########### # Generic # ########### -# the direction that the language is written (ltr = left to right, rtl = right to left) +# the direction that the language is written (ltr=left to right, rtl = right to left) language.direction=ltr pdfPrompt=Select PDF(s) @@ -25,13 +25,20 @@ downloadPdf=Download PDF text=Text font=Font selectFillter=-- Select -- -pageNum=Page Number +pageNum=Page Number sizes.small=Small sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Open in same window settings.downloadOption.2=Open in new window settings.downloadOption.3=Download file settings.zipThreshold=Zip files when the number of downloaded files exceeds +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -71,7 +122,7 @@ merge.tags=merge,Page operations,Back end,server side home.split.title=Split home.split.desc=Split PDFs into multiple documents -split.tags=Page operations,divide,Multi Page,cut,server side +split.tags=Page operations,divide,Multi Page,cut,server side home.rotate.title=Rotate home.rotate.desc=Easily rotate your PDFs. @@ -208,7 +259,7 @@ home.add-page-numbers.desc=Add Page numbers throughout a document in a set locat add-page-numbers.tags=paginate,label,organize,index home.auto-rename.title=Auto Rename PDF File -home.auto-rename.desc=Auto renames a PDF file based on its detected header +home.auto-rename.desc=Auto renames a PDF file based on its detected header auto-rename.tags=auto-detect,header-based,organize,relabel home.adjust-contrast.title=Adjust Colors/Contrast @@ -396,16 +447,16 @@ scalePages.submit=Submit #certSign certSign.title=Certificate Signing certSign.header=Sign a PDF with your certificate (Work in progress) -certSign.selectPDF=Select a PDF File for Signing: -certSign.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der): -certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der): -certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate): +certSign.selectPDF=Select a PDF File for Signing: +certSign.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der): +certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der): +certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate): certSign.certType=Certificate Type -certSign.password=Enter Your Keystore or Private Key Password (If Any): +certSign.password=Enter Your Keystore or Private Key Password (If Any): certSign.showSig=Show Signature certSign.reason=Reason certSign.location=Location -certSign.name=Name +certSign.name=Name certSign.submit=Sign PDF @@ -505,7 +556,7 @@ compress.selectText.1=Manual Mode - From 1 to 4 compress.selectText.2=Optimization level: compress.selectText.3=4 (Terrible for text images) compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size -compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB) +compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB) compress.submit=Compress @@ -520,6 +571,11 @@ addImage.submit=Add image #merge merge.title=Merge merge.header=Merge multiple PDFs (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Merge @@ -620,17 +676,14 @@ watermark.selectText.4=Rotation (0-360): watermark.selectText.5=widthSpacer (Space between each watermark horizontally): watermark.selectText.6=heightSpacer (Space between each watermark vertically): watermark.selectText.7=Opacity (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Add Watermark -#remove-watermark -remove-watermark.title=Remove Watermark -remove-watermark.header=Remove Watermark -remove-watermark.selectText.1=Select PDF to remove watermark from: -remove-watermark.selectText.2=Watermark Text: -remove-watermark.submit=Remove Watermark - - #Change permissions permissions.title=Change Permissions permissions.header=Change Permissions @@ -657,7 +710,7 @@ removePassword.submit=Remove #changeMetadata -changeMetadata.title=Change Metadata +changeMetadata.title=Title: changeMetadata.header=Change Metadata changeMetadata.selectText.1=Please edit the variables you wish to change changeMetadata.selectText.2=Delete all metadata @@ -725,4 +778,4 @@ PDFToHTML.submit=Convert PDFToXML.title=PDF to XML PDFToXML.header=PDF to XML PDFToXML.credit=This service uses LibreOffice for file conversion. -PDFToXML.submit=Convert \ No newline at end of file +PDFToXML.submit=Convert diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index 6f31beec..6199dad7 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -31,7 +31,14 @@ sizes.medium=Mediano sizes.large=Grande sizes.x-large=Extra grande error.pdfPassword=El documento PDF está protegido con contraseña y no se ha proporcionado o es incorrecta - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Abrir en la misma ventana settings.downloadOption.2=Abrir en una nueva ventana settings.downloadOption.3=Descargar el fichero settings.zipThreshold=Ficheros ZIP cuando excede el número de ficheros descargados +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Añadir imagen #merge merge.title=Unir merge.header=Unir múltiples PDFs (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Unir @@ -626,17 +676,14 @@ watermark.selectText.4=Rotación (0-360): watermark.selectText.5=Ancho (Espacio entre cada marca de agua horizontalmente): watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente): watermark.selectText.7=Opacidad (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Añadir marca de agua -#remove-watermark -remove-watermark.title=Eliminar marca de agua -remove-watermark.header=Eliminar marca de agua -remove-watermark.selectText.1=Seleccionar PDF para eliminar la marca de agua: -remove-watermark.selectText.2=Texto de la marca de agua: -remove-watermark.submit=Eliminar marca de agua - - #Change permissions permissions.title=Cambiar permisos permissions.header=Cambiar permisos diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index c1e6eba0..47aa1990 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=PDF dokumentua pasahitzarekin babestuta dago eta pasahitza ez da sartu edo akastuna da - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Ireki leiho berean settings.downloadOption.2=Ireki leiho berrian settings.downloadOption.3=Deskargatu fitxategia settings.zipThreshold=ZIP fitxategiak deskargatutako fitxategi kopurua gainditzen denean +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Gehitu irudia #merge merge.title=Elkartu merge.header=Elkartu zenbait PDF (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Elkartu @@ -626,17 +676,14 @@ watermark.selectText.4=Errotazioa (0-360): watermark.selectText.5=Zabalera (ur-marka bakoitzaren arteko espazioa horizontalean): watermark.selectText.6=Altuera (ur-marka bakoitzaren arteko espazioa bertikalean): watermark.selectText.7=Opakutasuna (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Gehitu ur-marka -#remove-watermark -remove-watermark.title=Ezabatu ur-marka -remove-watermark.header=Ezabatu ur-marka -remove-watermark.selectText.1=Hautatu PDFa ur-marka ezabatzeko: -remove-watermark.selectText.2=Ur-markaren testua: -remove-watermark.submit=Ezabatu ur-marka - - #Change permissions permissions.title=Aldatu baimenak permissions.header=Aldatu baimenak diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index 0379d987..d35a801d 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -1,7 +1,7 @@ ########### # Generic # ########### -# the direction that the language is written (ltr = left to right, rtl = right to left) +# the direction that the language is written (ltr=left to right, rtl = right to left) language.direction=ltr pdfPrompt=Sélectionnez le(s) PDF @@ -31,7 +31,14 @@ sizes.medium=Moyen sizes.large=Grand sizes.x-large=Très grand error.pdfPassword=Le document PDF est protégé par un mot de passe et le mot de passe n\u2019a pas été fourni ou était incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Ouvrir dans la même fenêtre settings.downloadOption.2=Ouvrir dans une nouvelle fenêtre settings.downloadOption.3=Télécharger le fichier settings.zipThreshold=Compresser les fichiers en ZIP lorsque le nombre de fichiers téléchargés dépasse +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -278,6 +329,15 @@ pdfToSinglePage.header=Fusionner des pages pdfToSinglePage.submit=Convertir en une seule page +#pageExtracter +########################## +### TODO: Translate ### +########################## +pageExtracter.title=Extract Pages +pageExtracter.header=Extract Pages +pageExtracter.submit=Extract + + #getPdfInfo getPdfInfo.title=Récupérer les informations getPdfInfo.header=Récupérer les informations @@ -293,6 +353,7 @@ MarkdownToPDF.help=(Travail en cours). MarkdownToPDF.credit=Utilise WeasyPrint. + #url-to-pdf URLToPDF.title=URL en PDF URLToPDF.header=URL en PDF @@ -454,7 +515,7 @@ ScannerImageSplit.selectText.8=Définit la surface de contour minimale pour une ScannerImageSplit.selectText.9=Taille de la bordure ScannerImageSplit.selectText.10=Définit la taille de la bordure ajoutée et supprimée pour éviter les bordures blanches dans la sortie (par défaut\u00a0: 1). - + #OCR ocr.title=OCR / Nettoyage des numérisations ocr.header=OCR (Reconnaissance optique de caractères) / Nettoyage des numérisations @@ -513,6 +574,11 @@ addImage.submit=Ajouter une image #merge merge.title=Fusionner merge.header=Fusionner plusieurs PDF +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Fusionner @@ -527,6 +593,16 @@ multiTool.title=Outil multifonction PDF multiTool.header=Outil multifonction PDF +#pageRemover +########################## +### TODO: Translate ### +########################## +pageRemover.title=Page Remover +pageRemover.header=PDF Page remover +pageRemover.pagesToDelete=Pages to delete (Enter a comma-separated list of page numbers) : +pageRemover.submit=Delete Pages + + #rotate rotate.title=Pivoter rotate.header=Pivoter @@ -534,7 +610,7 @@ rotate.selectAngle=Angle de rotation (par multiples de 90\u202fdegrés) rotate.submit=Pivoter -#split +#merge split.title=Diviser split.header=Diviser split.desc.1=Les numéros que vous sélectionnez sont le numéro de page sur lequel vous souhaitez faire une division @@ -549,17 +625,17 @@ split.splitPages=Pages sur lesquelles diviser split.submit=Diviser -#imageToPDF +#merge imageToPDF.title=Image en PDF imageToPDF.header=Image en PDF +imageToPDF.submit=Convertir imageToPDF.selectText.1=Étirer pour adapter imageToPDF.selectText.2=Rotation automatique du PDF imageToPDF.selectText.3=Logique multi-fichiers (uniquement activée si vous travaillez avec plusieurs images) imageToPDF.selectText.4=Fusionner en un seul PDF imageToPDF.selectText.5=Convertir en PDF séparés -imageToPDF.submit=Convertir - - + + #pdfToImage pdfToImage.title=Image en PDF pdfToImage.header=Image en PDF @@ -637,7 +713,7 @@ removePassword.submit=Supprimer #changeMetadata -changeMetadata.title=Modifier les métadonnées +changeMetadata.title=Titre changeMetadata.header=Modifier les métadonnées changeMetadata.selectText.1=Veuillez modifier les variables que vous souhaitez modifier. changeMetadata.selectText.2=Supprimer toutes les métadonnées @@ -656,6 +732,16 @@ changeMetadata.selectText.5=Ajouter une entrée de métadonnées personnalisée changeMetadata.submit=Modifier +#xlsToPdf +########################## +### TODO: Translate ### +########################## +xlsToPdf.title=Excel to PDF +xlsToPdf.header=Excel to PDF +xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert +xlsToPdf.convert=convert + + #pdfToPDFA pdfToPDFA.title=PDF en PDF/A pdfToPDFA.header=PDF en PDF/A diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 4e5cf304..219a3685 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Apri in questa finestra settings.downloadOption.2=Apri in una nuova finestra settings.downloadOption.3=Scarica file settings.zipThreshold=Comprimi file in .zip quando il numero di download supera +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Aggiungi immagine #merge merge.title=Unisci merge.header=Unisci 2 o più PDF +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Unisci @@ -626,17 +676,14 @@ watermark.selectText.4=Rotazione (0-360): watermark.selectText.5=spazio orizzontale (tra ogni filigrana): watermark.selectText.6=spazio verticale (tra ogni filigrana): watermark.selectText.7=Opacità (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Aggiungi Filigrana -#remove-watermark -remove-watermark.title=Rimuovi Filigrana -remove-watermark.header=Rimuovi filigrana -remove-watermark.selectText.1=Seleziona PDF da cui rimuovere la filigrana: -remove-watermark.selectText.2=Testo: -remove-watermark.submit=Rimuovi Filigrana - - #Change permissions permissions.title=Cambia Permessi permissions.header=Cambia permessi diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index 4e49c578..5847a5f6 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=PDFにパスワードが設定されてますが、パスワードが入力されてないか間違ってます。 - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=同じウィンドウで開く settings.downloadOption.2=新しいウィンドウで開く settings.downloadOption.3=ファイルをダウンロード settings.zipThreshold=このファイル数を超えたときにファイルを圧縮する +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=画像の追加 #merge merge.title=結合 merge.header=複数のPDFを結合 (2ファイル以上) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=結合 @@ -626,17 +676,14 @@ watermark.selectText.4=回転 (0-360): watermark.selectText.5=幅スペース (各透かし間の水平方向のスペース): watermark.selectText.6=高さスペース (各透かし間の垂直方向のスペース): watermark.selectText.7=不透明度 (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=透かしを追加 -#remove-watermark -remove-watermark.title=透かしの削除 -remove-watermark.header=透かしの削除 -remove-watermark.selectText.1=透かしを削除するPDFを選択: -remove-watermark.selectText.2=透かしのテキスト: -remove-watermark.submit=透かしを削除 - - #Change permissions permissions.title=権限の変更 permissions.header=権限の変更 diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index 4c5a1e8c..889929f8 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=현재 창에서 열기 settings.downloadOption.2=새 창에서 열기 settings.downloadOption.3=다운로드 settings.zipThreshold=다운로드한 파일 수가 초과된 경우 파일 압축하기 +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=이미지 추가 #merge merge.title=병합 merge.header=여러 개의 PDF 병합 (2개 이상) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=병합 @@ -626,17 +676,14 @@ watermark.selectText.4=회전 각도 (0-360): watermark.selectText.5=가로 간격 (각 워터마크 사이의 가로 공간): watermark.selectText.6=세로 간격 (각 워터마크 사이의 세로 공간): watermark.selectText.7=투명도 (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=워터마크 추가 -#remove-watermark -remove-watermark.title=워터마크 제거 -remove-watermark.header=워터마크 제거 -remove-watermark.selectText.1=워터마크를 제거할 PDF 선택: -remove-watermark.selectText.2=워터마크 텍스트: -remove-watermark.submit=워터마크 제거 - - #Change permissions permissions.title=권한 변경 permissions.header=권한 변경 diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 2e04e80a..a386b8dd 100644 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=Dokument PDF jest zabezpieczony hasłem, musisz podać prawidłowe hasło. - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Otwórz w tym samym oknie settings.downloadOption.2=Otwórz w nowym oknie settings.downloadOption.3=Pobierz plik settings.zipThreshold=Spakuj pliki, gdy liczba pobranych plików przekroczy +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Dodaj obraz #merge merge.title=Połącz merge.header=Połącz wiele dokumentów PDF (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Połącz @@ -626,17 +676,14 @@ watermark.selectText.4=Obrót (0-360): watermark.selectText.5=Odstęp w poziomie (odstęp między każdym znakiem wodnym w poziomie): watermark.selectText.6=Odstęp w pionie (odstęp między każdym znakiem wodnym w pionie): watermark.selectText.7=Nieprzezroczystość (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Dodaj znak wodny -#remove-watermark -remove-watermark.title=Usuń znak wodny -remove-watermark.header=Usuń znak wodny -remove-watermark.selectText.1=Wybierz dokument PDF, aby usunąć znak wodny z: -remove-watermark.selectText.2=Treść zanku wodnego: -remove-watermark.submit=Usuń znak wodny - - #Change permissions permissions.title=Zmień uprawnienia permissions.header=Zmień uprawnienia diff --git a/src/main/resources/messages_pt_BR.properties b/src/main/resources/messages_pt_BR.properties index 21640a5d..1e334c0d 100644 --- a/src/main/resources/messages_pt_BR.properties +++ b/src/main/resources/messages_pt_BR.properties @@ -31,7 +31,14 @@ sizes.medium=Médio sizes.large=Grande sizes.x-large=Muito grande error.pdfPassword=O documento PDF está protegido por senha e a senha não foi fornecida ou está incorreta - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,12 +61,57 @@ settings.downloadOption.1=Abrir na mesma janela settings.downloadOption.2=Abrir em nova janela settings.downloadOption.3=⇬ Fazer download do arquivo settings.zipThreshold=Compactar arquivos quando o número de arquivos baixados exceder +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # ############# home.desc=Seu melhor utilitário para suas necessidades de PDF. + home.multiTool.title=Multiferramenta de PDF home.multiTool.desc=Mesclar, girar, reorganizar e remover páginas multiTool.tags=Multi Ferramenta, Operação Múltipla, Interface do Usuário, Clique e Arraste, Front-end, Lado do Cliente @@ -76,6 +128,7 @@ home.rotate.title=Girar home.rotate.desc=Girar facilmente seus PDFs. rotate.tags=Lado do Servidor + home.imageToPdf.title=Imagem para PDF home.imageToPdf.desc=Converter uma imagem (PNG, JPEG, GIF) em PDF. imageToPdf.tags=conversão, img, jpg, imagem, foto @@ -88,6 +141,7 @@ home.pdfOrganiser.title=Organizar home.pdfOrganiser.desc=Remover/reorganizar as páginas em qualquer ordem. pdfOrganiser.tags=duplex, par, ímpar, ordenar, mover + home.addImage.title=Adicionar Imagem home.addImage.desc=Adicionar uma imagem em um local definido no PDF (trabalho em andamento) addImage.tags=img, jpg, imagem, foto @@ -100,6 +154,7 @@ home.permissions.title=Alterar Permissões home.permissions.desc=Alterar as permissões do seu documento PDF. permissions.tags=leitura, escrita, edição, impressão + home.removePages.title=Remover home.removePages.desc=Excluir as páginas indesejadas do seu documento PDF. removePages.tags=Remover páginas, excluir páginas @@ -116,6 +171,7 @@ home.compressPdfs.title=Comprimir home.compressPdfs.desc=Comprimir PDFs para reduzir o tamanho do arquivo. compressPdfs.tags=compactar, pequeno, mínimo + home.changeMetadata.title=Alterar Metadados home.changeMetadata.desc=Alterar/remover/adicionar metadados de um documento PDF. changeMetadata.tags=Título, autor, data, criação, hora, editor, produtor, estatísticas @@ -128,6 +184,7 @@ home.ocr.title=OCR / Limpeza de Digitalizações home.ocr.desc=A limpeza verifica e detecta texto em imagens de um PDF e o adiciona novamente como texto. ocr.tags=reconhecimento, texto, imagem, digitalização, leitura, identificação, detecção, editável + home.extractImages.title=Extrair Imagens home.extractImages.desc=Extrair todas as imagens de um PDF e salvá-las em um arquivo zip. extractImages.tags=imagem, foto, salvar, arquivo, zip, captura, coleta @@ -152,6 +209,7 @@ home.PDFToHTML.title=PDF para HTML home.PDFToHTML.desc=Converter PDF para o formato HTML PDFToHTML.tags=conteúdo web, compatível com navegador + home.PDFToXML.title=PDF para XML home.PDFToXML.desc=Converter PDF para o formato XML PDFToXML.tags=extração-de-dados,conteúdo-estruturado,interoperabilidade,transformação,converter @@ -228,26 +286,27 @@ home.HTMLToPDF.title=HTML para PDF home.HTMLToPDF.desc=Converte qualquer arquivo HTML ou zip para PDF HTMLToPDF.tags=marcação,conteúdo-web,transformação,converter + home.MarkdownToPDF.title=Markdown para PDF home.MarkdownToPDF.desc=Converte qualquer arquivo Markdown para PDF MarkdownToPDF.tags=marcação,conteúdo-web,transformação,converter + home.getPdfInfo.title=Obter TODAS as Informações de um PDF home.getPdfInfo.desc=Obtém todas as informações possíveis de um PDF getPdfInfo.tags=informações,dados,estatísticas + home.extractPage.title=Extrair Página(s) home.extractPage.desc=Extrai páginas selecionadas de um PDF extractPage.tags=extrair + home.PdfToSinglePage.title=PDF para Página Única Grande home.PdfToSinglePage.desc=Combina todas as páginas de um PDF em uma única página grande PdfToSinglePage.tags=página única -########################## -### TODO: Translate ### -########################## home.showJS.title=Mostrar Javascript home.showJS.desc=Procura e exibe qualquer JavaScript injetado em um PDF showJS.tags=JavaScript @@ -258,30 +317,31 @@ showJS.tags=JavaScript # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Exibir JavaScript showJS.header=Exibir JavaScript showJS.downloadJS=Download do JavaScript showJS.submit=Exibir + #pdfToSinglePage pdfToSinglePage.title=PDF para Página Única pdfToSinglePage.header=PDF para Página Única pdfToSinglePage.submit=Converter para Página Única + #pageExtracter pageExtracter.title=Extrair Páginas pageExtracter.header=Extrair Páginas pageExtracter.submit=Extrair + #getPdfInfo getPdfInfo.title=Obter Informações do PDF getPdfInfo.header=Obter Informações do PDF getPdfInfo.submit=Obter Informações getPdfInfo.downloadJson=Download JSON + #markdown-to-pdf MarkdownToPDF.title=Markdown para PDF MarkdownToPDF.header=Markdown para PDF @@ -289,12 +349,15 @@ MarkdownToPDF.submit=Converter MarkdownToPDF.help=Trabalho em andamento MarkdownToPDF.credit=Usa o WeasyPrint + + #url-to-pdf URLToPDF.title=URL para PDF URLToPDF.header=URL para PDF URLToPDF.submit=Converter URLToPDF.credit=Usa o WeasyPrint + #html-to-pdf HTMLToPDF.title=HTML para PDF HTMLToPDF.header=HTML para PDF @@ -302,6 +365,7 @@ HTMLToPDF.help=Aceita arquivos HTML e ZIPs contendo html/css/imagens etc necess HTMLToPDF.submit=Converter HTMLToPDF.credit=Usa o WeasyPrint + #sanitizePDF sanitizePDF.title=Sanitizar PDF sanitizePDF.header=Sanitizar um arquivo PDF @@ -312,8 +376,8 @@ sanitizePDF.selectText.4=Remover links sanitizePDF.selectText.5=Remover fontes sanitizePDF.submit=Sanitizar PDF + #addPageNumbers -autoCrop.title=Adicionar Números de Página addPageNumbers.title=Adicionar Números de Página addPageNumbers.header=Adicionar Números de Página addPageNumbers.selectText.1=Selecionar arquivo PDF: @@ -324,11 +388,13 @@ addPageNumbers.selectText.5=Páginas a Numerar addPageNumbers.selectText.6=Texto Personalizado addPageNumbers.submit=Adicionar Números de Página + #auto-rename auto-rename.title=Rename Automático auto-rename.header=Rename Automático de PDF auto-rename.submit=Rename Automático + #adjustContrast adjustContrast.title=Ajustar Contraste adjustContrast.header=Ajustar Contraste @@ -337,11 +403,13 @@ adjustContrast.brightness=Brilho: adjustContrast.saturation=Saturação: adjustContrast.download=Download + #crop crop.title=Cortar crop.header=Cortar Imagem crop.submit=Enviar + #autoSplitPDF autoSplitPDF.title=Divisão Automática de PDF autoSplitPDF.header=Divisão Automática de PDF @@ -356,6 +424,7 @@ autoSplitPDF.dividerDownload1=Download 'Folha Divisória Automática (mínima).p autoSplitPDF.dividerDownload2=Download 'Folha Divisória Automática (com instruções).pdf' autoSplitPDF.submit=Enviar + #pipeline pipeline.title=Pipeline @@ -366,6 +435,7 @@ pageLayout.header=Layout de Múltiplas Páginas pageLayout.pagesPerSheet=Páginas por folha: pageLayout.submit=Enviar + #scalePages scalePages.title=Ajustar Tamanho/Escala da Página scalePages.header=Ajustar Tamanho/Escala da Página @@ -373,6 +443,7 @@ scalePages.pageSize=Tamanho de uma página do documento. scalePages.scaleFactor=Fator de zoom (corte) de uma página. scalePages.submit=Enviar + #certSign certSign.title=Assinatura com Certificado certSign.header=Assine um PDF com o seu certificado (Em desenvolvimento) @@ -388,6 +459,7 @@ certSign.location=Localização certSign.name=Nome certSign.submit=Assinar PDF + #removeBlanks removeBlanks.title=Remover Páginas em Branco removeBlanks.header=Remover Páginas em Branco @@ -397,6 +469,7 @@ removeBlanks.whitePercent=Porcentagem de Branco (%): removeBlanks.whitePercentDesc=Porcentagem da página que deve ser branca para ser removida removeBlanks.submit=Remover Páginas em Branco + #compare compare.title=Comparar compare.header=Comparar PDFs @@ -404,6 +477,7 @@ compare.document.1=Documento 1 compare.document.2=Documento 2 compare.submit=Comparar + #sign sign.title=Assinar sign.header=Assinar PDFs @@ -413,16 +487,19 @@ sign.text=Inserir Texto sign.clear=Limpar sign.add=Adicionar + #repair repair.title=Reparar repair.header=Reparar PDFs repair.submit=Reparar + #flatten flatten.title=Achatar flatten.header=Achatar PDFs flatten.submit=Achatar + #ScannerImageSplit ScannerImageSplit.selectText.1=Limite de Ângulo: ScannerImageSplit.selectText.2=Define o ângulo absoluto mínimo necessário para que a imagem seja girada (padrão: 10). @@ -450,16 +527,22 @@ ocr.selectText.8=Normal (gerará um erro se o PDF já contiver texto) ocr.selectText.9=Configurações adicionais ocr.selectText.10=Modo OCR ocr.selectText.11=Remover imagens após o OCR (remove TODAS as imagens, útil apenas como parte do processo de conversão) +########################## +### TODO: Translate ### +########################## +ocr.selectText.12=Render Type (Advanced) ocr.help=Por favor, leia a documentação sobre como usar isso para outros idiomas e/ou fora do ambiente Docker ocr.credit=Este serviço usa OCRmyPDF e Tesseract para OCR. ocr.submit=Processar PDF com OCR + #extractImages extractImages.title=Extrair Imagens extractImages.header=Extrair Imagens extractImages.selectText=Selecione o formato de imagem para converter as imagens extraídas extractImages.submit=Extrair + #File to PDF fileToPDF.title=Arquivo para PDF fileToPDF.header=Converter Qualquer Arquivo para PDF @@ -467,6 +550,7 @@ fileToPDF.credit=Este serviço usa o LibreOffice e o Unoconv para conversão de fileToPDF.supportedFileTypes=Os tipos de arquivo suportados devem incluir os listados abaixo. No entanto, para obter uma lista atualizada completa dos formatos suportados, consulte a documentação do LibreOffice. fileToPDF.submit=Converter para PDF + #compress compress.title=Comprimir compress.header=Comprimir PDF @@ -478,6 +562,7 @@ compress.selectText.4=Modo Automático - Ajusta automaticamente a qualidade para compress.selectText.5=Tamanho Esperado do PDF (por exemplo, 25 MB, 10,8 MB, 25 KB) compress.submit=Comprimir + #Add image addImage.title=Adicionar Imagem addImage.header=Adicionar Imagem ao PDF @@ -485,32 +570,43 @@ addImage.everyPage=Para cada página? addImage.upload=Enviar Imagem addImage.submit=Adicionar Imagem + #merge merge.title=Mesclar merge.header=Mesclar Vários PDFs (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Mesclar + #pdfOrganiser pdfOrganiser.title=Organizador de Páginas pdfOrganiser.header=Organizador de Páginas PDF pdfOrganiser.submit=Reorganizar Páginas + #multiTool multiTool.title=Multiferramenta de PDF multiTool.header=Multiferramenta de PDF + #pageRemover pageRemover.title=Remover Página pageRemover.header=Remover Páginas do PDF pageRemover.pagesToDelete=Páginas a serem excluídas (insira uma lista separada por vírgulas de números de página): pageRemover.submit=Excluir Páginas + #rotate rotate.title=Girar PDF rotate.header=Girar PDF rotate.selectAngle=Selecione o ângulo de rotação (múltiplos de 90 graus): rotate.submit=Girar + #merge split.title=Dividir PDF split.header=Dividir PDF @@ -525,6 +621,7 @@ split.desc.8=Documento Nº6: Páginas 9 e 10 split.splitPages=Digite as páginas para a divisão: split.submit=Dividir + #merge imageToPDF.title=Imagem para PDF imageToPDF.header=Converter Imagem para PDF @@ -534,7 +631,8 @@ imageToPDF.selectText.2=Girar Automaticamente imageToPDF.selectText.3=Lógica de Vários Arquivos (Ativada apenas ao trabalhar com várias imagens) imageToPDF.selectText.4=Mesclar em um Único PDF imageToPDF.selectText.5=Converter em PDFs Separados - + + #pdfToImage pdfToImage.title=PDF para Imagem pdfToImage.header=Converter PDF para Imagem @@ -570,6 +668,7 @@ addPassword.selectText.15=Restringe o que pode ser feito com o documento após a addPassword.selectText.16=Restringe a abertura do próprio documento addPassword.submit=Criptografar + #watermark watermark.title=Adicionar Marca d'Água watermark.header=Adicionar Marca d'Água @@ -584,12 +683,6 @@ watermark.selectText.8=Tipo de Marca d'Água watermark.selectText.9=Imagem da Marca d'Água watermark.submit=Adicionar Marca d'Água -#remove-watermark -remove-watermark.title=Remover Marca d'Água -remove-watermark.header=Remover Marca d'Água -remove-watermark.selectText.1=Selecione o PDF para Remover a Marca d'Água -remove-watermark.selectText.2=Texto da Marca d'Água -remove-watermark.submit=Remover Marca d'Água #Change permissions permissions.title=Alterar Permissões @@ -607,6 +700,7 @@ permissions.selectText.9=Impedir Impressão permissions.selectText.10=Impedir Impressão de Formatos Diferentes permissions.submit=Mudar + #remove password removePassword.title=Remover Senha removePassword.header=Remover Senha (Descriptografar) @@ -616,7 +710,7 @@ removePassword.submit=Remover #changeMetadata -changeMetadata.title=Alterar Metadados +changeMetadata.title=Título: changeMetadata.header=Alterar Metadados changeMetadata.selectText.1=Edite as Variáveis que Deseja Alterar changeMetadata.selectText.2=Excluir Todos os Metadados @@ -634,18 +728,21 @@ changeMetadata.selectText.4=Outros Metadados changeMetadata.selectText.5=Adicionar Entrada de Metadados Personalizados changeMetadata.submit=Mudar + #xlsToPdf xlsToPdf.title=Excel para PDF xlsToPdf.header=Excel para PDF xlsToPdf.selectText.1=Selecione a Planilha Excel XLS ou XLSX para Converter xlsToPdf.convert=Converter + #pdfToPDFA pdfToPDFA.title=PDF para PDF/A pdfToPDFA.header=PDF para PDF/A pdfToPDFA.credit=Este serviço usa OCRmyPDF para Conversão de PDF/A pdfToPDFA.submit=Converter + #PDFToWord PDFToWord.title=PDF para Word PDFToWord.header=PDF para Word @@ -653,6 +750,7 @@ PDFToWord.selectText.1=Formato do Arquivo de Saída PDFToWord.credit=Este serviço usa o LibreOffice para Conversão de Arquivos. PDFToWord.submit=Converter + #PDFToPresentation PDFToPresentation.title=PDF para Apresentação PDFToPresentation.header=PDF para Apresentação @@ -660,6 +758,7 @@ PDFToPresentation.selectText.1=Formato do Arquivo de Saída PDFToPresentation.credit=Este serviço usa o LibreOffice para Conversão de Arquivos. PDFToPresentation.submit=Converter + #PDFToText PDFToText.title=PDF para Texto/RTF PDFToText.header=PDF para Texto/RTF @@ -667,12 +766,14 @@ PDFToText.selectText.1=Formato do Arquivo de Saída PDFToText.credit=Este serviço usa o LibreOffice para Conversão de Arquivos. PDFToText.submit=Converter + #PDFToHTML PDFToHTML.title=PDF para HTML PDFToHTML.header=PDF para HTML PDFToHTML.credit=Este serviço usa o LibreOffice para Conversão de Arquivos. PDFToHTML.submit=Converter + #PDFToXML PDFToXML.title=PDF para XML PDFToXML.header=PDF para XML diff --git a/src/main/resources/messages_ro_RO.properties b/src/main/resources/messages_ro_RO.properties index 11a5c090..f03af619 100644 --- a/src/main/resources/messages_ro_RO.properties +++ b/src/main/resources/messages_ro_RO.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Deschide în aceeași fereastră settings.downloadOption.2=Deschide într-o fereastră nouă settings.downloadOption.3=Descarcă fișierul settings.zipThreshold=Împachetează fișierele când numărul de fișiere descărcate depășește +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Adăugare imagine #merge merge.title=Unire merge.header=Unirea mai multor PDF-uri (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Unire @@ -626,17 +676,14 @@ watermark.selectText.4=Rotire (0-360): watermark.selectText.5=widthSpacer (Spațiu între fiecare filigran pe orizontală): watermark.selectText.6=heightSpacer (Spațiu între fiecare filigran pe verticală): watermark.selectText.7=Opacitate (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Adăugați Filigran -#remove-watermark -remove-watermark.title=Eliminați Filigran -remove-watermark.header=Eliminați Filigran -remove-watermark.selectText.1=Selectați PDF-ul de la care să eliminați filigranul: -remove-watermark.selectText.2=Textul Filigranului: -remove-watermark.submit=Eliminați Filigran - - #Change permissions permissions.title=Schimbați Permisiunile permissions.header=Schimbați Permisiunile diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index 89be5d64..425b2a0a 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Открыть в том же окне settings.downloadOption.2=Открыть в новом окне settings.downloadOption.3=Загрузить файл settings.zipThreshold=Zip-файлы, когда количество загруженных файлов превышает +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Добавить изображение #merge merge.title=Объединить merge.header=Объединение нескольких PDF-файлов (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Объединить @@ -626,17 +676,14 @@ watermark.selectText.4=Поворот (0-360): watermark.selectText.5=widthSpacer (пробел между каждым водяным знаком по горизонтали): watermark.selectText.6=heightSpacer (пробел между каждым водяным знаком по вертикали): watermark.selectText.7=Непрозрачность (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Добавить водяной знак -#remove-watermark -remove-watermark.title=Удалить водяной знак -remove-watermark.header=Удалить водяной знак -remove-watermark.selectText.1=Выберите PDF, чтобы удалить водяной знак из: -remove-watermark.selectText.2=Текст водяного знака: -remove-watermark.submit=Удалить водяной знак - - #Change permissions permissions.title=Изменить разрешения permissions.header=Изменить разрешения diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index 06637e86..847f94a6 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=Öppnas i samma fönster settings.downloadOption.2=Öppna i nytt fönster settings.downloadOption.3=Ladda ner fil settings.zipThreshold=Zip-filer när antalet nedladdade filer överskrider +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=Lägg till bild #merge merge.title=Sammanfoga merge.header=Slå samman flera PDF-filer (2+) +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=Slå samman @@ -626,17 +676,14 @@ watermark.selectText.4=Rotation (0-360): watermark.selectText.5=widthSpacer (mellanrum mellan varje vattenstämpel horisontellt): watermark.selectText.6=heightSpacer (mellanrum mellan varje vattenstämpel vertikalt): watermark.selectText.7=Opacitet (0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=Lägg till vattenstämpel -#remove-watermark -remove-watermark.title=Ta bort vattenstämpel -remove-watermark.header=Ta bort vattenstämpel -remove-watermark.selectText.1=Välj PDF för att ta bort vattenstämpel från: -remove-watermark.selectText.2=Vattenstämpeltext: -remove-watermark.submit=Ta bort vattenstämpel - - #Change permissions permissions.title=Ändra behörigheter permissions.header=Ändra behörigheter diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index 7ece8479..82c047ea 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -31,7 +31,14 @@ sizes.medium=Medium sizes.large=Large sizes.x-large=X-Large error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect - +########################## +### TODO: Translate ### +########################## +delete=Delete +username=Username +password=Password +welcome=Welcome +=Property ############# # NAVBAR # @@ -54,6 +61,50 @@ settings.downloadOption.1=在同一窗口打开 settings.downloadOption.2=在新窗口中打开 settings.downloadOption.3=下载文件 settings.zipThreshold=当下载的文件数量超过限制时,将文件压缩。 +########################## +### TODO: Translate ### +########################## +settings.signOut=Sign Out +settings.accountSettings=Account Settings + +########################## +### TODO: Translate ### +########################## +account.title=Account Settings +account.accountSettings=Account Settings +account.adminSettings=Admin Settings - View and Add Users +account.userControlSettings=User Control Settings +account.changeUsername=New Username +account.changeUsername=Change Username +account.password=Confirmation Password +account.oldPassword=Old password +account.newPassword=New Password +account.changePassword=Change Password +account.confirmNewPassword=Confirm New Password +account.signOut=Sign Out +account.yourApiKey=Your API Key +account.syncTitle=Sync browser settings with Account +account.settingsCompare=Settings Comparison: +account.property=Property +account.webBrowserSettings=Web Browser Setting +account.syncToBrowser=Sync Account -> Browser +account.syncToAccount=Sync Account <- Browser + + +########################## +### TODO: Translate ### +########################## +adminUserSettings.title=User Control Settings +adminUserSettings.header=Admin User Control Settings +adminUserSettings.admin=Admin +adminUserSettings.user=User +adminUserSettings.addUser=Add New User +adminUserSettings.roles=Roles +adminUserSettings.role=Role +adminUserSettings.actions=Actions +adminUserSettings.apiUser=Limited API User +adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.submit=Save User ############# # HOME-PAGE # @@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page PdfToSinglePage.tags=single page -########################## -### TODO: Translate ### -########################## home.showJS.title=Show Javascript home.showJS.desc=Searches and displays any JS injected into a PDF showJS.tags=JS @@ -269,9 +317,6 @@ showJS.tags=JS # # ########################### #showJS -########################## -### TODO: Translate ### -########################## showJS.title=Show Javascript showJS.header=Show Javascript showJS.downloadJS=Download Javascript @@ -526,6 +571,11 @@ addImage.submit=添加图片 #merge merge.title=合并 merge.header=合并多个PDF(2个以上)。 +########################## +### TODO: Translate ### +########################## +merge.sortByName=Sort by name +merge.sortByDate=Sort by date merge.submit=合并 @@ -626,17 +676,14 @@ watermark.selectText.4=旋转(0-360): watermark.selectText.5=widthSpacer(水平方向上每个水印之间的空间): watermark.selectText.6=heightSpacer(每个水印之间的垂直空间): watermark.selectText.7=透明度(0% - 100%): +########################## +### TODO: Translate ### +########################## +watermark.selectText.8=Watermark Type: +watermark.selectText.9=Watermark Image: watermark.submit=添加水印 -#remove-watermark -remove-watermark.title=去除水印 -remove-watermark.header=去除水印 -remove-watermark.selectText.1=选择要去除水印的PDF: -remove-watermark.selectText.2=水印文本: -remove-watermark.submit=移除水印 - - #Change permissions permissions.title=更改权限 permissions.header=改变权限 diff --git a/src/main/resources/static/images/clipboard.svg b/src/main/resources/static/images/clipboard.svg new file mode 100644 index 00000000..360e0894 --- /dev/null +++ b/src/main/resources/static/images/clipboard.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/static/images/eye-slash.svg b/src/main/resources/static/images/eye-slash.svg new file mode 100644 index 00000000..c5208375 --- /dev/null +++ b/src/main/resources/static/images/eye-slash.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/static/images/eye.svg b/src/main/resources/static/images/eye.svg new file mode 100644 index 00000000..412ff692 --- /dev/null +++ b/src/main/resources/static/images/eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/static/js/merge.js b/src/main/resources/static/js/merge.js index 523be4a8..d27730b9 100644 --- a/src/main/resources/static/js/merge.js +++ b/src/main/resources/static/js/merge.js @@ -1,63 +1,113 @@ +let currentSort = { + field: null, + descending: false +}; + document.getElementById("fileInput-input").addEventListener("change", function() { - var files = this.files; - var list = document.getElementById("selectedFiles"); - list.innerHTML = ""; - for (var i = 0; i < files.length; i++) { - var item = document.createElement("li"); - item.className = "list-group-item"; - item.innerHTML = ` -
-
${files[i].name}
-
- - -
-
- `; - list.appendChild(item); - } + var files = this.files; + displayFiles(files); +}); - var moveUpButtons = document.querySelectorAll(".move-up"); - for (var i = 0; i < moveUpButtons.length; i++) { - moveUpButtons[i].addEventListener("click", function(event) { - event.preventDefault(); - var parent = this.closest(".list-group-item"); - var grandParent = parent.parentNode; - if (parent.previousElementSibling) { - grandParent.insertBefore(parent, parent.previousElementSibling); - updateFiles(); - } - }); - } +function displayFiles(files) { + var list = document.getElementById("selectedFiles"); + list.innerHTML = ""; - var moveDownButtons = document.querySelectorAll(".move-down"); - for (var i = 0; i < moveDownButtons.length; i++) { - moveDownButtons[i].addEventListener("click", function(event) { - event.preventDefault(); - var parent = this.closest(".list-group-item"); - var grandParent = parent.parentNode; - if (parent.nextElementSibling) { - grandParent.insertBefore(parent.nextElementSibling, parent); - updateFiles(); - } - }); - } + for (var i = 0; i < files.length; i++) { + var item = document.createElement("li"); + item.className = "list-group-item"; + item.innerHTML = ` +
+
${files[i].name}
+
+ + +
+
+ `; + list.appendChild(item); + } - function updateFiles() { - var dataTransfer = new DataTransfer(); - var liElements = document.querySelectorAll("#selectedFiles li"); + attachMoveButtons(); +} - for (var i = 0; i < liElements.length; i++) { - var fileNameFromList = liElements[i].querySelector(".filename").innerText; - var fileFromFiles; - for (var j = 0; j < files.length; j++) { - var file = files[j]; - if (file.name === fileNameFromList) { - dataTransfer.items.add(file); - break; - } - } - } - document.getElementById("fileInput-input").files = dataTransfer.files; - } -}); \ No newline at end of file +function attachMoveButtons() { + var moveUpButtons = document.querySelectorAll(".move-up"); + for (var i = 0; i < moveUpButtons.length; i++) { + moveUpButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.previousElementSibling) { + grandParent.insertBefore(parent, parent.previousElementSibling); + updateFiles(); + } + }); + } + + var moveDownButtons = document.querySelectorAll(".move-down"); + for (var i = 0; i < moveDownButtons.length; i++) { + moveDownButtons[i].addEventListener("click", function(event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + var grandParent = parent.parentNode; + if (parent.nextElementSibling) { + grandParent.insertBefore(parent.nextElementSibling, parent); + updateFiles(); + } + }); + } +} + +document.getElementById("sortByNameBtn").addEventListener("click", function() { + if (currentSort.field === "name" && !currentSort.descending) { + currentSort.descending = true; + sortFiles((a, b) => b.name.localeCompare(a.name)); + } else { + currentSort.field = "name"; + currentSort.descending = false; + sortFiles((a, b) => a.name.localeCompare(b.name)); + } +}); + +document.getElementById("sortByDateBtn").addEventListener("click", function() { + if (currentSort.field === "lastModified" && !currentSort.descending) { + currentSort.descending = true; + sortFiles((a, b) => b.lastModified - a.lastModified); + } else { + currentSort.field = "lastModified"; + currentSort.descending = false; + sortFiles((a, b) => a.lastModified - b.lastModified); + } +}); + +function sortFiles(comparator) { + // Convert FileList to array and sort + const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator); + + // Refresh displayed list + displayFiles(sortedFilesArray); + + // Update the files property + const dataTransfer = new DataTransfer(); + sortedFilesArray.forEach(file => dataTransfer.items.add(file)); + document.getElementById("fileInput-input").files = dataTransfer.files; +} + +function updateFiles() { + var dataTransfer = new DataTransfer(); + var liElements = document.querySelectorAll("#selectedFiles li"); + const files = document.getElementById("fileInput-input").files; + + for (var i = 0; i < liElements.length; i++) { + var fileNameFromList = liElements[i].querySelector(".filename").innerText; + var fileFromFiles; + for (var j = 0; j < files.length; j++) { + var file = files[j]; + if (file.name === fileNameFromList) { + dataTransfer.items.add(file); + break; + } + } + } + document.getElementById("fileInput-input").files = dataTransfer.files; +} diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html new file mode 100644 index 00000000..9d7c0ef6 --- /dev/null +++ b/src/main/resources/templates/account.html @@ -0,0 +1,295 @@ + + + + + + + +
+
+
+

+
+
+
+ + +

User Settings

+
+ + +

User!

+ + + +

+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +

Change Password?

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ + + + + +
+
+
+
+ + + + +
+ +

Sync browser settings with Account

+
+

Settings Comparison:

+ + + + + + + + + + + +
PropertyAccount SettingWeb Browser Setting
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + diff --git a/src/main/resources/templates/addUsers.html b/src/main/resources/templates/addUsers.html new file mode 100644 index 00000000..fe37e71c --- /dev/null +++ b/src/main/resources/templates/addUsers.html @@ -0,0 +1,73 @@ + + + + + + + +
+
+
+

+
+
+
+ + +

Admin User Control Settings

+ + + + + + + + + + + + + + + + + + + +
UsernameRolesActions
+ Delete +
+ + +

Add New User

+
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
+
+ +
+
+
+ + diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 89215fcb..3c43dfba 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -341,9 +341,18 @@ - + + + + + + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 00000000..5c23214e --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,59 @@ + + + + + Login + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ + + diff --git a/src/main/resources/templates/merge-pdfs.html b/src/main/resources/templates/merge-pdfs.html index 0574f3ad..72be72e7 100644 --- a/src/main/resources/templates/merge-pdfs.html +++ b/src/main/resources/templates/merge-pdfs.html @@ -23,6 +23,8 @@
    + +