Browse Source

restructuring, working on password reset email

next steps:
- create reset url and add it to the translation fill map
- implement message translation
- implement otp validation and login
feature/document
Stephan Richter 4 months ago
parent
commit
7a5bb50ee2
  1. 4
      backend/src/main/java/de/srsoftware/umbrella/backend/Application.java
  2. 1
      core/build.gradle.kts
  3. 1
      core/src/main/java/de/srsoftware/umbrella/core/Constants.java
  4. 20
      core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java
  5. 87
      core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java
  6. 11
      frontend/src/routes/user/ResetPw.svelte
  7. 2
      legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java
  8. 1
      messages/build.gradle.kts
  9. 2
      messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java
  10. 18
      messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java
  11. 2
      messages/src/main/java/de/srsoftware/umbrella/message/SqliteMessageDb.java
  12. 4
      messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java
  13. 27
      messages/src/main/java/de/srsoftware/umbrella/message/model/Envelope.java
  14. 14
      messages/src/main/java/de/srsoftware/umbrella/message/model/Message.java
  15. 1
      user/build.gradle.kts
  16. 2
      user/src/main/java/de/srsoftware/umbrella/user/Constants.java
  17. 1
      user/src/main/java/de/srsoftware/umbrella/user/Paths.java
  18. 81
      user/src/main/java/de/srsoftware/umbrella/user/UserModule.java
  19. 5
      user/src/main/java/de/srsoftware/umbrella/user/api/UserDb.java
  20. 4
      user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java
  21. 1
      user/src/main/java/de/srsoftware/umbrella/user/model/Session.java
  22. 49
      user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java
  23. 59
      user/src/main/java/de/srsoftware/umbrella/user/model/User.java
  24. 20
      user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java

4
backend/src/main/java/de/srsoftware/umbrella/backend/Application.java

@ -51,7 +51,7 @@ public class Application {
var connectionProvider = new ConnectionProvider(); var connectionProvider = new ConnectionProvider();
var messageDb = new SqliteMessageDb(connectionProvider.get(messageDbFile)); var messageDb = new SqliteMessageDb(connectionProvider.get(messageDbFile));
var userDb = new SqliteDB(connectionProvider.get(userDbFile)); var userDb = new SqliteDB(connectionProvider.get(userDbFile));
var loginServicedb = new SqliteDB(connectionProvider.get(loginDbFile)); var loginServiceDb = new SqliteDB(connectionProvider.get(loginDbFile));
var translationModule = new Translations(); var translationModule = new Translations();
@ -63,7 +63,7 @@ public class Application {
new LegacyApi(userDb,config).bindPath("/legacy").on(server); new LegacyApi(userDb,config).bindPath("/legacy").on(server);
new MessageApi(messageSystem).bindPath("/api/messages").on(server); new MessageApi(messageSystem).bindPath("/api/messages").on(server);
translationModule.bindPath("/api/translations").on(server); translationModule.bindPath("/api/translations").on(server);
new UserModule(userDb,loginServicedb).bindPath("/api/user").on(server); new UserModule(userDb,loginServiceDb,messageSystem).bindPath("/api/user").on(server);
new WebHandler().bindPath("/").on(server); new WebHandler().bindPath("/").on(server);
server.start(); server.start();
LOG.log(INFO,"Started web server at {0}",port); LOG.log(INFO,"Started web server at {0}",port);

1
core/build.gradle.kts

@ -12,6 +12,7 @@ repositories {
dependencies { dependencies {
implementation("de.srsoftware:tools.mime:1.1.2") implementation("de.srsoftware:tools.mime:1.1.2")
implementation("de.srsoftware:tools.optionals:1.0.0") implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:2.0.3")
implementation("org.json:json:20240303") implementation("org.json:json:20240303")
implementation("org.xerial:sqlite-jdbc:3.49.0.0") implementation("org.xerial:sqlite-jdbc:3.49.0.0")
testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation(platform("org.junit:junit-bom:5.10.0"))

1
core/src/main/java/de/srsoftware/umbrella/core/Constants.java

@ -36,6 +36,7 @@ public class Constants {
public static final String MIME = "mime"; public static final String MIME = "mime";
public static final String NUMBER = "number"; public static final String NUMBER = "number";
public static final String OPTIONAL = "optional"; public static final String OPTIONAL = "optional";
public static final String PASS = "pass";
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
public static final String POST = "POST"; public static final String POST = "POST";

20
core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java

@ -0,0 +1,20 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.tools.Optionals.allSet;
import static java.text.MessageFormat.format;
public class EmailAddress {
private final String email;
public EmailAddress(String addr){
var parts = addr.split("@");
if (parts.length != 2 || !allSet(parts[0],parts[1])) throw new IllegalArgumentException(format("{0} is not a valid email address!",addr));
email = addr;
}
@Override
public String toString() {
return email;
}
}

87
core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java

@ -0,0 +1,87 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.UmbrellaException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.json.JSONObject;
/* © SRSoftware 2025 */
public class UmbrellaUser implements Mappable {
private final long id;
private final String theme, name, lang;
private final EmailAddress email;
public UmbrellaUser(long id, String name, EmailAddress email, String theme, String languageCode) {
this.email = email;
this.name = name;
this.id = id;
this.theme = theme;
this.lang = languageCode;
}
public EmailAddress email(){
return email;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof UmbrellaUser user)) return false;
return Objects.equals(email, user.email)
&& Objects.equals(name, user.name)
&& Objects.equals(id, user.id)
&& Objects.equals(lang, user.lang);
}
@Override
public int hashCode() {
return Objects.hash(email, id, lang, name);
}
public long id(){
return id;
}
public String language(){
return lang;
}
public String name(){
return name;
}
public static UmbrellaUser of(JSONObject json) throws UmbrellaException {
if (json.has(USER)) json = json.getJSONObject(USER);
if (!json.has(ID)) throw new UmbrellaException(HTTP_BAD_REQUEST,ERROR_MISSING_FIELD,ID);
var id = json.getLong(ID);
var name = json.has(NAME) ? json.getString(NAME) : null;
var email = json.has(EMAIL) ? new EmailAddress(json.getString(EMAIL)) : null;
var theme = json.has(THEME) ? json.getString(THEME) : null;
var lang = json.has(LANGUAGE) ? json.getString(LANGUAGE) : null;
return new UmbrellaUser(id, name, email, theme, lang);
}
public String theme(){
return theme;
}
@Override
public Map<String,Object> toMap() {
var map = new HashMap<String,Object>();
map.put(ID,id);
map.put(LOGIN, name()); // this is still used by old umbrella modules
map.put(NAME, name());
map.put(EMAIL,email().toString());
map.put(THEME,theme);
map.put(LANGUAGE,lang);
return map;
}
}

11
frontend/src/routes/user/ResetPw.svelte

@ -8,7 +8,16 @@
async function submit(ev){ async function submit(ev){
ev.preventDefault(); ev.preventDefault();
caption = t('user.sent_email'); const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/reset_pw`;
caption = t('user.data_sent');
const resp = await fetch(url,{
method : 'POST',
body : mail
});
if (resp.ok) {
} else {
}
} }

2
legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java

@ -21,8 +21,8 @@ import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.Token; import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.user.model.Session; import de.srsoftware.umbrella.user.model.Session;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import de.srsoftware.umbrella.user.sqlite.SqliteDB; import de.srsoftware.umbrella.user.sqlite.SqliteDB;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;

1
messages/build.gradle.kts

@ -2,7 +2,6 @@ description = "Umbrella : Message subsystem"
dependencies{ dependencies{
implementation(project(":core")) implementation(project(":core"))
implementation(project(":user"))
implementation("com.sun.mail:jakarta.mail:2.0.1") implementation("com.sun.mail:jakarta.mail:2.0.1")
implementation("de.srsoftware:configuration.api:1.0.2") implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.jdbc:1.3.2")

2
messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java

@ -2,8 +2,8 @@
package de.srsoftware.umbrella.message; package de.srsoftware.umbrella.message;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.message.model.Settings; import de.srsoftware.umbrella.message.model.Settings;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
public interface MessageDb { public interface MessageDb {
public Settings getSettings(UmbrellaUser user) throws UmbrellaException; public Settings getSettings(UmbrellaUser user) throws UmbrellaException;

18
messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java

@ -4,17 +4,15 @@ package de.srsoftware.umbrella.message;
import static de.srsoftware.tools.PathHandler.CONTENT_TYPE; import static de.srsoftware.tools.PathHandler.CONTENT_TYPE;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.message.Constants.*; import static de.srsoftware.umbrella.message.Constants.*;
import static de.srsoftware.umbrella.user.Constants.PASS;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.api.Translator; import de.srsoftware.umbrella.core.api.Translator;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.message.model.CombinedMessage; import de.srsoftware.umbrella.message.model.CombinedMessage;
import de.srsoftware.umbrella.message.model.Envelope; import de.srsoftware.umbrella.message.model.Envelope;
import de.srsoftware.umbrella.message.model.PostBox; import de.srsoftware.umbrella.message.model.PostBox;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import de.srsoftware.umbrella.user.model.User;
import jakarta.activation.DataHandler; import jakarta.activation.DataHandler;
import jakarta.mail.Message; import jakarta.mail.Message;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
@ -31,7 +29,7 @@ import java.util.function.Function;
public class MessageSystem implements PostBox { public class MessageSystem implements PostBox {
public static final System.Logger LOG = System.getLogger(MessageSystem.class.getSimpleName()); public static final System.Logger LOG = System.getLogger(MessageSystem.class.getSimpleName());
private final Timer timer = new Timer(); private final Timer timer = new Timer();
private record Receiver(User user, de.srsoftware.umbrella.message.model.Message message){} private record Receiver(UmbrellaUser user, de.srsoftware.umbrella.message.model.Message message){}
private class SubmissionTask extends TimerTask{ private class SubmissionTask extends TimerTask{
@ -98,11 +96,11 @@ public class MessageSystem implements PostBox {
private synchronized void processMessages(Integer scheduledHour) { private synchronized void processMessages(Integer scheduledHour) {
LOG.log(INFO,"Running {0}…",scheduledHour == null ? "instantly" : "scheduled at "+scheduledHour); LOG.log(INFO,"Running {0}…",scheduledHour == null ? "instantly" : "scheduled at "+scheduledHour);
var queue = new ArrayList<>(this.queue); var queue = new ArrayList<>(this.queue);
var dueRecipients = new ArrayList<User>(); var dueRecipients = new ArrayList<UmbrellaUser>();
List<User> recipients = queue.stream().map(Envelope::receivers).flatMap(Set::stream).filter(Objects::nonNull).distinct().toList(); List<UmbrellaUser> recipients = queue.stream().map(Envelope::receivers).flatMap(Set::stream).filter(Objects::nonNull).distinct().toList();
{ // for known users: get notification preferences, fallback to _immediately_ for unknown users { // for known users: get notification preferences, fallback to _immediately_ for unknown users
for (User recv : recipients) { for (UmbrellaUser recv : recipients) {
if (recv instanceof UmbrellaUser uu) { if (recv instanceof UmbrellaUser uu) {
try { try {
if (!db.getSettings(uu).sendAt(scheduledHour)) continue; if (!db.getSettings(uu).sendAt(scheduledHour)) continue;
@ -156,17 +154,17 @@ public class MessageSystem implements PostBox {
} }
private void send(CombinedMessage message, User receiver, Date date) throws MessagingException { private void send(CombinedMessage message, UmbrellaUser receiver, Date date) throws MessagingException {
LOG.log(TRACE,"Sending combined message to {0}…",receiver); LOG.log(TRACE,"Sending combined message to {0}…",receiver);
session = session(); session = session();
MimeMessage msg = new MimeMessage(session); MimeMessage msg = new MimeMessage(session);
msg.addHeader(CONTENT_TYPE, "text/markdown; charset=UTF-8"); msg.addHeader(CONTENT_TYPE, "text/markdown; charset=UTF-8");
msg.addHeader("format", "flowed"); msg.addHeader("format", "flowed");
msg.addHeader("Content-Transfer-Encoding", "8bit"); msg.addHeader("Content-Transfer-Encoding", "8bit");
msg.setFrom(message.sender().email()); msg.setFrom(message.sender().email().toString());
msg.setSubject(message.subject(), UTF8); msg.setSubject(message.subject(), UTF8);
msg.setSentDate(date); msg.setSentDate(date);
var toEmail = debugAddress != null ? debugAddress : receiver.email(); var toEmail = debugAddress != null ? debugAddress : receiver.email().toString();
msg.setRecipients(Message.RecipientType.TO, toEmail); msg.setRecipients(Message.RecipientType.TO, toEmail);
if (message.attachments().isEmpty()){ if (message.attachments().isEmpty()){

2
messages/src/main/java/de/srsoftware/umbrella/message/SqliteMessageDb.java

@ -10,8 +10,8 @@ import static java.lang.System.Logger.Level.WARNING;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.message.model.Settings; import de.srsoftware.umbrella.message.model.Settings;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;

4
messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java

@ -5,7 +5,7 @@ import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.user.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -44,7 +44,7 @@ public class CombinedMessage {
body.append("# ").append(message.subject()).append(":\n\n"); body.append("# ").append(message.subject()).append(":\n\n");
body.append(message.body()); body.append(message.body());
} }
attachments.addAll(message.attachments()); if (message.attachments() != null) attachments.addAll(message.attachments());
mergedMessages.add(message); mergedMessages.add(message);
} }

27
messages/src/main/java/de/srsoftware/umbrella/message/model/Envelope.java

@ -7,7 +7,8 @@ import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.user.model.User; import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -17,28 +18,38 @@ import org.json.JSONObject;
public class Envelope { public class Envelope {
private Message message; private Message message;
private Set<User> receivers; private Set<UmbrellaUser> receivers;
public Envelope(Message message, HashSet<User> receivers) { public Envelope(Message message, UmbrellaUser receiver){
this(message,new HashSet<>(Set.of(receiver)));
}
public Envelope(Message message, HashSet<UmbrellaUser> receivers) {
this.message = message; this.message = message;
this.receivers = receivers; this.receivers = receivers;
} }
/**
* TODO: this is legacy, move to legacy module!
* @param json
* @return
* @throws UmbrellaException
*/
public static Envelope from(JSONObject json) throws UmbrellaException { public static Envelope from(JSONObject json) throws UmbrellaException {
if (!json.has(RECEIVERS)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,RECEIVERS); if (!json.has(RECEIVERS)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,RECEIVERS);
var message = Message.from(json); var message = Message.from(json);
var obj = json.get(RECEIVERS); var obj = json.get(RECEIVERS);
if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj)); if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj));
if (!(obj instanceof JSONArray receiverList)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,RECEIVERS,JSONARRAY); if (!(obj instanceof JSONArray receiverList)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,RECEIVERS,JSONARRAY);
var receivers = new HashSet<User>(); var receivers = new HashSet<UmbrellaUser>();
for (var o : receiverList){ for (var o : receiverList){
if (!(o instanceof JSONObject receiverData)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,"entries of "+RECEIVERS,JSONOBJECT); if (!(o instanceof JSONObject receiverData)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,"entries of "+RECEIVERS,JSONOBJECT);
receivers.add(User.of(receiverData)); receivers.add(UmbrellaUser.of(receiverData));
} }
return new Envelope(message,receivers); return new Envelope(message,receivers);
} }
public boolean isFor(User receiver) { public boolean isFor(UmbrellaUser receiver) {
return receivers.contains(receiver); return receivers.contains(receiver);
} }
@ -46,12 +57,12 @@ public class Envelope {
return message; return message;
} }
public Set<User> receivers(){ public Set<UmbrellaUser> receivers(){
return receivers; return receivers;
} }
@Override @Override
public String toString() { public String toString() {
return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(User::email).collect(Collectors.joining(", ")),message.subject()); return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(UmbrellaUser::email).map(EmailAddress::toString).collect(Collectors.joining(", ")),message.subject());
} }
} }

14
messages/src/main/java/de/srsoftware/umbrella/message/model/Message.java

@ -7,16 +7,12 @@ import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.user.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.user.model.User; import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
public record Message(UmbrellaUser sender, String subject, String body, List<Attachment> attachments) { public record Message(UmbrellaUser sender, String subject, String body, Map<String,String> fills, List<Attachment> attachments) {
public static Message from(JSONObject json) throws UmbrellaException { public static Message from(JSONObject json) throws UmbrellaException {
for (var key : Set.of(SENDER, SUBJECT, BODY)) { for (var key : Set.of(SENDER, SUBJECT, BODY)) {
if (!json.has(key)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,key); if (!json.has(key)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,key);
@ -25,7 +21,7 @@ public record Message(UmbrellaUser sender, String subject, String body, List<Att
if (!(json.get(SUBJECT) instanceof String subject && isSet(subject))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, SUBJECT,STRING); if (!(json.get(SUBJECT) instanceof String subject && isSet(subject))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, SUBJECT,STRING);
if (!(json.get(BODY) instanceof String body && isSet(body))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, BODY,STRING); if (!(json.get(BODY) instanceof String body && isSet(body))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, BODY,STRING);
var user = User.of(senderObject); var user = UmbrellaUser.of(senderObject);
if (!(user instanceof UmbrellaUser sender)) throw new UmbrellaException(400,"Sender is not an umbrella user!"); if (!(user instanceof UmbrellaUser sender)) throw new UmbrellaException(400,"Sender is not an umbrella user!");
var attachments = new ArrayList<Attachment>(); var attachments = new ArrayList<Attachment>();
if (json.has(ATTACHMENTS)){ if (json.has(ATTACHMENTS)){
@ -39,7 +35,7 @@ public record Message(UmbrellaUser sender, String subject, String body, List<Att
} }
} }
} }
return new Message(sender,subject,body,attachments); return new Message(sender,subject,body,null, attachments);
} }
@Override @Override

1
user/build.gradle.kts

@ -2,6 +2,7 @@ description = "Umbrella : User"
dependencies{ dependencies{
implementation(project(":core")) implementation(project(":core"))
implementation(project(":messages"))
implementation("de.srsoftware:configuration.api:1.0.2") implementation("de.srsoftware:configuration.api:1.0.2")
implementation("de.srsoftware:tools.jdbc:1.3.2") implementation("de.srsoftware:tools.jdbc:1.3.2")
implementation("de.srsoftware:tools.mime:1.1.2") implementation("de.srsoftware:tools.mime:1.1.2")

2
user/src/main/java/de/srsoftware/umbrella/user/Constants.java

@ -33,8 +33,6 @@ public class Constants {
public static final String OIDC_LINK = "oidc_link"; public static final String OIDC_LINK = "oidc_link";
public static final String OIDC_SCOPE = "openid"; public static final String OIDC_SCOPE = "openid";
public static final String PASS = "pass";
public static final String REDIRECT_URI = "redirect_uri"; public static final String REDIRECT_URI = "redirect_uri";
public static final String RESPONSE_TYPE = "response_type"; public static final String RESPONSE_TYPE = "response_type";
public static final String SCOPE = "scope"; public static final String SCOPE = "scope";

1
user/src/main/java/de/srsoftware/umbrella/user/Paths.java

@ -16,6 +16,7 @@ public class Paths {
public static final String BUTTONS = "buttons"; public static final String BUTTONS = "buttons";
public static final String OIDC_LOGIN = "oidc_login"; public static final String OIDC_LOGIN = "oidc_login";
public static final String OPENID_LOGIN = "openid_login"; public static final String OPENID_LOGIN = "openid_login";
public static final String RESET_PW = "reset_pw";
public static final String SESSION = "session"; public static final String SESSION = "session";
public static final String VALIDATE_TOKEN = "validateToken"; public static final String VALIDATE_TOKEN = "validateToken";
public static final String WHOAMI = "whoami"; public static final String WHOAMI = "whoami";

81
user/src/main/java/de/srsoftware/umbrella/user/UserModule.java

@ -3,6 +3,7 @@ package de.srsoftware.umbrella.user;
import static de.srsoftware.tools.MimeType.MIME_FORM_URL; import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.Optionals.*; import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.tools.Strings.uuid;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Paths.LIST; import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Paths.LOGOUT; import static de.srsoftware.umbrella.core.Paths.LOGOUT;
@ -27,6 +28,11 @@ import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.Token; import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.message.MessageSystem;
import de.srsoftware.umbrella.message.model.Envelope;
import de.srsoftware.umbrella.message.model.Message;
import de.srsoftware.umbrella.user.api.LoginServiceDb; import de.srsoftware.umbrella.user.api.LoginServiceDb;
import de.srsoftware.umbrella.user.api.UserDb; import de.srsoftware.umbrella.user.api.UserDb;
import de.srsoftware.umbrella.user.model.*; import de.srsoftware.umbrella.user.model.*;
@ -47,6 +53,7 @@ import org.json.JSONObject;
public class UserModule extends BaseHandler { public class UserModule extends BaseHandler {
private record State(LoginService loginService, JSONObject config){ private record State(LoginService loginService, JSONObject config){
public static State of(LoginService loginService, JSONObject config) { public static State of(LoginService loginService, JSONObject config) {
return new State(loginService,config); return new State(loginService,config);
@ -57,7 +64,9 @@ public class UserModule extends BaseHandler {
private static final System.Logger LOG = System.getLogger("User"); private static final System.Logger LOG = System.getLogger("User");
private final UserDb users; private final UserDb users;
private final LoginServiceDb logins; private final LoginServiceDb logins;
private final HashMap<String, State> stateMep = new HashMap<>(); // map from state to OIDC provider name private final HashMap<String, State> stateMap = new HashMap<>(); // map from state to OIDC provider name
private final HashMap<String,String> tokenMap = new HashMap<>();
private final MessageSystem messages;
static { static {
try { try {
@ -67,9 +76,10 @@ public class UserModule extends BaseHandler {
} }
} }
public UserModule(UserDb userDb, LoginServiceDb loginDb){ public UserModule(UserDb userDb, LoginServiceDb loginDb, MessageSystem messageSystem){
users = userDb;
logins = loginDb; logins = loginDb;
messages = messageSystem;
users = userDb;
} }
@ -234,6 +244,7 @@ public class UserModule extends BaseHandler {
case OIDC: return postOIDC(ex,path); case OIDC: return postOIDC(ex,path);
case IMPERSONATE: return impersonate(ex,targetId); case IMPERSONATE: return impersonate(ex,targetId);
case LOGIN: return postLogin(ex); case LOGIN: return postLogin(ex);
case RESET_PW: return postResetPassword(ex);
} }
return super.doPost(path, ex); return super.doPost(path, ex);
} }
@ -249,7 +260,7 @@ public class UserModule extends BaseHandler {
if (!params.has(CODE)) return sendContent(ex,HTTP_BAD_REQUEST,"missing auth code"); if (!params.has(CODE)) return sendContent(ex,HTTP_BAD_REQUEST,"missing auth code");
if (!params.has(STATE)) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted"); if (!params.has(STATE)) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
var code = params.getString(CODE); var code = params.getString(CODE);
var state = stateMep.remove(params.getString(STATE)); var state = stateMap.remove(params.getString(STATE));
if (state == null) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted"); if (state == null) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
var redirect = url(ex).replaceAll("/api/.*",""); var redirect = url(ex).replaceAll("/api/.*","");
var location = state.config.getString(TOKEN_ENDPOINT); var location = state.config.getString(TOKEN_ENDPOINT);
@ -284,23 +295,6 @@ public class UserModule extends BaseHandler {
} }
} }
private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException {
var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
var audience = state.loginService.clientId();
var httpJwks = new HttpsJwks(jwksEndpoint);
var resolver = new HttpsJwksVerificationKeyResolver(httpJwks);
JwtConsumer consumer = new JwtConsumerBuilder()
.setVerificationKeyResolver(resolver)
.setExpectedAudience(audience)
.build();
try {
var claims = consumer.processToClaims(jwt);
return claims.getSubject();
} catch (InvalidJwtException | MalformedClaimException e) {
throw new UmbrellaException(500,"Failed to verify JWT!").causedBy(e);
}
}
private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException { private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException {
if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
try { try {
@ -366,7 +360,7 @@ public class UserModule extends BaseHandler {
var authEndpoint = config.getString(AUTH_ENDPOINT); var authEndpoint = config.getString(AUTH_ENDPOINT);
var clientId = loginService.clientId(); var clientId = loginService.clientId();
var state = UUID.randomUUID().toString(); var state = UUID.randomUUID().toString();
stateMep.put(state, State.of(loginService, config)); stateMap.put(state, State.of(loginService, config));
return sendContent(ex,Map.of(OIDC_CALLBACK, callback, AUTH_ENDPOINT, authEndpoint, SCOPE, OIDC_SCOPE, CLIENT_ID, clientId, RESPONSE_TYPE, CODE, STATE, state)); return sendContent(ex,Map.of(OIDC_CALLBACK, callback, AUTH_ENDPOINT, authEndpoint, SCOPE, OIDC_SCOPE, CLIENT_ID, clientId, RESPONSE_TYPE, CODE, STATE, state));
} catch (UmbrellaException e){ } catch (UmbrellaException e){
return send(ex,e); return send(ex,e);
@ -471,6 +465,28 @@ public class UserModule extends BaseHandler {
} }
} }
private boolean postResetPassword(HttpExchange ex) throws IOException {
try {
var email = body(ex);
var addr = new EmailAddress(email);
var user = users.load(addr);
var token = uuid();
var oldToken = tokenMap.get(email);
if (oldToken != null) tokenMap.remove(oldToken);
tokenMap.put(token,email);
tokenMap.put(email,token);
var subject = "user.your_password_reset_token";
var content = "user.go_to_url_to_reset_password";
var fills = Map.of("token",token);
var message = new Message(user,subject,content,fills,null);
var envelope = new Envelope(message,user);
messages.send(envelope);
} catch (UmbrellaException e){
}
return sendEmptyResponse(HTTP_OK,ex);
}
private boolean patchService(HttpExchange ex, String serviceName, UmbrellaUser requestingUser) throws IOException { private boolean patchService(HttpExchange ex, String serviceName, UmbrellaUser requestingUser) throws IOException {
if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex);
try { try {
@ -513,12 +529,10 @@ public class UserModule extends BaseHandler {
return score; return score;
} }
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException { private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
var id = user.id(); var id = user.id();
var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name(); var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name();
var email = json.has(EMAIL) && json.get(EMAIL) instanceof String e && !e.isBlank() ? e : user.email(); var email = json.has(EMAIL) && json.get(EMAIL) instanceof String e && !e.isBlank() ? new EmailAddress(e) : user.email();
var pass = json.has(PASSWORD) && json.get(PASSWORD) instanceof String p && !p.isBlank() ? Password.of(BAD_HASHER.hash(p,null)) : user.hashedPassword(); var pass = json.has(PASSWORD) && json.get(PASSWORD) instanceof String p && !p.isBlank() ? Password.of(BAD_HASHER.hash(p,null)) : user.hashedPassword();
var theme = json.has(THEME) && json.get(THEME) instanceof String t && !t.isBlank() ? t : user.theme(); var theme = json.has(THEME) && json.get(THEME) instanceof String t && !t.isBlank() ? t : user.theme();
var lang = json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String l && !l.isBlank() ? l : user.language(); var lang = json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String l && !l.isBlank() ? l : user.language();
@ -526,6 +540,23 @@ public class UserModule extends BaseHandler {
return sendContent(ex,HTTP_OK,saved); return sendContent(ex,HTTP_OK,saved);
} }
private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException {
var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
var audience = state.loginService.clientId();
var httpJwks = new HttpsJwks(jwksEndpoint);
var resolver = new HttpsJwksVerificationKeyResolver(httpJwks);
JwtConsumer consumer = new JwtConsumerBuilder()
.setVerificationKeyResolver(resolver)
.setExpectedAudience(audience)
.build();
try {
var claims = consumer.processToClaims(jwt);
return claims.getSubject();
} catch (InvalidJwtException | MalformedClaimException e) {
throw new UmbrellaException(500,"Failed to verify JWT!").causedBy(e);
}
}
private static boolean weak(String password){ private static boolean weak(String password){
return score(password) < 14; return score(password) < 14;
}; };

5
user/src/main/java/de/srsoftware/umbrella/user/api/UserDb.java

@ -3,10 +3,11 @@ package de.srsoftware.umbrella.user.api;
import de.srsoftware.umbrella.core.Token; import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.user.model.DbUser; import de.srsoftware.umbrella.user.model.DbUser;
import de.srsoftware.umbrella.user.model.Password; import de.srsoftware.umbrella.user.model.Password;
import de.srsoftware.umbrella.user.model.Session; import de.srsoftware.umbrella.user.model.Session;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import java.util.List; import java.util.List;
public interface UserDb { public interface UserDb {
@ -28,6 +29,8 @@ public interface UserDb {
Session load(Token token) throws UmbrellaException; Session load(Token token) throws UmbrellaException;
UmbrellaUser load(EmailAddress email) throws UmbrellaException;
UmbrellaUser load(Long id) throws UmbrellaException; UmbrellaUser load(Long id) throws UmbrellaException;
UmbrellaUser load(Session session) throws UmbrellaException; UmbrellaUser load(Session session) throws UmbrellaException;

4
user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java

@ -7,6 +7,8 @@ import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.IMPERSONATE;
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.LIST_USERS; import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.LIST_USERS;
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.MANAGE_LOGIN_SERVICES; import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.MANAGE_LOGIN_SERVICES;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -27,7 +29,7 @@ public class DbUser extends UmbrellaUser {
private final Password hashedPass; private final Password hashedPass;
private final Long lastLogoff; private final Long lastLogoff;
public DbUser(long id, String name, String email, Password hashedPassword, String theme, String languageCode, Set<PERMISSION> permissions, Long lastLogoff) { public DbUser(long id, String name, EmailAddress email, Password hashedPassword, String theme, String languageCode, Set<PERMISSION> permissions, Long lastLogoff) {
super(id, name, email, theme, languageCode); super(id, name, email, theme, languageCode);
this.hashedPass = hashedPassword; this.hashedPass = hashedPassword;
this.permissions = permissions; this.permissions = permissions;

1
user/src/main/java/de/srsoftware/umbrella/user/model/Session.java

@ -3,6 +3,7 @@ package de.srsoftware.umbrella.user.model;
import de.srsoftware.tools.SessionToken; import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.Token; import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.time.Instant; import java.time.Instant;
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */

49
user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java

@ -1,49 +0,0 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.user.model;
import static de.srsoftware.umbrella.core.Constants.*;
import de.srsoftware.tools.Mappable;
import java.util.HashMap;
import java.util.Map;
/* © SRSoftware 2025 */
public class UmbrellaUser extends User implements Mappable {
private final long id;
private final String theme, lang;
public UmbrellaUser(long id, String name, String email, String theme, String languageCode) {
super(name,email);
this.id = id;
this.theme = theme;
this.lang = languageCode;
}
public long id(){
return id;
}
public String language(){
return lang;
}
public String theme(){
return theme;
}
@Override
public Map<String,Object> toMap() {
var map = new HashMap<String,Object>();
map.put(ID,id);
map.put(LOGIN, name()); // this is still used by old umbrella modules
map.put(NAME, name());
map.put(EMAIL,email());
map.put(THEME,theme);
map.put(LANGUAGE,lang);
return map;
}
}

59
user/src/main/java/de/srsoftware/umbrella/user/model/User.java

@ -1,59 +0,0 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.user.model;
import static de.srsoftware.tools.Optionals.nullable;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Constants.LANGUAGE;
import static java.text.MessageFormat.format;
import java.util.Objects;
import org.json.JSONObject;
public class User {
private final String email, name;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String email(){
return email;
}
public String name(){
return name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof User user)) return false;
return Objects.equals(email, user.email) && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(email, name);
}
@Override
public String toString() {
return format("{1}({0})", nullable(name()).orElse(email()),getClass().getSimpleName());
}
public static User of(JSONObject json){
if (json.has(USER)) json = json.getJSONObject(USER);
var name = json.has(NAME) ? json.getString(NAME) : null;
var email = json.has(EMAIL) ? json.getString(EMAIL) : null;
if (json.has(ID) && json.has(THEME)) {
return new UmbrellaUser(
json.getLong(ID),
name,
email,
json.getString(THEME),
json.has(LANGUAGE) ? json.getString(LANGUAGE) : null
);
}
return new User(name,email);
}
}

20
user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java

@ -14,12 +14,13 @@ import de.srsoftware.tools.PasswordHasher;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.Token; import de.srsoftware.umbrella.core.Token;
import de.srsoftware.umbrella.core.UmbrellaException; import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.user.BadHasher; import de.srsoftware.umbrella.user.BadHasher;
import de.srsoftware.umbrella.user.api.LoginServiceDb; import de.srsoftware.umbrella.user.api.LoginServiceDb;
import de.srsoftware.umbrella.user.api.UserDb; import de.srsoftware.umbrella.user.api.UserDb;
import de.srsoftware.umbrella.user.model.*; import de.srsoftware.umbrella.user.model.*;
import de.srsoftware.umbrella.user.model.Session; import de.srsoftware.umbrella.user.model.Session;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
@ -351,6 +352,21 @@ CREATE TABLE IF NOT EXISTS {0} (
return loginService; return loginService;
} }
@Override
public UmbrellaUser load(EmailAddress email) throws UmbrellaException {
if (email == null) throw new UmbrellaException(400,"Email must not be null!");
UmbrellaUser user = null;
try {
var rs = select(ALL).from(TABLE_USERS).where(EMAIL,equal(email.toString())).exec(db);
if (rs.next()) user = toUser(rs);
rs.close();
} catch (SQLException e) {
LOG.log(WARNING,"Failed to load user for \"{0}\"!",email);
}
if (user == null) throw new UmbrellaException(500,"Failed to load user for \"{0}\"!",email);
return user;
}
@Override @Override
public UmbrellaUser load(Long id) throws UmbrellaException { public UmbrellaUser load(Long id) throws UmbrellaException {
if (id == null) throw new UmbrellaException(400,"Id must not be null!"); if (id == null) throw new UmbrellaException(400,"Id must not be null!");
@ -501,7 +517,7 @@ CREATE TABLE IF NOT EXISTS {0} (
return new DbUser( return new DbUser(
id, id,
rs.getString(LOGIN), rs.getString(LOGIN),
rs.getString(EMAIL), rs.getString(EMAIL) instanceof String addr && !addr.isBlank() ? new EmailAddress(addr) : null,
Password.of(rs.getString(PASS)), Password.of(rs.getString(PASS)),
rs.getString(THEME), rs.getString(THEME),
"de", // TODO: save in DB "de", // TODO: save in DB

Loading…
Cancel
Save