mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01: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:
parent
85d9b5b83d
commit
4ae79d92ae
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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') });
|
||||
}
|
||||
|
||||
@ -588,6 +588,7 @@ export default function PeopleSection() {
|
||||
<InviteMembersModal
|
||||
opened={inviteModalOpened}
|
||||
onClose={() => setInviteModalOpened(false)}
|
||||
onSuccess={fetchData}
|
||||
/>
|
||||
|
||||
{/* Edit User Modal */}
|
||||
|
||||
@ -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') });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user