mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Fix email invite/ allow non auth and table refresh issues (#5076)
# Description of Changes - Show warning when email invite fails but user is created - Auto-refresh user/team tables after modifications - Fix invite email URLs to use frontend URL instead of backend - Support anonymous SMTP for local development --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
+29
-4
@@ -35,15 +35,40 @@ public class MailConfig {
|
|||||||
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
|
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
|
||||||
mailSender.setHost(mailProperties.getHost());
|
mailSender.setHost(mailProperties.getHost());
|
||||||
mailSender.setPort(mailProperties.getPort());
|
mailSender.setPort(mailProperties.getPort());
|
||||||
mailSender.setUsername(mailProperties.getUsername());
|
|
||||||
mailSender.setPassword(mailProperties.getPassword());
|
|
||||||
mailSender.setDefaultEncoding("UTF-8");
|
mailSender.setDefaultEncoding("UTF-8");
|
||||||
|
|
||||||
|
// Only set username and password if they are provided
|
||||||
|
String username = mailProperties.getUsername();
|
||||||
|
String password = mailProperties.getPassword();
|
||||||
|
boolean hasCredentials =
|
||||||
|
(username != null && !username.trim().isEmpty())
|
||||||
|
|| (password != null && !password.trim().isEmpty());
|
||||||
|
|
||||||
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
mailSender.setUsername(username);
|
||||||
|
log.info("SMTP username configured");
|
||||||
|
} else {
|
||||||
|
log.info("SMTP username not configured - using anonymous connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
mailSender.setPassword(password);
|
||||||
|
log.info("SMTP password configured");
|
||||||
|
} else {
|
||||||
|
log.info("SMTP password not configured");
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieves the JavaMail properties to configure additional SMTP parameters
|
// Retrieves the JavaMail properties to configure additional SMTP parameters
|
||||||
Properties props = mailSender.getJavaMailProperties();
|
Properties props = mailSender.getJavaMailProperties();
|
||||||
|
|
||||||
// Enables SMTP authentication
|
// Only enable SMTP authentication if credentials are provided
|
||||||
props.put("mail.smtp.auth", "true");
|
if (hasCredentials) {
|
||||||
|
props.put("mail.smtp.auth", "true");
|
||||||
|
log.info("SMTP authentication enabled");
|
||||||
|
} else {
|
||||||
|
props.put("mail.smtp.auth", "false");
|
||||||
|
log.info("SMTP authentication disabled - no credentials provided");
|
||||||
|
}
|
||||||
|
|
||||||
// Enables STARTTLS to encrypt the connection if supported by the SMTP server
|
// Enables STARTTLS to encrypt the connection if supported by the SMTP server
|
||||||
props.put("mail.smtp.starttls.enable", "true");
|
props.put("mail.smtp.starttls.enable", "true");
|
||||||
|
|||||||
+38
-4
@@ -407,7 +407,8 @@ public class UserController {
|
|||||||
public ResponseEntity<?> inviteUsers(
|
public ResponseEntity<?> inviteUsers(
|
||||||
@RequestParam(name = "emails", required = true) String emails,
|
@RequestParam(name = "emails", required = true) String emails,
|
||||||
@RequestParam(name = "role", defaultValue = "ROLE_USER") String role,
|
@RequestParam(name = "role", defaultValue = "ROLE_USER") String role,
|
||||||
@RequestParam(name = "teamId", required = false) Long teamId)
|
@RequestParam(name = "teamId", required = false) Long teamId,
|
||||||
|
HttpServletRequest request)
|
||||||
throws SQLException, UnsupportedProviderException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
|
|
||||||
// Check if email invites are enabled
|
// Check if email invites are enabled
|
||||||
@@ -477,6 +478,9 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build login URL
|
||||||
|
String loginUrl = buildLoginUrl(request);
|
||||||
|
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
int failureCount = 0;
|
int failureCount = 0;
|
||||||
StringBuilder errors = new StringBuilder();
|
StringBuilder errors = new StringBuilder();
|
||||||
@@ -488,7 +492,7 @@ public class UserController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
InviteResult result = processEmailInvite(email, effectiveTeamId, role);
|
InviteResult result = processEmailInvite(email, effectiveTeamId, role, loginUrl);
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
successCount++;
|
successCount++;
|
||||||
} else {
|
} else {
|
||||||
@@ -687,15 +691,45 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to build the login URL from the application configuration or request.
|
||||||
|
*
|
||||||
|
* @param request The HTTP request
|
||||||
|
* @return The login URL
|
||||||
|
*/
|
||||||
|
private String buildLoginUrl(HttpServletRequest request) {
|
||||||
|
String baseUrl;
|
||||||
|
String configuredFrontendUrl = applicationProperties.getSystem().getFrontendUrl();
|
||||||
|
if (configuredFrontendUrl != null && !configuredFrontendUrl.trim().isEmpty()) {
|
||||||
|
// Use configured frontend URL (remove trailing slash if present)
|
||||||
|
baseUrl =
|
||||||
|
configuredFrontendUrl.endsWith("/")
|
||||||
|
? configuredFrontendUrl.substring(0, configuredFrontendUrl.length() - 1)
|
||||||
|
: configuredFrontendUrl;
|
||||||
|
} else {
|
||||||
|
// Fall back to backend URL from request
|
||||||
|
baseUrl =
|
||||||
|
request.getScheme()
|
||||||
|
+ "://"
|
||||||
|
+ request.getServerName()
|
||||||
|
+ (request.getServerPort() != 80 && request.getServerPort() != 443
|
||||||
|
? ":" + request.getServerPort()
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
return baseUrl + "/login";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to process a single email invitation.
|
* Helper method to process a single email invitation.
|
||||||
*
|
*
|
||||||
* @param email The email address to invite
|
* @param email The email address to invite
|
||||||
* @param teamId The team ID to assign the user to
|
* @param teamId The team ID to assign the user to
|
||||||
* @param role The role to assign to the user
|
* @param role The role to assign to the user
|
||||||
|
* @param loginUrl The URL to the login page
|
||||||
* @return InviteResult containing success status and optional error message
|
* @return InviteResult containing success status and optional error message
|
||||||
*/
|
*/
|
||||||
private InviteResult processEmailInvite(String email, Long teamId, String role) {
|
private InviteResult processEmailInvite(
|
||||||
|
String email, Long teamId, String role, String loginUrl) {
|
||||||
try {
|
try {
|
||||||
// Validate email format (basic check)
|
// Validate email format (basic check)
|
||||||
if (!email.contains("@") || !email.contains(".")) {
|
if (!email.contains("@") || !email.contains(".")) {
|
||||||
@@ -715,7 +749,7 @@ public class UserController {
|
|||||||
|
|
||||||
// Send invite email
|
// Send invite email
|
||||||
try {
|
try {
|
||||||
emailService.get().sendInviteEmail(email, email, temporaryPassword);
|
emailService.get().sendInviteEmail(email, email, temporaryPassword, loginUrl);
|
||||||
log.info("Sent invite email to: {}", email);
|
log.info("Sent invite email to: {}", email);
|
||||||
return InviteResult.success();
|
return InviteResult.success();
|
||||||
} catch (Exception emailEx) {
|
} catch (Exception emailEx) {
|
||||||
|
|||||||
+12
-2
@@ -115,10 +115,12 @@ public class EmailService {
|
|||||||
* @param to The recipient email address
|
* @param to The recipient email address
|
||||||
* @param username The username for the new account
|
* @param username The username for the new account
|
||||||
* @param temporaryPassword The temporary password
|
* @param temporaryPassword The temporary password
|
||||||
|
* @param loginUrl The URL to the login page
|
||||||
* @throws MessagingException If there is an issue with creating or sending the email.
|
* @throws MessagingException If there is an issue with creating or sending the email.
|
||||||
*/
|
*/
|
||||||
@Async
|
@Async
|
||||||
public void sendInviteEmail(String to, String username, String temporaryPassword)
|
public void sendInviteEmail(
|
||||||
|
String to, String username, String temporaryPassword, String loginUrl)
|
||||||
throws MessagingException {
|
throws MessagingException {
|
||||||
String subject = "Welcome to Stirling PDF";
|
String subject = "Welcome to Stirling PDF";
|
||||||
|
|
||||||
@@ -144,6 +146,14 @@ public class EmailService {
|
|||||||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 4px;">
|
<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>
|
<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>
|
</div>
|
||||||
|
<!-- 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;">Log In to Stirling PDF</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>
|
||||||
<p>Please keep these credentials secure and do not share them with anyone.</p>
|
<p>Please keep these credentials secure and do not share them with anyone.</p>
|
||||||
<p style="margin-bottom: 0;">— The Stirling PDF Team</p>
|
<p style="margin-bottom: 0;">— The Stirling PDF Team</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +165,7 @@ public class EmailService {
|
|||||||
</div>
|
</div>
|
||||||
</body></html>
|
</body></html>
|
||||||
"""
|
"""
|
||||||
.formatted(username, temporaryPassword);
|
.formatted(username, temporaryPassword, loginUrl, loginUrl);
|
||||||
|
|
||||||
sendPlainEmail(to, subject, body, true);
|
sendPlainEmail(to, subject, body, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5261,7 +5261,7 @@ emailsPlaceholder = "[email protected], [email protected]"
|
|||||||
emailsRequired = "At least one email address is required"
|
emailsRequired = "At least one email address is required"
|
||||||
submit = "Send Invites"
|
submit = "Send Invites"
|
||||||
success = "user(s) invited successfully"
|
success = "user(s) invited successfully"
|
||||||
partialSuccess = "Some invites failed"
|
partialFailure = "Some invites failed"
|
||||||
allFailed = "Failed to invite users"
|
allFailed = "Failed to invite users"
|
||||||
error = "Failed to send invites"
|
error = "Failed to send invites"
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
interface InviteMembersModalProps {
|
interface InviteMembersModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InviteMembersModal({ opened, onClose }: InviteMembersModalProps) {
|
export default function InviteMembersModal({ opened, onClose, onSuccess }: InviteMembersModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -136,6 +137,7 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
|
|||||||
});
|
});
|
||||||
alert({ alertType: 'success', title: t('workspace.people.addMember.success') });
|
alert({ alertType: 'success', title: t('workspace.people.addMember.success') });
|
||||||
onClose();
|
onClose();
|
||||||
|
onSuccess?.();
|
||||||
// Reset form
|
// Reset form
|
||||||
setInviteForm({
|
setInviteForm({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -168,11 +170,23 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.successCount > 0) {
|
if (response.successCount > 0) {
|
||||||
|
// Show success message
|
||||||
alert({
|
alert({
|
||||||
alertType: 'success',
|
alertType: 'success',
|
||||||
title: t('workspace.people.emailInvite.success', { count: response.successCount, defaultValue: `Successfully invited ${response.successCount} user(s)` })
|
title: t('workspace.people.emailInvite.success', { count: response.successCount, defaultValue: `Successfully invited ${response.successCount} user(s)` })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show warning if there were partial failures
|
||||||
|
if (response.failureCount > 0 && response.errors) {
|
||||||
|
alert({
|
||||||
|
alertType: 'warning',
|
||||||
|
title: t('workspace.people.emailInvite.partialFailure', 'Some invites failed'),
|
||||||
|
body: response.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
onSuccess?.();
|
||||||
setEmailInviteForm({
|
setEmailInviteForm({
|
||||||
emails: '',
|
emails: '',
|
||||||
role: 'ROLE_USER',
|
role: 'ROLE_USER',
|
||||||
@@ -208,6 +222,7 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
|
|||||||
sendEmail: inviteLinkForm.sendEmail,
|
sendEmail: inviteLinkForm.sendEmail,
|
||||||
});
|
});
|
||||||
setGeneratedInviteLink(response.inviteUrl);
|
setGeneratedInviteLink(response.inviteUrl);
|
||||||
|
onSuccess?.();
|
||||||
if (inviteLinkForm.sendEmail && inviteLinkForm.email) {
|
if (inviteLinkForm.sendEmail && inviteLinkForm.email) {
|
||||||
alert({ alertType: 'success', title: t('workspace.people.inviteLink.emailSent', 'Invite link generated and sent via email') });
|
alert({ alertType: 'success', title: t('workspace.people.inviteLink.emailSent', 'Invite link generated and sent via email') });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,6 +588,7 @@ export default function PeopleSection() {
|
|||||||
<InviteMembersModal
|
<InviteMembersModal
|
||||||
opened={inviteModalOpened}
|
opened={inviteModalOpened}
|
||||||
onClose={() => setInviteModalOpened(false)}
|
onClose={() => setInviteModalOpened(false)}
|
||||||
|
onSuccess={fetchData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Edit User Modal */}
|
{/* Edit User Modal */}
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ export default function TeamsSection() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
await teamService.createTeam(newTeamName);
|
await teamService.createTeam(newTeamName);
|
||||||
alert({ alertType: 'success', title: t('workspace.teams.createTeam.success') });
|
alert({ alertType: 'success', title: t('workspace.teams.createTeam.success') });
|
||||||
setCreateModalOpened(false);
|
|
||||||
setNewTeamName('');
|
setNewTeamName('');
|
||||||
fetchTeams();
|
setCreateModalOpened(false);
|
||||||
|
await fetchTeams();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to create team:', error);
|
console.error('Failed to create team:', error);
|
||||||
const errorMessage = error.response?.data?.message ||
|
const errorMessage = error.response?.data?.message ||
|
||||||
@@ -105,10 +105,10 @@ export default function TeamsSection() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
await teamService.renameTeam(selectedTeam.id, renameTeamName);
|
await teamService.renameTeam(selectedTeam.id, renameTeamName);
|
||||||
alert({ alertType: 'success', title: t('workspace.teams.renameTeam.success') });
|
alert({ alertType: 'success', title: t('workspace.teams.renameTeam.success') });
|
||||||
setRenameModalOpened(false);
|
|
||||||
setSelectedTeam(null);
|
|
||||||
setRenameTeamName('');
|
setRenameTeamName('');
|
||||||
fetchTeams();
|
setSelectedTeam(null);
|
||||||
|
setRenameModalOpened(false);
|
||||||
|
await fetchTeams();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to rename team:', error);
|
console.error('Failed to rename team:', error);
|
||||||
const errorMessage = error.response?.data?.message ||
|
const errorMessage = error.response?.data?.message ||
|
||||||
@@ -134,7 +134,7 @@ export default function TeamsSection() {
|
|||||||
try {
|
try {
|
||||||
await teamService.deleteTeam(team.id);
|
await teamService.deleteTeam(team.id);
|
||||||
alert({ alertType: 'success', title: t('workspace.teams.deleteTeam.success') });
|
alert({ alertType: 'success', title: t('workspace.teams.deleteTeam.success') });
|
||||||
fetchTeams();
|
await fetchTeams();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to delete team:', error);
|
console.error('Failed to delete team:', error);
|
||||||
const errorMessage = error.response?.data?.message ||
|
const errorMessage = error.response?.data?.message ||
|
||||||
@@ -182,10 +182,10 @@ export default function TeamsSection() {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
await teamService.addUserToTeam(selectedTeam.id, parseInt(selectedUserId));
|
await teamService.addUserToTeam(selectedTeam.id, parseInt(selectedUserId));
|
||||||
alert({ alertType: 'success', title: t('workspace.teams.addMemberToTeam.success') });
|
alert({ alertType: 'success', title: t('workspace.teams.addMemberToTeam.success') });
|
||||||
setAddMemberModalOpened(false);
|
|
||||||
setSelectedTeam(null);
|
|
||||||
setSelectedUserId('');
|
setSelectedUserId('');
|
||||||
fetchTeams();
|
setSelectedTeam(null);
|
||||||
|
setAddMemberModalOpened(false);
|
||||||
|
await fetchTeams();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add member to team:', error);
|
console.error('Failed to add member to team:', error);
|
||||||
alert({ alertType: 'error', title: t('workspace.teams.addMemberToTeam.error') });
|
alert({ alertType: 'error', title: t('workspace.teams.addMemberToTeam.error') });
|
||||||
|
|||||||
Reference in New Issue
Block a user