feat: add EmailConnectorService for Logto email connector management

This commit is contained in:
hsiegeln
2026-04-25 17:58:26 +02:00
parent 2cd15509ba
commit 283d3e34a0

View File

@@ -0,0 +1,186 @@
package net.siegeln.cameleer.saas.vendor;
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class EmailConnectorService {
private static final Logger log = LoggerFactory.getLogger(EmailConnectorService.class);
private static final String SMTP_FACTORY_ID = "simple-mail-transfer-protocol";
private final LogtoManagementClient logtoClient;
public EmailConnectorService(LogtoManagementClient logtoClient) {
this.logtoClient = logtoClient;
}
public record SmtpConfig(String host, int port, String username, String password, String fromEmail) {}
public record EmailConnectorStatus(
String connectorId,
String factoryId,
String host,
int port,
String username,
String fromEmail,
boolean registrationEnabled
) {}
/** Get the current email connector config, or null if none is configured. */
@SuppressWarnings("unchecked")
public EmailConnectorStatus getEmailConnector() {
var connectors = logtoClient.listConnectors();
var emailConnector = connectors.stream()
.filter(c -> "Email".equals(c.get("type")))
.findFirst()
.orElse(null);
if (emailConnector == null) {
return null;
}
var config = (Map<String, Object>) emailConnector.getOrDefault("config", Map.of());
var auth = (Map<String, Object>) config.getOrDefault("auth", Map.of());
String host = String.valueOf(config.getOrDefault("host", ""));
int port = config.containsKey("port") ? ((Number) config.get("port")).intValue() : 587;
String username = String.valueOf(auth.getOrDefault("user", ""));
String fromEmail = String.valueOf(config.getOrDefault("fromEmail", ""));
boolean registrationEnabled = isRegistrationEnabled();
return new EmailConnectorStatus(
String.valueOf(emailConnector.get("id")),
String.valueOf(emailConnector.get("connectorId")),
host, port, username, fromEmail, registrationEnabled
);
}
/** Create or update the SMTP email connector. Returns the connector status. */
public EmailConnectorStatus saveSmtpConnector(SmtpConfig smtp, Boolean registrationEnabled) {
var connectorConfig = buildSmtpConfig(smtp);
// Check if an email connector already exists
var existing = getEmailConnector();
if (existing != null) {
logtoClient.updateConnector(existing.connectorId(), connectorConfig);
log.info("Updated SMTP email connector: {}", existing.connectorId());
} else {
var result = logtoClient.createConnector(SMTP_FACTORY_ID, connectorConfig);
log.info("Created SMTP email connector: {}", result != null ? result.get("id") : "unknown");
}
// Handle registration toggle
boolean enableReg = registrationEnabled != null ? registrationEnabled : (existing == null);
setRegistrationEnabled(enableReg);
return getEmailConnector();
}
/** Delete the email connector and disable registration. */
public void deleteEmailConnector() {
var existing = getEmailConnector();
if (existing != null) {
logtoClient.deleteConnector(existing.connectorId());
setRegistrationEnabled(false);
log.info("Deleted email connector: {}", existing.connectorId());
}
}
/** Send a test email through the configured connector. */
public void sendTestEmail(String toEmail) {
var existing = getEmailConnector();
if (existing == null) {
throw new IllegalStateException("No email connector configured");
}
// Re-read the full config from Logto to pass to the test endpoint
var connectors = logtoClient.listConnectors();
@SuppressWarnings("unchecked")
var emailConnector = connectors.stream()
.filter(c -> "Email".equals(c.get("type")))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Email connector not found"));
@SuppressWarnings("unchecked")
var config = (Map<String, Object>) emailConnector.getOrDefault("config", Map.of());
logtoClient.testConnector(existing.factoryId(), toEmail, config);
}
/** Set registration mode on the Logto sign-in experience. */
public void setRegistrationEnabled(boolean enabled) {
if (enabled) {
logtoClient.updateSignInExperience(Map.of(
"signInMode", "SignInAndRegister",
"signUp", Map.of(
"identifiers", List.of("email"),
"password", true,
"verify", true
),
"signIn", Map.of(
"methods", List.of(
Map.of("identifier", "email", "password", true, "verificationCode", false, "isPasswordPrimary", true),
Map.of("identifier", "username", "password", true, "verificationCode", false, "isPasswordPrimary", true)
)
)
));
} else {
logtoClient.updateSignInExperience(Map.of(
"signInMode", "SignIn",
"signIn", Map.of(
"methods", List.of(
Map.of("identifier", "username", "password", true, "verificationCode", false, "isPasswordPrimary", true)
)
)
));
}
}
/** Check if registration is currently enabled in Logto. */
@SuppressWarnings("unchecked")
private boolean isRegistrationEnabled() {
var signInExp = logtoClient.getSignInExperience();
if (signInExp == null) return false;
return "SignInAndRegister".equals(signInExp.get("signInMode"));
}
/** Build the Logto SMTP connector config with Cameleer-branded email templates. */
private Map<String, Object> buildSmtpConfig(SmtpConfig smtp) {
var config = new HashMap<String, Object>();
config.put("host", smtp.host());
config.put("port", smtp.port());
config.put("auth", Map.of("user", smtp.username(), "pass", smtp.password()));
config.put("fromEmail", smtp.fromEmail());
config.put("templates", List.of(
Map.of(
"usageType", "Register",
"contentType", "text/html",
"subject", "Verify your email for Cameleer",
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to verify your email and create your account:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request this, you can safely ignore this email.</p></div>"
),
Map.of(
"usageType", "SignIn",
"contentType", "text/html",
"subject", "Your Cameleer sign-in code",
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your sign-in verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
),
Map.of(
"usageType", "ForgotPassword",
"contentType", "text/html",
"subject", "Reset your Cameleer password",
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to reset your password:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request a password reset, you can safely ignore this email.</p></div>"
),
Map.of(
"usageType", "Generic",
"contentType", "text/html",
"subject", "Your Cameleer verification code",
"content", "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
)
));
return config;
}
}