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:
Anthony Stirling 2025-11-29 16:03:44 +00:00 committed by GitHub
parent 85d9b5b83d
commit 4ae79d92ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 106 additions and 21 deletions

View File

@ -35,15 +35,40 @@ public class MailConfig {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(mailProperties.getHost());
mailSender.setPort(mailProperties.getPort());
mailSender.setUsername(mailProperties.getUsername());
mailSender.setPassword(mailProperties.getPassword());
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
Properties props = mailSender.getJavaMailProperties();
// Enables SMTP authentication
props.put("mail.smtp.auth", "true");
// Only enable SMTP authentication if credentials are provided
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
props.put("mail.smtp.starttls.enable", "true");

View File

@ -407,7 +407,8 @@ public class UserController {
public ResponseEntity<?> inviteUsers(
@RequestParam(name = "emails", required = true) String emails,
@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 {
// Check if email invites are enabled
@ -477,6 +478,9 @@ public class UserController {
}
}
// Build login URL
String loginUrl = buildLoginUrl(request);
int successCount = 0;
int failureCount = 0;
StringBuilder errors = new StringBuilder();
@ -488,7 +492,7 @@ public class UserController {
continue;
}
InviteResult result = processEmailInvite(email, effectiveTeamId, role);
InviteResult result = processEmailInvite(email, effectiveTeamId, role, loginUrl);
if (result.isSuccess()) {
successCount++;
} else {
@ -687,15 +691,45 @@ public class UserController {
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.
*
* @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
* @param loginUrl The URL to the login page
* @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 {
// Validate email format (basic check)
if (!email.contains("@") || !email.contains(".")) {
@ -715,7 +749,7 @@ public class UserController {
// Send invite email
try {
emailService.get().sendInviteEmail(email, email, temporaryPassword);
emailService.get().sendInviteEmail(email, email, temporaryPassword, loginUrl);
log.info("Sent invite email to: {}", email);
return InviteResult.success();
} catch (Exception emailEx) {

View File

@ -115,10 +115,12 @@ public class EmailService {
* @param to The recipient email address
* @param username The username for the new account
* @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.
*/
@Async
public void sendInviteEmail(String to, String username, String temporaryPassword)
public void sendInviteEmail(
String to, String username, String temporaryPassword, String loginUrl)
throws MessagingException {
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;">
<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>
<!-- 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 style="margin-bottom: 0;"> The Stirling PDF Team</p>
</div>
@ -155,7 +165,7 @@ public class EmailService {
</div>
</body></html>
"""
.formatted(username, temporaryPassword);
.formatted(username, temporaryPassword, loginUrl, loginUrl);
sendPlainEmail(to, subject, body, true);
}

View File

@ -5261,7 +5261,7 @@ emailsPlaceholder = "user1@example.com, user2@example.com"
emailsRequired = "At least one email address is required"
submit = "Send Invites"
success = "user(s) invited successfully"
partialSuccess = "Some invites failed"
partialFailure = "Some invites failed"
allFailed = "Failed to invite users"
error = "Failed to send invites"

View File

@ -27,9 +27,10 @@ import { useNavigate } from 'react-router-dom';
interface InviteMembersModalProps {
opened: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export default function InviteMembersModal({ opened, onClose }: InviteMembersModalProps) {
export default function InviteMembersModal({ opened, onClose, onSuccess }: InviteMembersModalProps) {
const { t } = useTranslation();
const { config } = useAppConfig();
const navigate = useNavigate();
@ -136,6 +137,7 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
});
alert({ alertType: 'success', title: t('workspace.people.addMember.success') });
onClose();
onSuccess?.();
// Reset form
setInviteForm({
username: '',
@ -168,11 +170,23 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
});
if (response.successCount > 0) {
// Show success message
alert({
alertType: 'success',
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();
onSuccess?.();
setEmailInviteForm({
emails: '',
role: 'ROLE_USER',
@ -208,6 +222,7 @@ export default function InviteMembersModal({ opened, onClose }: InviteMembersMod
sendEmail: inviteLinkForm.sendEmail,
});
setGeneratedInviteLink(response.inviteUrl);
onSuccess?.();
if (inviteLinkForm.sendEmail && inviteLinkForm.email) {
alert({ alertType: 'success', title: t('workspace.people.inviteLink.emailSent', 'Invite link generated and sent via email') });
}

View File

@ -588,6 +588,7 @@ export default function PeopleSection() {
<InviteMembersModal
opened={inviteModalOpened}
onClose={() => setInviteModalOpened(false)}
onSuccess={fetchData}
/>
{/* Edit User Modal */}

View File

@ -80,9 +80,9 @@ export default function TeamsSection() {
setProcessing(true);
await teamService.createTeam(newTeamName);
alert({ alertType: 'success', title: t('workspace.teams.createTeam.success') });
setCreateModalOpened(false);
setNewTeamName('');
fetchTeams();
setCreateModalOpened(false);
await fetchTeams();
} catch (error: any) {
console.error('Failed to create team:', error);
const errorMessage = error.response?.data?.message ||
@ -105,10 +105,10 @@ export default function TeamsSection() {
setProcessing(true);
await teamService.renameTeam(selectedTeam.id, renameTeamName);
alert({ alertType: 'success', title: t('workspace.teams.renameTeam.success') });
setRenameModalOpened(false);
setSelectedTeam(null);
setRenameTeamName('');
fetchTeams();
setSelectedTeam(null);
setRenameModalOpened(false);
await fetchTeams();
} catch (error: any) {
console.error('Failed to rename team:', error);
const errorMessage = error.response?.data?.message ||
@ -134,7 +134,7 @@ export default function TeamsSection() {
try {
await teamService.deleteTeam(team.id);
alert({ alertType: 'success', title: t('workspace.teams.deleteTeam.success') });
fetchTeams();
await fetchTeams();
} catch (error: any) {
console.error('Failed to delete team:', error);
const errorMessage = error.response?.data?.message ||
@ -182,10 +182,10 @@ export default function TeamsSection() {
setProcessing(true);
await teamService.addUserToTeam(selectedTeam.id, parseInt(selectedUserId));
alert({ alertType: 'success', title: t('workspace.teams.addMemberToTeam.success') });
setAddMemberModalOpened(false);
setSelectedTeam(null);
setSelectedUserId('');
fetchTeams();
setSelectedTeam(null);
setAddMemberModalOpened(false);
await fetchTeams();
} catch (error) {
console.error('Failed to add member to team:', error);
alert({ alertType: 'error', title: t('workspace.teams.addMemberToTeam.error') });