Skip to content

Commit 7db52a5

Browse files
author
dterefe
committed
Added email verification alongside env-variables to configure an smtp server.
1 parent ba02004 commit 7db52a5

File tree

19 files changed

+995
-77
lines changed

19 files changed

+995
-77
lines changed

DUUIRestService/build_run.sh

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,18 @@ export DBX_REDIRECT_URL=http://localhost:5173/account/dropbox
1212
export GOOGLE_CLIENT_ID=
1313
export GOOGLE_CLIENT_SECRET=
1414
export GOOGLE_REDIRECT_URI=http://localhost:5173/account/google
15-
export JAVA_TOOL_OPTIONS="--add-opens=java.base/java.util=ALL-UNNAMED"
15+
export SMPTP_HOST=localhost
16+
export SMTP_PORT=1025
17+
export SMTP_USER=
18+
export SMTP_PASSWORD=
19+
export MAIL_FROM_EMAIL=help@duui.de
20+
export USE_SMTP_DEBUG=true
1621

22+
export JAVA_TOOL_OPTIONS="--add-opens=java.base/java.util=ALL-UNNAMED"
1723

18-
# mvn clean compile
19-
# mvn exec:java -Dexec.mainClass="org.texttechnologylab.duui.api.Main"
20-
mvn clean -Dstyle.color=never -Djansi.strip=true package
21-
#java -jar target/DUUIRestService-1.0-all.jar
24+
# To run a local SMTP server for testing email sending, you can use MailHog:
25+
# docker run --rm -p 1025:1025 -p 8025:8025 mailhog/mailhog
2226

23-
#mvn -q -DskipTests package
24-
#
25-
## Confirm no signatures inside the fat jar
26-
#jar tf target/*-all.jar | egrep 'META-INF/.*\.(SF|DSA|RSA|EC)' || echo "no signatures"
27-
#
28-
## (optional) confirm uimaFIT indices exist
29-
#jar tf target/*-all.jar | grep 'META-INF/org.apache.uima.fit/'
30-
#
31-
java -jar target/DUUIRestService-1.0-all.jar
27+
mvn clean compile
28+
mvn -Dstyle.color=never -Djansi.strip=true package
29+
java -Djava.net.preferIPv4Stack=true -jar target/DUUIRestService-1.0-all.jar

DUUIRestService/pom.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<maven.javadoc.skip>true</maven.javadoc.skip>
4343
<duui.version>efaf4412ee</duui.version>
4444
<uima.version>3.5.0</uima.version>
45-
<typesystem.version>3.0.8</typesystem.version>
45+
<typesystem.version>3.0.12</typesystem.version>
4646
<maven.javadoc.skip>false</maven.javadoc.skip>
4747
<dkpro.core.version>2.4.0</dkpro.core.version>
4848
</properties>
@@ -51,6 +51,8 @@
5151
<repository>
5252
<id>jitpack.io</id>
5353
<url>https://jitpack.io</url>
54+
<releases><enabled>true</enabled></releases>
55+
<snapshots><enabled>true</enabled></snapshots>
5456
</repository>
5557
</repositories>
5658

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/Config.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,10 @@ public String getMongoPassword() {
7272
}
7373

7474
public String getSmtpHost() {
75-
return getValue("SMTP_HOST", null);
75+
return getValue("SMTP_HOST", "localhost");
7676
}
7777

78-
public int getSmtpPort() {
79-
return Integer.parseInt(getValue("SMTP_PORT", "587"));
80-
}
78+
public String getSmtpPort() { return getValue("SMTP_PORT", "587"); }
8179

8280
public String getSmtpUser() {
8381
return getValue("SMTP_USERNAME", null);
@@ -91,6 +89,10 @@ public String getSmtpFromEmail() {
9189
return getValue("MAIL_FROM_EMAIL", null);
9290
}
9391

92+
public boolean getUseSmptpDebug() {
93+
return Boolean.parseBoolean(getValue("USE_SMTP_DEBUG", "true"));
94+
}
95+
9496
public String getDropboxKey() {
9597

9698
String value;

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/Methods.java

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.texttechnologylab.duui.api;
22

3+
import com.mongodb.client.MongoCollection;
4+
import org.bson.Document;
35
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
57
import org.texttechnologylab.duui.api.controllers.users.DUUIUserController;
@@ -9,6 +11,8 @@
911
import org.texttechnologylab.duui.api.routes.components.DUUIComponentRequestHandler;
1012
import org.texttechnologylab.duui.api.routes.pipelines.DUUIPipelineRequestHandler;
1113
import org.texttechnologylab.duui.api.routes.processes.DUUIProcessRequestHandler;
14+
import org.texttechnologylab.duui.api.storage.DUUIMongoDBStorage;
15+
import org.texttechnologylab.duui.api.utils.DUUIMailClient;
1216

1317
import java.time.Duration;
1418

@@ -151,25 +155,74 @@ public static void init() {
151155
put("/google", DUUIUserController::finishGoogleOAuthFromCode);
152156
});
153157

154-
path("/activation", () -> {
155-
post("/send", (req, res) -> {
156-
String userId = req.queryParams("userId");
157-
String email = req.queryParams("email");
158+
path("/verification", () -> {
159+
get("/email/:email", (req, res) -> {
160+
String email = req.params("email");
161+
MongoCollection<Document> usersCollection = DUUIMongoDBStorage.Users();
158162

159-
String code = DUUIUserController.issueActivationCode(userId, Duration.ofMinutes(10));
163+
Document userDoc = usersCollection.find(new Document("email", email)).limit(1).first();
164+
if (userDoc == null) {
165+
res.status(401);
166+
return null;
167+
}
160168

161-
DUUIUserController.sendMail(email, "Your activation code", "Code: " + code + "\nExpires in 10 minutes.");
162-
return "ok";
169+
org.bson.types.ObjectId id = userDoc.getObjectId("_id");
170+
if (id == null) {
171+
res.status(404);
172+
return null;
173+
}
174+
175+
return id.toHexString();
176+
});
177+
178+
path("/activation", () -> {
179+
post("/send", (req, res) -> {
180+
String userId = req.queryParams("userId");
181+
String email = req.queryParams("email");
182+
183+
String code = DUUIUserController.issueActivationCode(userId, Duration.ofMinutes(10));
184+
185+
DUUIMailClient.sendMail(email, "Your activation code", "Code: " + code + "\nExpires in 10 minutes.");
186+
187+
res.status(200);
188+
return "ok";
189+
});
190+
191+
post("/verify", (req, res) -> {
192+
String userId = req.queryParams("userId");
193+
String code = req.queryParams("code");
194+
195+
boolean ok = DUUIUserController.verifyActivationCode(userId, code);
196+
if (!ok) halt(400, "invalid or expired");
197+
198+
res.status(200);
199+
return "activated";
200+
});
163201
});
164202

165-
post("/verify", (req, res) -> {
166-
String userId = req.queryParams("userId");
167-
String code = req.queryParams("code");
203+
path("/recovery", () -> {
204+
post("/send", (req, res) -> {
205+
String userId = req.queryParams("userId");
206+
String email = req.queryParams("email");
207+
208+
String code = DUUIUserController.issueRecoveryCode(userId, Duration.ofMinutes(10));
209+
210+
DUUIMailClient.sendMail(email, "Your password reset code", "Code: " + code + "\nExpires in 10 minutes.");
211+
212+
res.status(200);
213+
return "ok";
214+
});
215+
216+
post("/verify", (req, res) -> {
217+
String userId = req.queryParams("userId");
218+
String code = req.queryParams("code");
219+
220+
boolean ok = DUUIUserController.verifyRecoveryCode(userId, code);
221+
if (!ok) halt(400, "invalid or expired");
168222

169-
boolean ok = DUUIUserController.verifyActivationCode(userId, code);
170-
if (!ok) halt(400, "invalid or expired");
171-
// mark user activated in your users collection here
172-
return "activated";
223+
res.status(200);
224+
return "reset";
225+
});
173226
});
174227

175228
});

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/controllers/users/DUUIUserController.java

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.texttechnologylab.duui.api.controllers.pipelines.DUUIPipelineController;
2323
import org.texttechnologylab.duui.api.routes.DUUIRequestHelper;
2424

25-
import static com.amazonaws.regions.ServiceAbbreviations.Config;
2625
import static org.texttechnologylab.duui.api.routes.DUUIRequestHelper.*;
2726

2827
import org.texttechnologylab.duui.api.storage.DUUIMongoDBStorage;
@@ -45,10 +44,6 @@
4544
import spark.Request;
4645
import spark.Response;
4746

48-
import javax.mail.*;
49-
import javax.mail.internet.InternetAddress;
50-
import javax.mail.internet.MimeMessage;
51-
5247

5348
/**
5449
* A Controller for database operations related to the users collection.
@@ -1420,42 +1415,122 @@ public static boolean verifyActivationCode(String userId, String code) throws Ex
14201415
Filters.gte("expiresAt", now)
14211416
);
14221417

1418+
log.info("verifyActivationCode: start | userId={} now={}", userId, now);
1419+
log.debug("verifyActivationCode: filter={}", filter.toString());
1420+
14231421
var update = Updates.combine(
14241422
Updates.set("used", true)
14251423
);
14261424

1427-
var res = col.findOneAndUpdate(filter, update, new FindOneAndUpdateOptions()
1428-
.returnDocument(ReturnDocument.AFTER));
1425+
var options = new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER);
1426+
1427+
var res = col.findOneAndUpdate(filter, update, options);
1428+
1429+
if (res != null) {
1430+
log.info("verifyActivationCode: activation record matched and marked used for userId={}", userId);
1431+
log.debug("verifyActivationCode: activation record after update={}", res.toString());
1432+
1433+
try {
1434+
// Korrigierter Filter: match by _id as ObjectId
1435+
Bson userFilter = Filters.eq("_id", new ObjectId(userId));
1436+
var userDoc = DUUIMongoDBStorage
1437+
.Users()
1438+
.findOneAndUpdate(userFilter, Updates.set("activated", true), new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER));
1439+
1440+
if (userDoc != null) {
1441+
log.info("verifyActivationCode: user document updated, activated=true for userId={}", userId);
1442+
log.info("verifyActivationCode: updated user document={}", userDoc.toJson());
1443+
} else {
1444+
// Fallback: inspect UpdateResult to get matched/modified counts
1445+
UpdateResult ur = DUUIMongoDBStorage.Users().updateOne(userFilter, Updates.set("activated", true));
1446+
log.warn("verifyActivationCode: findOneAndUpdate returned null for userId={}, fallback updateOne result: matched={}, modified={}, acknowledged={}",
1447+
userId, ur.getMatchedCount(), ur.getModifiedCount(), ur.wasAcknowledged());
1448+
}
1449+
} catch (IllegalArgumentException iae) {
1450+
log.error("verifyActivationCode: invalid userId format: {}", userId, iae);
1451+
} catch (Exception e) {
1452+
log.error("verifyActivationCode: failed to set activated=true for userId={}", userId, e);
1453+
}
1454+
} else {
1455+
log.info("verifyActivationCode: no activation record found (invalid/expired/already used) for userId={}", userId);
1456+
}
14291457

14301458
return res != null; // true => success; false => invalid/expired/already used
14311459
}
14321460

1433-
public static void sendMail(String to, String subject, String body) throws MessagingException {
1434-
String host = Main.config.getSmtpHost();
1435-
String port = Main.config.getSmtpPort();
1436-
String user = Main.config.getSmtpUser();
1437-
String pass = Main.config.getSmtpPassword();
1438-
String from = Main.config.getSmtpFromEmail();
1439-
1440-
Properties props = new Properties();
1441-
props.put("mail.smtp.auth", "true");
1442-
props.put("mail.smtp.starttls.enable", "true");
1443-
props.put("mail.smtp.host", host);
1444-
props.put("mail.smtp.port", port);
1445-
1446-
Session session = Session.getInstance(props, new Authenticator() {
1447-
protected PasswordAuthentication getPasswordAuthentication() {
1448-
return new PasswordAuthentication(user, pass);
1449-
}
1450-
});
14511461

1452-
Message message = new MimeMessage(session);
1453-
message.setFrom(new InternetAddress(from));
1454-
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
1455-
message.setSubject(subject);
1456-
message.setText(body);
1462+
// public static boolean verifyActivationCode(String userId, String code) throws Exception {
1463+
// MongoCollection<DataModel.MongoActivation> col = DUUIMongoDBStorage.Activations();
1464+
// String codeHash = hashCode(code);
1465+
// Instant now = Instant.now();
1466+
//
1467+
// var filter = Filters.and(
1468+
// Filters.eq("userId", userId),
1469+
// Filters.eq("purpose", "activation"),
1470+
// Filters.eq("codeHash", codeHash),
1471+
// Filters.eq("used", false),
1472+
// Filters.gte("expiresAt", now)
1473+
// );
1474+
//
1475+
// var update = Updates.combine(
1476+
// Updates.set("used", true)
1477+
// );
1478+
//
1479+
// var res = col.findOneAndUpdate(filter, update, new FindOneAndUpdateOptions()
1480+
// .returnDocument(ReturnDocument.AFTER));
1481+
//
1482+
// if (res != null) {
1483+
// DUUIMongoDBStorage
1484+
// .Users()
1485+
// .findOneAndUpdate(
1486+
// Filters.eq(userId),
1487+
// Updates.set("activated", true)
1488+
// );
1489+
// }
1490+
//
1491+
//
1492+
// return res != null; // true => success; false => invalid/expired/already used
1493+
// }
14571494

1458-
Transport.send(message);
1495+
public static String issueRecoveryCode(String userId, Duration ttl) throws Exception {
1496+
MongoCollection<DataModel.MongoActivation> col = DUUIMongoDBStorage.Activations();
1497+
String code = sixDigit();
1498+
String codeHash = hashCode(code);
1499+
Instant now = Instant.now();
1500+
Instant exp = now.plus(ttl);
1501+
1502+
// upsert: replace any previous unused code for this user/purpose
1503+
var filter = Filters.and(Filters.eq("userId", userId),
1504+
Filters.eq("purpose", "recovery"),
1505+
Filters.eq("used", false));
1506+
var rec = new DataModel.MongoActivation(null, userId, "recovery", codeHash, exp, now, false);
1507+
var options = new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER);
1508+
col.findOneAndReplace(filter, rec, options);
1509+
1510+
return code; // send this in the email
1511+
}
1512+
1513+
public static boolean verifyRecoveryCode(String userId, String code) throws Exception {
1514+
MongoCollection<DataModel.MongoActivation> col = DUUIMongoDBStorage.Activations();
1515+
String codeHash = hashCode(code);
1516+
Instant now = Instant.now();
1517+
1518+
var filter = Filters.and(
1519+
Filters.eq("userId", userId),
1520+
Filters.eq("purpose", "recovery"),
1521+
Filters.eq("codeHash", codeHash),
1522+
Filters.eq("used", false),
1523+
Filters.gte("expiresAt", now)
1524+
);
1525+
1526+
var update = Updates.combine(
1527+
Updates.set("used", true)
1528+
);
1529+
1530+
var res = col.findOneAndUpdate(filter, update, new FindOneAndUpdateOptions()
1531+
.returnDocument(ReturnDocument.AFTER));
1532+
1533+
return res != null; // true => success; false => invalid/expired/already used
14591534
}
14601535

14611536

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/storage/DUUIMongoDBStorage.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ public static MongoCollection<Document> Users() {
363363
return getClient().getDatabase(config.getMongoDatabase()).getCollection("users");
364364
}
365365

366+
public static MongoCollection<DataModel.MongoUser> TypedUsers() {
367+
DUUIStorageMetrics.incrementUsersCounter();
368+
return getClient().getDatabase(config.getMongoDatabase()).getCollection("users", DataModel.MongoUser.class);
369+
}
370+
366371
/**
367372
* Utility functions for fast access to collections in the database.
368373
*

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/storage/DataModel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ public record MongoUser(
175175
Role role,
176176
Integer workers,
177177
String session,
178+
Boolean activated,
178179
String password_reset_token,
179180
Long reset_token_expiration,
180181
Map<String, MongoPreference> preferences,

0 commit comments

Comments
 (0)