Merge branch 'V2' into settingsPageEnhanced

Resolved conflicts by:
- Kept invite link feature (inviteLinkExpiryHours in ApplicationProperties)
- Kept enhanced email invite validation (both SMTP and invites enabled check)
- Adopted V2's Posthog/Scarf tracking additions
- Adopted V2's password length validation (min 6 chars) on both backend and frontend
- Converted email templates to Java text blocks
- Kept success message functionality in Login flow
- Used @app path aliases consistently across frontend files
- Fixed remaining relative imports in Workbench, RestartConfirmationModal, useRestartServer, and InviteAccept
- Accepted deletion of refactored files (App.tsx, Landing.tsx, etc.)
- Kept InviteAccept.tsx for invite link feature
This commit is contained in:
Anthony Stirling
2025-10-28 20:45:29 +00:00
769 changed files with 4650 additions and 3220 deletions

View File

@@ -355,6 +355,8 @@ public class ApplicationProperties {
private String tessdataDir;
private Boolean enableAlphaFunctionality;
private Boolean enableAnalytics;
private Boolean enablePosthog;
private Boolean enableScarf;
private Datasource datasource;
private Boolean disableSanitize;
private int maxDPI;
@@ -372,6 +374,18 @@ public class ApplicationProperties {
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
}
public boolean isPosthogEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
}
public boolean isScarfEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnableScarf() == null || this.getEnableScarf());
}
}
@Data

View File

@@ -56,7 +56,7 @@ public class PostHogService {
}
private void captureSystemInfo() {
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
if (!applicationProperties.getSystem().isPosthogEnabled()) {
return;
}
try {
@@ -67,7 +67,7 @@ public class PostHogService {
}
public void captureEvent(String eventName, Map<String, Object> properties) {
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
if (!applicationProperties.getSystem().isPosthogEnabled()) {
return;
}
@@ -325,6 +325,14 @@ public class PostHogService {
properties,
"system_enableAnalytics",
applicationProperties.getSystem().isAnalyticsEnabled());
addIfNotEmpty(
properties,
"system_enablePosthog",
applicationProperties.getSystem().isPosthogEnabled());
addIfNotEmpty(
properties,
"system_enableScarf",
applicationProperties.getSystem().isScarfEnabled());
// Capture UI properties
addIfNotEmpty(

View File

@@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
@@ -31,7 +32,7 @@ public class SettingsController {
@AutoJobPostMapping("/update-enable-analytics")
@Hidden
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body(

View File

@@ -84,6 +84,8 @@ public class ConfigController {
applicationProperties.getSystem().getEnableAlphaFunctionality());
configData.put(
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
configData.put("enablePosthog", applicationProperties.getSystem().getEnablePosthog());
configData.put("enableScarf", applicationProperties.getSystem().getEnableScarf());
// Premium/Enterprise settings
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());

View File

@@ -107,8 +107,8 @@ mail:
from: '' # sender email address
legal:
termsAndConditions: https://www.stirlingpdf.com/terms # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirling.com/legal/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
@@ -121,7 +121,9 @@ system:
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
enableAnalytics: null # Master toggle for analytics: set to 'true' to enable all analytics, 'false' to disable all analytics, or leave as 'null' to prompt admin on first launch
enablePosthog: null # Enable PostHog analytics (open-source product analytics): set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
enableScarf: null # Enable Scarf tracking pixel: set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion

View File

@@ -366,6 +366,10 @@ public class UserController {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Password is required."));
}
if (password.length() < 6) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Password must be at least 6 characters."));
}
userService.saveUser(username, password, effectiveTeamId, role, forceChange);
}
return ResponseEntity.ok(Map.of("message", "User created successfully"));
@@ -458,41 +462,12 @@ public class UserController {
continue;
}
try {
// Validate email format (basic check)
if (!email.contains("@") || !email.contains(".")) {
errors.append(email).append(": Invalid email format; ");
failureCount++;
continue;
}
// Check if user already exists
if (userService.usernameExistsIgnoreCase(email)) {
errors.append(email).append(": User already exists; ");
failureCount++;
continue;
}
// Generate random password
String temporaryPassword = java.util.UUID.randomUUID().toString().substring(0, 12);
// Create user with forceChange=true
userService.saveUser(email, temporaryPassword, effectiveTeamId, role, true);
// Send invite email
try {
emailService.get().sendInviteEmail(email, email, temporaryPassword);
successCount++;
log.info("Sent invite email to: {}", email);
} catch (Exception emailEx) {
log.error("Failed to send invite email to {}: {}", email, emailEx.getMessage());
errors.append(email).append(": User created but email failed to send; ");
}
} catch (Exception e) {
log.error("Failed to invite user {}: {}", email, e.getMessage());
errors.append(email).append(": ").append(e.getMessage()).append("; ");
InviteResult result = processEmailInvite(email, effectiveTeamId, role);
if (result.isSuccess()) {
successCount++;
} else {
failureCount++;
errors.append(result.getErrorMessage()).append("; ");
}
}
@@ -685,4 +660,73 @@ public class UserController {
}
return ResponseEntity.ok(apiKey);
}
/**
* Helper method to process a single email invitation.
*
* @param email The email address to invite
* @param teamId The team ID to assign the user to
* @param role The role to assign to the user
* @return InviteResult containing success status and optional error message
*/
private InviteResult processEmailInvite(String email, Long teamId, String role) {
try {
// Validate email format (basic check)
if (!email.contains("@") || !email.contains(".")) {
return InviteResult.failure(email + ": Invalid email format");
}
// Check if user already exists
if (userService.usernameExistsIgnoreCase(email)) {
return InviteResult.failure(email + ": User already exists");
}
// Generate random password
String temporaryPassword = java.util.UUID.randomUUID().toString().substring(0, 12);
// Create user with forceChange=true
userService.saveUser(email, temporaryPassword, teamId, role, true);
// Send invite email
try {
emailService.get().sendInviteEmail(email, email, temporaryPassword);
log.info("Sent invite email to: {}", email);
return InviteResult.success();
} catch (Exception emailEx) {
log.error("Failed to send invite email to {}: {}", email, emailEx.getMessage());
return InviteResult.failure(email + ": User created but email failed to send");
}
} catch (Exception e) {
log.error("Failed to invite user {}: {}", email, e.getMessage());
return InviteResult.failure(email + ": " + e.getMessage());
}
}
/** Result object for individual email invite processing. */
private static class InviteResult {
private final boolean success;
private final String errorMessage;
private InviteResult(boolean success, String errorMessage) {
this.success = success;
this.errorMessage = errorMessage;
}
static InviteResult success() {
return new InviteResult(true, null);
}
static InviteResult failure(String errorMessage) {
return new InviteResult(false, errorMessage);
}
boolean isSuccess() {
return success;
}
String getErrorMessage() {
return errorMessage;
}
}
}

View File

@@ -123,38 +123,39 @@ public class EmailService {
String subject = "Welcome to Stirling PDF";
String body =
String.format(
"<html><body style=\"margin: 0; padding: 0;\">"
+ "<div style=\"font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;\">"
+ " <div style=\"max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;\">"
+ " <!-- Logo -->"
+ " <div style=\"text-align: center; padding: 20px; background-color: #222;\">"
+ " <img src=\"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg\" alt=\"Stirling PDF\" style=\"max-height: 60px;\">"
+ " </div>"
+ " <!-- Content -->"
+ " <div style=\"padding: 30px; color: #333;\">"
+ " <h2 style=\"color: #222; margin-top: 0;\">Welcome to Stirling PDF!</h2>"
+ " <p>Hi there,</p>"
+ " <p>You have been invited to join the workspace. Below are your login credentials:</p>"
+ " <!-- Credentials Box -->"
+ " <div style=\"background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; border-radius: 4px;\">"
+ " <p style=\"margin: 0 0 10px 0;\"><strong>Username:</strong> %s</p>"
+ " <p style=\"margin: 0;\"><strong>Temporary Password:</strong> %s</p>"
+ " </div>"
+ " <div style=\"background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;\">"
+ " <p style=\"margin: 0; color: #856404;\"><strong>⚠️ Important:</strong> You will be required to change your password upon first login for security reasons.</p>"
+ " </div>"
+ " <p>Please keep these credentials secure and do not share them with anyone.</p>"
+ " <p style=\"margin-bottom: 0;\">— The Stirling PDF Team</p>"
+ " </div>"
+ " <!-- Footer -->"
+ " <div style=\"text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;\">"
+ " &copy; 2025 Stirling PDF. All rights reserved."
+ " </div>"
+ " </div>"
+ "</div>"
+ "</body></html>",
username, temporaryPassword);
"""
<html><body style="margin: 0; padding: 0;">
<div style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;">
<div style="max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;">
<!-- Logo -->
<div style="text-align: center; padding: 20px; background-color: #222;">
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg" alt="Stirling PDF" style="max-height: 60px;">
</div>
<!-- Content -->
<div style="padding: 30px; color: #333;">
<h2 style="color: #222; margin-top: 0;">Welcome to Stirling PDF!</h2>
<p>Hi there,</p>
<p>You have been invited to join the workspace. Below are your login credentials:</p>
<!-- Credentials Box -->
<div style="background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0 0 10px 0;"><strong>Username:</strong> %s</p>
<p style="margin: 0;"><strong>Temporary Password:</strong> %s</p>
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0; color: #856404;"><strong>⚠️ Important:</strong> You will be required to change your password upon first login for security reasons.</p>
</div>
<p>Please keep these credentials secure and do not share them with anyone.</p>
<p style="margin-bottom: 0;">— The Stirling PDF Team</p>
</div>
<!-- Footer -->
<div style="text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;">
&copy; 2025 Stirling PDF. All rights reserved.
</div>
</div>
</div>
</body></html>
"""
.formatted(username, temporaryPassword);
sendPlainEmail(to, subject, body, true);
}
@@ -173,41 +174,42 @@ public class EmailService {
String subject = "You've been invited to Stirling PDF";
String body =
String.format(
"<html><body style=\"margin: 0; padding: 0;\">"
+ "<div style=\"font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;\">"
+ " <div style=\"max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;\">"
+ " <!-- Logo -->"
+ " <div style=\"text-align: center; padding: 20px; background-color: #222;\">"
+ " <img src=\"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg\" alt=\"Stirling PDF\" style=\"max-height: 60px;\">"
+ " </div>"
+ " <!-- Content -->"
+ " <div style=\"padding: 30px; color: #333;\">"
+ " <h2 style=\"color: #222; margin-top: 0;\">Welcome to Stirling PDF!</h2>"
+ " <p>Hi there,</p>"
+ " <p>You have been invited to join the Stirling PDF workspace. Click the button below to set up your account:</p>"
+ " <!-- CTA Button -->"
+ " <div style=\"text-align: center; margin: 30px 0;\">"
+ " <a href=\"%s\" style=\"display: inline-block; background-color: #007bff; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 5px; font-weight: bold;\">Accept Invitation</a>"
+ " </div>"
+ " <p style=\"font-size: 14px; color: #666;\">Or copy and paste this link in your browser:</p>"
+ " <div style=\"background-color: #f8f9fa; padding: 12px; margin: 15px 0; border-radius: 4px; word-break: break-all; font-size: 13px; color: #555;\">"
+ " %s"
+ " </div>"
+ " <div style=\"background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;\">"
+ " <p style=\"margin: 0; color: #856404; font-size: 14px;\"><strong>⚠️ Important:</strong> This invitation link will expire on %s. Please complete your registration before then.</p>"
+ " </div>"
+ " <p>If you didn't expect this invitation, you can safely ignore this email.</p>"
+ " <p style=\"margin-bottom: 0;\">— The Stirling PDF Team</p>"
+ " </div>"
+ " <!-- Footer -->"
+ " <div style=\"text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;\">"
+ " &copy; 2025 Stirling PDF. All rights reserved."
+ " </div>"
+ " </div>"
+ "</div>"
+ "</body></html>",
inviteUrl, inviteUrl, expiresAt);
"""
<html><body style="margin: 0; padding: 0;">
<div style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;">
<div style="max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;">
<!-- Logo -->
<div style="text-align: center; padding: 20px; background-color: #222;">
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg" alt="Stirling PDF" style="max-height: 60px;">
</div>
<!-- Content -->
<div style="padding: 30px; color: #333;">
<h2 style="color: #222; margin-top: 0;">Welcome to Stirling PDF!</h2>
<p>Hi there,</p>
<p>You have been invited to join the Stirling PDF workspace. Click the button below to set up your account:</p>
<!-- CTA Button -->
<div style="text-align: center; margin: 30px 0;">
<a href="%s" style="display: inline-block; background-color: #007bff; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 5px; font-weight: bold;">Accept Invitation</a>
</div>
<p style="font-size: 14px; color: #666;">Or copy and paste this link in your browser:</p>
<div style="background-color: #f8f9fa; padding: 12px; margin: 15px 0; border-radius: 4px; word-break: break-all; font-size: 13px; color: #555;">
%s
</div>
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0; color: #856404; font-size: 14px;"><strong>⚠️ Important:</strong> This invitation link will expire on %s. Please complete your registration before then.</p>
</div>
<p>If you didn't expect this invitation, you can safely ignore this email.</p>
<p style="margin-bottom: 0;">— The Stirling PDF Team</p>
</div>
<!-- Footer -->
<div style="text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;">
&copy; 2025 Stirling PDF. All rights reserved.
</div>
</div>
</div>
</body></html>
"""
.formatted(inviteUrl, inviteUrl, expiresAt);
sendPlainEmail(to, subject, body, true);
}