forked from Archives/Athou_commafeed
use quarkus mailer for password recovery
This commit is contained in:
@@ -3,10 +3,9 @@ TODO
|
|||||||
|
|
||||||
MVP:
|
MVP:
|
||||||
|
|
||||||
- quarkus mailer for smtp
|
|
||||||
- https://quarkus.io/guides/mailer
|
|
||||||
- cookie duration too short
|
- cookie duration too short
|
||||||
- https://github.com/quarkusio/quarkus/issues/42463
|
- https://github.com/quarkusio/quarkus/issues/42463
|
||||||
|
- Rewrite cookie with https://quarkus.io/guides/rest#request-or-response-filters in the mean time
|
||||||
|
|
||||||
- mvn profile instead of -Dquarkus.datasource.db-kind
|
- mvn profile instead of -Dquarkus.datasource.db-kind
|
||||||
- update github actions
|
- update github actions
|
||||||
|
|||||||
@@ -255,6 +255,10 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-websockets</artifactId>
|
<artifactId>quarkus-websockets</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-mailer</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||||
@@ -331,16 +335,11 @@
|
|||||||
<artifactId>passay</artifactId>
|
<artifactId>passay</artifactId>
|
||||||
<version>1.6.4</version>
|
<version>1.6.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>5.1.4</version>
|
<version>5.1.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.sun.mail</groupId>
|
|
||||||
<artifactId>jakarta.mail</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.rometools</groupId>
|
<groupId>com.rometools</groupId>
|
||||||
@@ -456,12 +455,6 @@
|
|||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.icegreen</groupId>
|
|
||||||
<artifactId>greenmail-junit5</artifactId>
|
|
||||||
<version>2.0.1</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.awaitility</groupId>
|
<groupId>org.awaitility</groupId>
|
||||||
<artifactId>awaitility</artifactId>
|
<artifactId>awaitility</artifactId>
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ public interface CommaFeedConfiguration {
|
|||||||
@WithDefault("false")
|
@WithDefault("false")
|
||||||
boolean imageProxyEnabled();
|
boolean imageProxyEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable password recovery via email.
|
||||||
|
*
|
||||||
|
* Quarkus mailer will need to be configured.
|
||||||
|
*/
|
||||||
|
@WithDefault("false")
|
||||||
|
boolean passwordRecoveryEnabled();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message displayed in a notification at the bottom of the page.
|
* Message displayed in a notification at the bottom of the page.
|
||||||
*/
|
*/
|
||||||
@@ -84,11 +92,6 @@ public interface CommaFeedConfiguration {
|
|||||||
*/
|
*/
|
||||||
Websocket websocket();
|
Websocket websocket();
|
||||||
|
|
||||||
/**
|
|
||||||
* SMTP settings for password recovery.
|
|
||||||
*/
|
|
||||||
Optional<Smtp> smtp();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis settings to enable caching. This is only really useful on instances with a lot of users.
|
* Redis settings to enable caching. This is only really useful on instances with a lot of users.
|
||||||
*/
|
*/
|
||||||
@@ -207,20 +210,6 @@ public interface CommaFeedConfiguration {
|
|||||||
boolean createDemoAccount();
|
boolean createDemoAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Smtp {
|
|
||||||
String host();
|
|
||||||
|
|
||||||
int port();
|
|
||||||
|
|
||||||
boolean tls();
|
|
||||||
|
|
||||||
String userName();
|
|
||||||
|
|
||||||
String password();
|
|
||||||
|
|
||||||
String fromAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Websocket {
|
interface Websocket {
|
||||||
/**
|
/**
|
||||||
* Enable websocket connection so the server can notify the web client that there are new entries for your feeds.
|
* Enable websocket connection so the server can notify the web client that there are new entries for your feeds.
|
||||||
|
|||||||
@@ -1,64 +1,20 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
|
||||||
import com.commafeed.CommaFeedConfiguration.Smtp;
|
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
|
|
||||||
|
import io.quarkus.mailer.Mail;
|
||||||
|
import io.quarkus.mailer.Mailer;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
import jakarta.mail.Authenticator;
|
|
||||||
import jakarta.mail.Message;
|
|
||||||
import jakarta.mail.PasswordAuthentication;
|
|
||||||
import jakarta.mail.Session;
|
|
||||||
import jakarta.mail.Transport;
|
|
||||||
import jakarta.mail.internet.InternetAddress;
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
/**
|
|
||||||
* Mailing service
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Singleton
|
@Singleton
|
||||||
public class MailService {
|
public class MailService {
|
||||||
|
|
||||||
private final CommaFeedConfiguration config;
|
private final Mailer mailer;
|
||||||
|
|
||||||
public void sendMail(User user, String subject, String content) throws Exception {
|
|
||||||
Optional<Smtp> settings = config.smtp();
|
|
||||||
if (settings.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("SMTP settings not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
final String username = settings.get().userName();
|
|
||||||
final String password = settings.get().password();
|
|
||||||
final String fromAddress = Optional.ofNullable(settings.get().fromAddress()).orElse(settings.get().userName());
|
|
||||||
|
|
||||||
String dest = user.getEmail();
|
|
||||||
|
|
||||||
Properties props = new Properties();
|
|
||||||
props.put("mail.smtp.auth", "true");
|
|
||||||
props.put("mail.smtp.starttls.enable", String.valueOf(settings.get().tls()));
|
|
||||||
props.put("mail.smtp.host", settings.get().host());
|
|
||||||
props.put("mail.smtp.port", String.valueOf(settings.get().port()));
|
|
||||||
|
|
||||||
Session session = Session.getInstance(props, new Authenticator() {
|
|
||||||
@Override
|
|
||||||
protected PasswordAuthentication getPasswordAuthentication() {
|
|
||||||
return new PasswordAuthentication(username, password);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Message message = new MimeMessage(session);
|
|
||||||
message.setFrom(new InternetAddress(fromAddress, "CommaFeed"));
|
|
||||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
|
|
||||||
message.setSubject("CommaFeed - " + subject);
|
|
||||||
message.setContent(content, "text/html; charset=utf-8");
|
|
||||||
|
|
||||||
Transport.send(message);
|
|
||||||
|
|
||||||
|
public void sendMail(User user, String subject, String content) {
|
||||||
|
Mail mail = Mail.withHtml(user.getEmail(), "CommaFeed - " + subject, content);
|
||||||
|
mailer.send(mail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class ServerREST {
|
|||||||
infos.setGitCommit(version.getGitCommit());
|
infos.setGitCommit(version.getGitCommit());
|
||||||
infos.setAllowRegistrations(config.users().allowRegistrations());
|
infos.setAllowRegistrations(config.users().allowRegistrations());
|
||||||
infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null));
|
infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null));
|
||||||
infos.setSmtpEnabled(config.smtp().isPresent());
|
infos.setSmtpEnabled(config.passwordRecoveryEnabled());
|
||||||
infos.setDemoAccountEnabled(config.users().createDemoAccount());
|
infos.setDemoAccountEnabled(config.users().createDemoAccount());
|
||||||
infos.setWebsocketEnabled(config.websocket().enabled());
|
infos.setWebsocketEnabled(config.websocket().enabled());
|
||||||
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ public class UserREST {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@Operation(summary = "send a password reset email")
|
@Operation(summary = "send a password reset email")
|
||||||
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
||||||
|
if (!config.passwordRecoveryEnabled()) {
|
||||||
|
throw new IllegalArgumentException("Password recovery is not enabled on this CommaFeed instance");
|
||||||
|
}
|
||||||
|
|
||||||
User user = userDAO.findByEmail(req.getEmail());
|
User user = userDAO.findByEmail(req.getEmail());
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
|
|||||||
@@ -14,18 +14,19 @@ import jakarta.ws.rs.GET;
|
|||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.core.NewCookie;
|
import jakarta.ws.rs.core.NewCookie;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Path("/logout")
|
@Path("/logout")
|
||||||
@PermitAll
|
@PermitAll
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class LogoutServlet {
|
public class LogoutServlet {
|
||||||
|
|
||||||
@ConfigProperty(name = "quarkus.http.auth.form.cookie-name")
|
|
||||||
String cookieName;
|
|
||||||
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
private final String cookieName;
|
||||||
|
|
||||||
|
public LogoutServlet(CommaFeedConfiguration config, @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) {
|
||||||
|
this.config = config;
|
||||||
|
this.cookieName = cookieName;
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
public Response get() {
|
public Response get() {
|
||||||
|
|||||||
@@ -35,12 +35,7 @@ quarkus.shutdown.timeout=5s
|
|||||||
%test.quarkus.log.category."liquibase".level=WARN
|
%test.quarkus.log.category."liquibase".level=WARN
|
||||||
%test.commafeed.users.create-demo-account=true
|
%test.commafeed.users.create-demo-account=true
|
||||||
%test.commafeed.users.allow-registrations=true
|
%test.commafeed.users.allow-registrations=true
|
||||||
%test.commafeed.smtp.host=localhost
|
%test.commafeed.password-recovery-enabled=true
|
||||||
%test.commafeed.smtp.port=3025
|
|
||||||
%test.commafeed.smtp.tls=false
|
|
||||||
%test.commafeed.smtp.user-name=user
|
|
||||||
%test.commafeed.smtp.password=pass
|
|
||||||
%test.commafeed.smtp.from-address=noreply@commafeed.com
|
|
||||||
|
|
||||||
|
|
||||||
# prod profile overrides
|
# prod profile overrides
|
||||||
|
|||||||
@@ -1,52 +1,44 @@
|
|||||||
package com.commafeed.integration.rest;
|
package com.commafeed.integration.rest;
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import com.commafeed.frontend.model.request.PasswordResetRequest;
|
import com.commafeed.frontend.model.request.PasswordResetRequest;
|
||||||
import com.commafeed.integration.BaseIT;
|
import com.commafeed.integration.BaseIT;
|
||||||
import com.icegreen.greenmail.util.GreenMail;
|
|
||||||
import com.icegreen.greenmail.util.ServerSetupTest;
|
|
||||||
|
|
||||||
|
import io.quarkus.mailer.MockMailbox;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import io.vertx.ext.mail.MailMessage;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.client.Entity;
|
import jakarta.ws.rs.client.Entity;
|
||||||
|
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
class UserIT extends BaseIT {
|
class UserIT extends BaseIT {
|
||||||
|
|
||||||
@Nested
|
@Inject
|
||||||
class PasswordReset {
|
MockMailbox mailbox;
|
||||||
|
|
||||||
private GreenMail greenMail;
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
mailbox.clear();
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@Test
|
||||||
void setup() {
|
void resetPassword() {
|
||||||
this.greenMail = new GreenMail(ServerSetupTest.SMTP);
|
PasswordResetRequest req = new PasswordResetRequest();
|
||||||
this.greenMail.start();
|
req.setEmail("admin@commafeed.com");
|
||||||
this.greenMail.setUser("noreply@commafeed.com", "user", "pass");
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE);
|
||||||
void cleanup() {
|
|
||||||
this.greenMail.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
List<MailMessage> mails = mailbox.getMailMessagesSentTo("admin@commafeed.com");
|
||||||
void resetPassword() throws Exception {
|
Assertions.assertEquals(1, mails.size());
|
||||||
PasswordResetRequest req = new PasswordResetRequest();
|
|
||||||
req.setEmail("admin@commafeed.com");
|
|
||||||
|
|
||||||
getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE);
|
MailMessage message = mails.get(0);
|
||||||
|
Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject());
|
||||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
Assertions.assertTrue(message.getHtml().startsWith("You asked for password recovery for account 'admin'"));
|
||||||
Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject());
|
Assertions.assertEquals("admin@commafeed.com", message.getTo().get(0));
|
||||||
Assertions.assertTrue(message.getContent().toString().startsWith("You asked for password recovery for account 'admin'"));
|
|
||||||
Assertions.assertEquals("CommaFeed <noreply@commafeed.com>", message.getFrom()[0].toString());
|
|
||||||
Assertions.assertEquals("admin@commafeed.com", message.getAllRecipients()[0].toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user