diff --git a/src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java b/src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java index 0b702d3..5172398 100644 --- a/src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java +++ b/src/main/java/group/goforward/battlbuilder/configuration/SecurityConfig.java @@ -46,8 +46,7 @@ public class SecurityConfig { // protected .requestMatchers("/api/v1/builds/me/**").authenticated() - .requestMatchers("/api/v1/admin/**").authenticated() - + .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // everything else (adjust later as you lock down) .anyRequest().permitAll() ) diff --git a/src/main/java/group/goforward/battlbuilder/controllers/EmailTrackingController.java b/src/main/java/group/goforward/battlbuilder/controllers/EmailTrackingController.java new file mode 100644 index 0000000..d31c1f7 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/controllers/EmailTrackingController.java @@ -0,0 +1,48 @@ +package group.goforward.battlbuilder.controllers; + +import group.goforward.battlbuilder.repos.EmailRequestRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.time.LocalDateTime; + +@RestController +@RequestMapping("/api/email") +public class EmailTrackingController { + + // 1x1 transparent GIF + private static final byte[] PIXEL = new byte[] { + 71,73,70,56,57,97,1,0,1,0,-128,0,0,0,0,0,-1,-1,-1,33,-7,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59 + }; + + private final EmailRequestRepository repo; + + public EmailTrackingController(EmailRequestRepository repo) { + this.repo = repo; + } + + @GetMapping(value = "/open/{id}", produces = "image/gif") + public ResponseEntity open(@PathVariable Long id) { + repo.findById(id).ifPresent(r -> { + if (r.getOpenedAt() == null) r.setOpenedAt(LocalDateTime.now()); + r.setOpenCount((r.getOpenCount() == null ? 0 : r.getOpenCount()) + 1); + repo.save(r); + }); + + return ResponseEntity.ok() + .header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + .body(PIXEL); + } + + @GetMapping("/click/{id}") + public ResponseEntity click(@PathVariable Long id, @RequestParam String url) { + repo.findById(id).ifPresent(r -> { + if (r.getClickedAt() == null) r.setClickedAt(LocalDateTime.now()); + r.setClickCount((r.getClickCount() == null ? 0 : r.getClickCount()) + 1); + repo.save(r); + }); + + return ResponseEntity.status(302).location(URI.create(url)).build(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/model/EmailRequest.java b/src/main/java/group/goforward/battlbuilder/model/EmailRequest.java index 47743f9..ec77e8e 100644 --- a/src/main/java/group/goforward/battlbuilder/model/EmailRequest.java +++ b/src/main/java/group/goforward/battlbuilder/model/EmailRequest.java @@ -1,24 +1,25 @@ package group.goforward.battlbuilder.model; import jakarta.persistence.*; + import java.time.LocalDateTime; @Entity @Table(name = "email_requests") -@NamedQuery( - name = "EmailRequest.findSent", - query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.SENT" -) - -@NamedQuery( - name = "EmailRequest.findFailed", - query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.FAILED" -) - -@NamedQuery( - name = "EmailRequest.findPending", - query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.PENDING" -) +@NamedQueries({ + @NamedQuery( + name = "EmailRequest.findSent", + query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.SENT" + ), + @NamedQuery( + name = "EmailRequest.findFailed", + query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.FAILED" + ), + @NamedQuery( + name = "EmailRequest.findPending", + query = "SELECT e FROM EmailRequest e WHERE e.status = group.goforward.battlbuilder.model.EmailStatus.PENDING" + ) +}) public class EmailRequest { @Id @@ -34,101 +35,95 @@ public class EmailRequest { @Column(columnDefinition = "TEXT") private String body; - @Column(name = "sent_at") - private LocalDateTime sentAt; + @Column(name = "template_key", length = 100) + private String templateKey; @Enumerated(EnumType.STRING) @Column(nullable = false) private EmailStatus status; // PENDING, SENT, FAILED + @Column(name = "sent_at") + private LocalDateTime sentAt; + @Column(name = "error_message") private String errorMessage; + @Column(name = "opened_at") + private LocalDateTime openedAt; + + @Column(name = "open_count", nullable = false) + private Integer openCount = 0; + + @Column(name = "clicked_at") + private LocalDateTime clickedAt; + + @Column(name = "click_count", nullable = false) + private Integer clickCount = 0; + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; - @PrePersist - protected void onCreate() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - if (status == null) { - status = EmailStatus.PENDING; - } - } - - @Column(name = "updated_at", nullable = false, updatable = false) + // ✅ should be updatable + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; - // Getters and Setters - public Long getId() { - return id; + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + createdAt = now; + updatedAt = now; + + if (status == null) status = EmailStatus.PENDING; + if (openCount == null) openCount = 0; + if (clickCount == null) clickCount = 0; } - public void setId(Long id) { - this.id = id; + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); } - public String getRecipient() { - return recipient; - } + // ===== Getters / Setters ===== - public void setRecipient(String recipient) { - this.recipient = recipient; - } + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } - public String getSubject() { - return subject; - } + public String getRecipient() { return recipient; } + public void setRecipient(String recipient) { this.recipient = recipient; } - public void setSubject(String subject) { - this.subject = subject; - } + public String getSubject() { return subject; } + public void setSubject(String subject) { this.subject = subject; } - public String getBody() { - return body; - } + public String getBody() { return body; } + public void setBody(String body) { this.body = body; } - public void setBody(String body) { - this.body = body; - } + public String getTemplateKey() { return templateKey; } + public void setTemplateKey(String templateKey) { this.templateKey = templateKey; } - public LocalDateTime getSentAt() { - return sentAt; - } + public EmailStatus getStatus() { return status; } + public void setStatus(EmailStatus status) { this.status = status; } - public void setSentAt(LocalDateTime sentAt) { - this.sentAt = sentAt; - } + public LocalDateTime getSentAt() { return sentAt; } + public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; } - public EmailStatus getStatus() { - return status; - } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } - public void setStatus(EmailStatus status) { - this.status = status; - } + public LocalDateTime getOpenedAt() { return openedAt; } + public void setOpenedAt(LocalDateTime openedAt) { this.openedAt = openedAt; } - public String getErrorMessage() { - return errorMessage; - } + public Integer getOpenCount() { return openCount; } + public void setOpenCount(Integer openCount) { this.openCount = openCount; } - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } + public LocalDateTime getClickedAt() { return clickedAt; } + public void setClickedAt(LocalDateTime clickedAt) { this.clickedAt = clickedAt; } - public LocalDateTime getCreatedAt() { - return createdAt; - } + public Integer getClickCount() { return clickCount; } + public void setClickCount(Integer clickCount) { this.clickCount = clickCount; } - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } -} + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/model/EmailTemplate.java b/src/main/java/group/goforward/battlbuilder/model/EmailTemplate.java new file mode 100644 index 0000000..9ceebec --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/model/EmailTemplate.java @@ -0,0 +1,85 @@ +package group.goforward.battlbuilder.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "email_template") +public class EmailTemplate { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "template_key", nullable = false, unique = true, length = 100) + private String templateKey; + + @Column(name = "name", nullable = false, length = 150) + private String name; + + @Column(name = "subject", nullable = false, length = 255) + private String subject; + + @Column(name = "mjml", columnDefinition = "text") + private String mjml; + + @Column(name = "html_body", nullable = false, columnDefinition = "text") + private String htmlBody; + + @Column(name = "text_body", columnDefinition = "text") + private String textBody; + + @Column(name = "enabled", nullable = false) + private Boolean enabled = true; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + void onCreate() { + LocalDateTime now = LocalDateTime.now(); + if (createdAt == null) createdAt = now; + if (updatedAt == null) updatedAt = now; + if (enabled == null) enabled = true; + } + + @PreUpdate + void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + // --- getters/setters --- + + public Long getId() { return id; } + + public String getTemplateKey() { return templateKey; } + public void setTemplateKey(String templateKey) { this.templateKey = templateKey; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getSubject() { return subject; } + public void setSubject(String subject) { this.subject = subject; } + + public String getMjml() { return mjml; } + public void setMjml(String mjml) { this.mjml = mjml; } + + public String getHtmlBody() { return htmlBody; } + public void setHtmlBody(String htmlBody) { this.htmlBody = htmlBody; } + + public String getTextBody() { return textBody; } + public void setTextBody(String textBody) { this.textBody = textBody; } + + public Boolean getEnabled() { return enabled; } + public void setEnabled(Boolean enabled) { this.enabled = enabled; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/repos/EmailTemplateRepository.java b/src/main/java/group/goforward/battlbuilder/repos/EmailTemplateRepository.java new file mode 100644 index 0000000..f3d62ca --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/repos/EmailTemplateRepository.java @@ -0,0 +1,14 @@ +package group.goforward.battlbuilder.repos; + +import group.goforward.battlbuilder.model.EmailTemplate; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmailTemplateRepository extends JpaRepository { + + Optional findByTemplateKeyAndEnabledTrue(String templateKey); + +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/auth/impl/BetaInviteService.java b/src/main/java/group/goforward/battlbuilder/services/auth/impl/BetaInviteService.java index 372875e..6055f08 100644 --- a/src/main/java/group/goforward/battlbuilder/services/auth/impl/BetaInviteService.java +++ b/src/main/java/group/goforward/battlbuilder/services/auth/impl/BetaInviteService.java @@ -4,7 +4,7 @@ import group.goforward.battlbuilder.model.AuthToken; import group.goforward.battlbuilder.model.User; import group.goforward.battlbuilder.repos.AuthTokenRepository; import group.goforward.battlbuilder.repos.UserRepository; -import group.goforward.battlbuilder.services.utils.EmailService; +import group.goforward.battlbuilder.services.utils.TemplatedEmailService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -14,13 +14,14 @@ import java.security.SecureRandom; import java.time.OffsetDateTime; import java.util.HexFormat; import java.util.List; +import java.util.Map; @Service public class BetaInviteService { private final UserRepository users; private final AuthTokenRepository tokens; - private final EmailService emailService; + private final TemplatedEmailService templatedEmailService; @Value("${app.publicBaseUrl:http://localhost:3000}") private String publicBaseUrl; @@ -30,15 +31,19 @@ public class BetaInviteService { private final SecureRandom secureRandom = new SecureRandom(); - public BetaInviteService(UserRepository users, AuthTokenRepository tokens, EmailService emailService) { + // ✅ Constructor injection + public BetaInviteService( + UserRepository users, + AuthTokenRepository tokens, + TemplatedEmailService templatedEmailService + ) { this.users = users; this.tokens = tokens; - this.emailService = emailService; + this.templatedEmailService = templatedEmailService; } public int inviteAllBetaUsers(int tokenMinutes, int limit, boolean dryRun) { - // You may need to adjust this query depending on your repo methods. - // See NOTE below if you don’t have this finder. + List betaUsers = (limit > 0) ? users.findTopNByRoleAndIsActiveFalseAndDeletedAtIsNull("BETA", limit) : users.findByRoleAndIsActiveFalseAndDeletedAtIsNull("BETA"); @@ -49,23 +54,24 @@ public class BetaInviteService { String email = user.getEmail(); String magicToken = generateToken(); - saveToken(email, AuthToken.TokenType.MAGIC_LOGIN, magicToken, - OffsetDateTime.now().plusMinutes(tokenMinutes)); + saveToken( + email, + AuthToken.TokenType.MAGIC_LOGIN, + magicToken, + OffsetDateTime.now().plusMinutes(tokenMinutes) + ); String magicUrl = publicBaseUrl + "/beta/magic?token=" + magicToken; - String subject = "Your Battl Builders beta access link"; - String body = """ - You’re in. - - Here’s your secure sign-in link (expires in %d minutes): - %s - - If you didn’t request this, you can ignore this email. - """.formatted(tokenMinutes, magicUrl); - if (!dryRun) { - emailService.sendEmail(email, subject, body); + templatedEmailService.send( + "beta_invite", // template_key + email, + Map.of( + "minutes", String.valueOf(tokenMinutes), + "magicUrl", magicUrl + ) + ); } sent++; @@ -74,7 +80,12 @@ public class BetaInviteService { return sent; } - private void saveToken(String email, AuthToken.TokenType type, String token, OffsetDateTime expiresAt) { + private void saveToken( + String email, + AuthToken.TokenType type, + String token, + OffsetDateTime expiresAt + ) { AuthToken t = new AuthToken(); t.setEmail(email); t.setType(type); @@ -93,7 +104,9 @@ public class BetaInviteService { private String hashToken(String token) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] hashed = md.digest((tokenPepper + ":" + token).getBytes(StandardCharsets.UTF_8)); + byte[] hashed = md.digest( + (tokenPepper + ":" + token).getBytes(StandardCharsets.UTF_8) + ); return HexFormat.of().formatHex(hashed); } catch (Exception e) { throw new RuntimeException("Failed to hash token", e); diff --git a/src/main/java/group/goforward/battlbuilder/services/utils/EmailService.java b/src/main/java/group/goforward/battlbuilder/services/utils/EmailService.java index 8255861..578ae2d 100644 --- a/src/main/java/group/goforward/battlbuilder/services/utils/EmailService.java +++ b/src/main/java/group/goforward/battlbuilder/services/utils/EmailService.java @@ -1,12 +1,13 @@ package group.goforward.battlbuilder.services.utils; -import aj.org.objectweb.asm.commons.Remapper; import group.goforward.battlbuilder.model.EmailRequest; public interface EmailService { EmailRequest sendEmail(String recipient, String subject, String body); + + EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody); + + EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody, String templateKey); + void deleteById(Integer id); - - -} - +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/utils/TemplateRenderer.java b/src/main/java/group/goforward/battlbuilder/services/utils/TemplateRenderer.java new file mode 100644 index 0000000..3b63737 --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/services/utils/TemplateRenderer.java @@ -0,0 +1,15 @@ +package group.goforward.battlbuilder.services.utils; + +import java.util.Map; + +public final class TemplateRenderer { + private TemplateRenderer() {} + + public static String render(String template, Map vars) { + String out = template; + for (var e : vars.entrySet()) { + out = out.replace("{{" + e.getKey() + "}}", e.getValue() == null ? "" : e.getValue()); + } + return out; + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/utils/TemplatedEmailService.java b/src/main/java/group/goforward/battlbuilder/services/utils/TemplatedEmailService.java new file mode 100644 index 0000000..2cc6e5b --- /dev/null +++ b/src/main/java/group/goforward/battlbuilder/services/utils/TemplatedEmailService.java @@ -0,0 +1,32 @@ +package group.goforward.battlbuilder.services.utils; + +import group.goforward.battlbuilder.model.EmailRequest; +import group.goforward.battlbuilder.model.EmailTemplate; +import group.goforward.battlbuilder.repos.EmailTemplateRepository; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class TemplatedEmailService { + + private final EmailTemplateRepository templates; + private final EmailService emailService; + + public TemplatedEmailService(EmailTemplateRepository templates, EmailService emailService) { + this.templates = templates; + this.emailService = emailService; + } + + public EmailRequest send(String templateKey, String to, Map vars) { + EmailTemplate t = templates.findByTemplateKeyAndEnabledTrue(templateKey) + .orElseThrow(() -> new IllegalArgumentException("Missing/disabled email template: " + templateKey)); + + String subject = TemplateRenderer.render(t.getSubject(), vars); + String html = TemplateRenderer.render(t.getHtmlBody(), vars); + String text = t.getTextBody() == null ? null : TemplateRenderer.render(t.getTextBody(), vars); + + // ✅ template_key persisted inside EmailService (no double-save) + return emailService.sendEmailHtml(to, subject, html, text, templateKey); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/battlbuilder/services/utils/impl/EmailServiceImpl.java b/src/main/java/group/goforward/battlbuilder/services/utils/impl/EmailServiceImpl.java index 191e707..27ac0e9 100644 --- a/src/main/java/group/goforward/battlbuilder/services/utils/impl/EmailServiceImpl.java +++ b/src/main/java/group/goforward/battlbuilder/services/utils/impl/EmailServiceImpl.java @@ -31,38 +31,81 @@ public class EmailServiceImpl implements EmailService { @Value("${app.email.outbound-enabled:true}") private boolean outboundEnabled; - /** - * Sends an email and persists send status. - * Uses multipart=true to avoid MimeMessageHelper errors when setting text. - */ @Override @Transactional public EmailRequest sendEmail(String recipient, String subject, String body) { + EmailRequest req = new EmailRequest(); + req.setRecipient(recipient); + req.setSubject(subject); + req.setBody(body); + req.setStatus(EmailStatus.PENDING); - // Persist initial request - EmailRequest emailRequest = new EmailRequest(); - emailRequest.setRecipient(recipient); - emailRequest.setSubject(subject); - emailRequest.setBody(body); - emailRequest.setStatus(EmailStatus.PENDING); - emailRequest.setCreatedAt(LocalDateTime.now()); - - emailRequest = emailRequestRepository.save(emailRequest); - - // ✅ Capture-only mode: record it, but don’t send + // Capture-only mode: store but don’t send if (!outboundEnabled) { - emailRequest.setStatus(EmailStatus.FAILED); - emailRequest.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)"); - return emailRequestRepository.save(emailRequest); + req.setStatus(EmailStatus.PENDING); + req.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)"); + return emailRequestRepository.save(req); } try { MimeMessage message = mailSender.createMimeMessage(); - - // ✅ multipart=true fixes "Not in multipart mode" errors MimeMessageHelper helper = new MimeMessageHelper( message, - true, // multipart + true, + StandardCharsets.UTF_8.name() + ); + + helper.setFrom(fromEmail); + helper.setTo(recipient); + helper.setSubject(subject); + helper.setText(body, false); + + mailSender.send(message); + + req.setStatus(EmailStatus.SENT); + req.setSentAt(LocalDateTime.now()); + } catch (Exception e) { + req.setStatus(EmailStatus.FAILED); + req.setErrorMessage(e.getMessage()); + } + + // ✅ Single INSERT (no update spam) + return emailRequestRepository.save(req); + } + + @Override + public void deleteById(Integer id) { + emailRequestRepository.deleteById(id.longValue()); + } + + @Override + public EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody) { + return sendEmailHtml(recipient, subject, htmlBody, textBody, null); + } + + @Override + @Transactional + public EmailRequest sendEmailHtml(String recipient, String subject, String htmlBody, String textBody, String templateKey) { + + EmailRequest req = new EmailRequest(); + req.setRecipient(recipient); + req.setSubject(subject); + req.setBody(htmlBody); // storing HTML for now + req.setTemplateKey(templateKey); + req.setStatus(EmailStatus.PENDING); + + // Capture-only mode: store but don’t send + if (!outboundEnabled) { + req.setStatus(EmailStatus.PENDING); + req.setErrorMessage("Outbound email suppressed by config (app.email.outbound-enabled=false)"); + return emailRequestRepository.save(req); + } + + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper( + message, + true, StandardCharsets.UTF_8.name() ); @@ -70,24 +113,19 @@ public class EmailServiceImpl implements EmailService { helper.setTo(recipient); helper.setSubject(subject); - // Plain text email (safe + deliverable) - helper.setText(body, false); + // plain + html (best practice) + helper.setText(textBody != null ? textBody : "", htmlBody); mailSender.send(message); - emailRequest.setStatus(EmailStatus.SENT); - emailRequest.setSentAt(LocalDateTime.now()); - + req.setStatus(EmailStatus.SENT); + req.setSentAt(LocalDateTime.now()); } catch (Exception e) { - emailRequest.setStatus(EmailStatus.FAILED); - emailRequest.setErrorMessage(e.getMessage()); + req.setStatus(EmailStatus.FAILED); + req.setErrorMessage(e.getMessage()); } - return emailRequestRepository.save(emailRequest); - } - - @Override - public void deleteById(Integer id) { - emailRequestRepository.deleteById(id.longValue()); + // ✅ Single INSERT (no extra UPDATEs) + return emailRequestRepository.save(req); } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 258622b..57db7dd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -56,7 +56,7 @@ security.jwt.access-token-days=30 # Beta Invite Email Toggle app.beta.captureOnly=true -app.email.outbound-enabled=false +app.email.outbound-enabled=true # CLI invite runner (off by default) app.beta.invite.run=false