From 7a5bb50ee2a3c6fef1cc8ed8138b5decf1d51d62 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 8 Jul 2025 15:39:48 +0200 Subject: [PATCH] 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 --- .../umbrella/backend/Application.java | 8 +- core/build.gradle.kts | 1 + .../srsoftware/umbrella/core/Constants.java | 1 + .../umbrella/core/model/EmailAddress.java | 20 +++++ .../umbrella/core/model/UmbrellaUser.java | 87 +++++++++++++++++++ frontend/src/routes/user/ResetPw.svelte | 11 ++- .../srsoftware/umbrella/legacy/LegacyApi.java | 2 +- messages/build.gradle.kts | 1 - .../umbrella/message/MessageDb.java | 2 +- .../umbrella/message/MessageSystem.java | 18 ++-- .../umbrella/message/SqliteMessageDb.java | 2 +- .../message/model/CombinedMessage.java | 4 +- .../umbrella/message/model/Envelope.java | 27 ++++-- .../umbrella/message/model/Message.java | 14 ++- user/build.gradle.kts | 1 + .../srsoftware/umbrella/user/Constants.java | 2 - .../de/srsoftware/umbrella/user/Paths.java | 1 + .../srsoftware/umbrella/user/UserModule.java | 83 ++++++++++++------ .../srsoftware/umbrella/user/api/UserDb.java | 5 +- .../umbrella/user/model/DbUser.java | 4 +- .../umbrella/user/model/Session.java | 1 + .../umbrella/user/model/UmbrellaUser.java | 49 ----------- .../srsoftware/umbrella/user/model/User.java | 59 ------------- .../umbrella/user/sqlite/SqliteDB.java | 20 ++++- 24 files changed, 245 insertions(+), 178 deletions(-) create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java create mode 100644 core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java delete mode 100644 user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java delete mode 100644 user/src/main/java/de/srsoftware/umbrella/user/model/User.java diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index c5412bb..613cd0b 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -51,11 +51,11 @@ public class Application { var connectionProvider = new ConnectionProvider(); var messageDb = new SqliteMessageDb(connectionProvider.get(messageDbFile)); 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(); - var messageSystem = new MessageSystem(messageDb,translationModule,config.subset("umbrella.modules.message").orElseThrow()); + var messageSystem = new MessageSystem(messageDb,translationModule,config.subset("umbrella.modules.message").orElseThrow()); var server = HttpServer.create(new InetSocketAddress(port), 0); server.setExecutor(Executors.newFixedThreadPool(threads)); @@ -63,7 +63,7 @@ public class Application { new LegacyApi(userDb,config).bindPath("/legacy").on(server); new MessageApi(messageSystem).bindPath("/api/messages").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); server.start(); LOG.log(INFO,"Started web server at {0}",port); diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5a137ce..17fa00a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,6 +12,7 @@ repositories { dependencies { implementation("de.srsoftware:tools.mime:1.1.2") implementation("de.srsoftware:tools.optionals:1.0.0") + implementation("de.srsoftware:tools.util:2.0.3") implementation("org.json:json:20240303") implementation("org.xerial:sqlite-jdbc:3.49.0.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index da71c2e..256dcf6 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/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 NUMBER = "number"; public static final String OPTIONAL = "optional"; + public static final String PASS = "pass"; public static final String PASSWORD = "password"; public static final String POST = "POST"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java new file mode 100644 index 0000000..2137f1f --- /dev/null +++ b/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; + } +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java new file mode 100644 index 0000000..f48a135 --- /dev/null +++ b/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 toMap() { + var map = new HashMap(); + 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; + } +} diff --git a/frontend/src/routes/user/ResetPw.svelte b/frontend/src/routes/user/ResetPw.svelte index 0c5fc30..6dd254b 100644 --- a/frontend/src/routes/user/ResetPw.svelte +++ b/frontend/src/routes/user/ResetPw.svelte @@ -8,7 +8,16 @@ async function submit(ev){ 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 { + } + } diff --git a/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java b/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java index 83d0f8f..ad55a93 100644 --- a/legacy/src/main/java/de/srsoftware/umbrella/legacy/LegacyApi.java +++ b/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.Token; 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.UmbrellaUser; import de.srsoftware.umbrella.user.sqlite.SqliteDB; import java.io.IOException; import java.time.Instant; diff --git a/messages/build.gradle.kts b/messages/build.gradle.kts index 5853338..5fd55ef 100644 --- a/messages/build.gradle.kts +++ b/messages/build.gradle.kts @@ -2,7 +2,6 @@ description = "Umbrella : Message subsystem" dependencies{ implementation(project(":core")) - implementation(project(":user")) implementation("com.sun.mail:jakarta.mail:2.0.1") implementation("de.srsoftware:configuration.api:1.0.2") implementation("de.srsoftware:tools.jdbc:1.3.2") diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java b/messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java index 0499721..5270ddf 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java +++ b/messages/src/main/java/de/srsoftware/umbrella/message/MessageDb.java @@ -2,8 +2,8 @@ package de.srsoftware.umbrella.message; import de.srsoftware.umbrella.core.UmbrellaException; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.message.model.Settings; -import de.srsoftware.umbrella.user.model.UmbrellaUser; public interface MessageDb { public Settings getSettings(UmbrellaUser user) throws UmbrellaException; diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java b/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java index 35863ce..a86dcaf 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java +++ b/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.umbrella.core.Constants.*; import static de.srsoftware.umbrella.message.Constants.*; -import static de.srsoftware.umbrella.user.Constants.PASS; import static java.lang.System.Logger.Level.*; import de.srsoftware.configuration.Configuration; import de.srsoftware.umbrella.core.UmbrellaException; 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.Envelope; 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.mail.Message; import jakarta.mail.MessagingException; @@ -31,7 +29,7 @@ import java.util.function.Function; public class MessageSystem implements PostBox { public static final System.Logger LOG = System.getLogger(MessageSystem.class.getSimpleName()); 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{ @@ -98,11 +96,11 @@ public class MessageSystem implements PostBox { private synchronized void processMessages(Integer scheduledHour) { LOG.log(INFO,"Running {0}…",scheduledHour == null ? "instantly" : "scheduled at "+scheduledHour); var queue = new ArrayList<>(this.queue); - var dueRecipients = new ArrayList(); - List recipients = queue.stream().map(Envelope::receivers).flatMap(Set::stream).filter(Objects::nonNull).distinct().toList(); + var dueRecipients = new ArrayList(); + List 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 (User recv : recipients) { + for (UmbrellaUser recv : recipients) { if (recv instanceof UmbrellaUser uu) { try { 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); session = session(); MimeMessage msg = new MimeMessage(session); msg.addHeader(CONTENT_TYPE, "text/markdown; charset=UTF-8"); msg.addHeader("format", "flowed"); msg.addHeader("Content-Transfer-Encoding", "8bit"); - msg.setFrom(message.sender().email()); + msg.setFrom(message.sender().email().toString()); msg.setSubject(message.subject(), UTF8); msg.setSentDate(date); - var toEmail = debugAddress != null ? debugAddress : receiver.email(); + var toEmail = debugAddress != null ? debugAddress : receiver.email().toString(); msg.setRecipients(Message.RecipientType.TO, toEmail); if (message.attachments().isEmpty()){ diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/SqliteMessageDb.java b/messages/src/main/java/de/srsoftware/umbrella/message/SqliteMessageDb.java index e578a3a..15079eb 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/SqliteMessageDb.java +++ b/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 de.srsoftware.umbrella.core.UmbrellaException; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.message.model.Settings; -import de.srsoftware.umbrella.user.model.UmbrellaUser; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java b/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java index 036389b..4f57b13 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java +++ b/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.text.MessageFormat.format; -import de.srsoftware.umbrella.user.model.UmbrellaUser; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -44,7 +44,7 @@ public class CombinedMessage { body.append("# ").append(message.subject()).append(":\n\n"); body.append(message.body()); } - attachments.addAll(message.attachments()); + if (message.attachments() != null) attachments.addAll(message.attachments()); mergedMessages.add(message); } diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/model/Envelope.java b/messages/src/main/java/de/srsoftware/umbrella/message/model/Envelope.java index 8861be3..085aee1 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/model/Envelope.java +++ b/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 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.List; import java.util.Set; @@ -17,28 +18,38 @@ import org.json.JSONObject; public class Envelope { private Message message; - private Set receivers; + private Set receivers; - public Envelope(Message message, HashSet receivers) { + public Envelope(Message message, UmbrellaUser receiver){ + this(message,new HashSet<>(Set.of(receiver))); + } + + public Envelope(Message message, HashSet receivers) { this.message = message; this.receivers = receivers; } + /** + * TODO: this is legacy, move to legacy module! + * @param json + * @return + * @throws UmbrellaException + */ public static Envelope from(JSONObject json) throws UmbrellaException { if (!json.has(RECEIVERS)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,RECEIVERS); var message = Message.from(json); var obj = json.get(RECEIVERS); if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj)); if (!(obj instanceof JSONArray receiverList)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,RECEIVERS,JSONARRAY); - var receivers = new HashSet(); + var receivers = new HashSet(); for (var o : receiverList){ 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); } - public boolean isFor(User receiver) { + public boolean isFor(UmbrellaUser receiver) { return receivers.contains(receiver); } @@ -46,12 +57,12 @@ public class Envelope { return message; } - public Set receivers(){ + public Set receivers(){ return receivers; } @Override 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()); } } diff --git a/messages/src/main/java/de/srsoftware/umbrella/message/model/Message.java b/messages/src/main/java/de/srsoftware/umbrella/message/model/Message.java index 39766b4..6ab76e3 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/model/Message.java +++ b/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 de.srsoftware.umbrella.core.UmbrellaException; -import de.srsoftware.umbrella.user.model.UmbrellaUser; -import de.srsoftware.umbrella.user.model.User; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.*; import org.json.JSONArray; import org.json.JSONObject; -public record Message(UmbrellaUser sender, String subject, String body, List attachments) { +public record Message(UmbrellaUser sender, String subject, String body, Map fills, List attachments) { public static Message from(JSONObject json) throws UmbrellaException { for (var key : Set.of(SENDER, SUBJECT, BODY)) { 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(); if (json.has(ATTACHMENTS)){ @@ -39,7 +35,7 @@ public record Message(UmbrellaUser sender, String subject, String body, List stateMep = new HashMap<>(); // map from state to OIDC provider name + private final HashMap stateMap = new HashMap<>(); // map from state to OIDC provider name + private final HashMap tokenMap = new HashMap<>(); + private final MessageSystem messages; static { try { @@ -67,9 +76,10 @@ public class UserModule extends BaseHandler { } } - public UserModule(UserDb userDb, LoginServiceDb loginDb){ - users = userDb; - logins = loginDb; + public UserModule(UserDb userDb, LoginServiceDb loginDb, MessageSystem messageSystem){ + logins = loginDb; + messages = messageSystem; + users = userDb; } @@ -234,6 +244,7 @@ public class UserModule extends BaseHandler { case OIDC: return postOIDC(ex,path); case IMPERSONATE: return impersonate(ex,targetId); case LOGIN: return postLogin(ex); + case RESET_PW: return postResetPassword(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(STATE)) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted"); 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"); var redirect = url(ex).replaceAll("/api/.*",""); 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 { if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex); try { @@ -366,7 +360,7 @@ public class UserModule extends BaseHandler { var authEndpoint = config.getString(AUTH_ENDPOINT); var clientId = loginService.clientId(); 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)); } catch (UmbrellaException 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 { if (!(requestingUser instanceof DbUser user && user.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex); try { @@ -513,12 +529,10 @@ public class UserModule extends BaseHandler { return score; } - - private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException { var id = user.id(); 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 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(); @@ -526,6 +540,23 @@ public class UserModule extends BaseHandler { 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){ return score(password) < 14; }; diff --git a/user/src/main/java/de/srsoftware/umbrella/user/api/UserDb.java b/user/src/main/java/de/srsoftware/umbrella/user/api/UserDb.java index fd8d02d..75959d6 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/api/UserDb.java +++ b/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.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.Password; import de.srsoftware.umbrella.user.model.Session; -import de.srsoftware.umbrella.user.model.UmbrellaUser; import java.util.List; public interface UserDb { @@ -28,6 +29,8 @@ public interface UserDb { Session load(Token token) throws UmbrellaException; + UmbrellaUser load(EmailAddress email) throws UmbrellaException; + UmbrellaUser load(Long id) throws UmbrellaException; UmbrellaUser load(Session session) throws UmbrellaException; diff --git a/user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java b/user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java index 6ceba28..d366fde 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java +++ b/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.MANAGE_LOGIN_SERVICES; +import de.srsoftware.umbrella.core.model.EmailAddress; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.util.Map; import java.util.Set; @@ -27,7 +29,7 @@ public class DbUser extends UmbrellaUser { private final Password hashedPass; private final Long lastLogoff; - public DbUser(long id, String name, String email, Password hashedPassword, String theme, String languageCode, Set permissions, Long lastLogoff) { + public DbUser(long id, String name, EmailAddress email, Password hashedPassword, String theme, String languageCode, Set permissions, Long lastLogoff) { super(id, name, email, theme, languageCode); this.hashedPass = hashedPassword; this.permissions = permissions; diff --git a/user/src/main/java/de/srsoftware/umbrella/user/model/Session.java b/user/src/main/java/de/srsoftware/umbrella/user/model/Session.java index f42b32a..a0c3c0b 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/model/Session.java +++ b/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.umbrella.core.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.time.Instant; /* © SRSoftware 2025 */ diff --git a/user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java b/user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java deleted file mode 100644 index affd904..0000000 --- a/user/src/main/java/de/srsoftware/umbrella/user/model/UmbrellaUser.java +++ /dev/null @@ -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 toMap() { - var map = new HashMap(); - 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; - } -} diff --git a/user/src/main/java/de/srsoftware/umbrella/user/model/User.java b/user/src/main/java/de/srsoftware/umbrella/user/model/User.java deleted file mode 100644 index 0f88304..0000000 --- a/user/src/main/java/de/srsoftware/umbrella/user/model/User.java +++ /dev/null @@ -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); - } -} diff --git a/user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java b/user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java index c6224f7..495b29f 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java +++ b/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.umbrella.core.Token; 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.api.LoginServiceDb; import de.srsoftware.umbrella.user.api.UserDb; import de.srsoftware.umbrella.user.model.*; import de.srsoftware.umbrella.user.model.Session; -import de.srsoftware.umbrella.user.model.UmbrellaUser; import java.security.NoSuchAlgorithmException; import java.sql.Connection; import java.sql.ResultSet; @@ -351,6 +352,21 @@ CREATE TABLE IF NOT EXISTS {0} ( 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 public UmbrellaUser load(Long id) throws UmbrellaException { 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( id, rs.getString(LOGIN), - rs.getString(EMAIL), + rs.getString(EMAIL) instanceof String addr && !addr.isBlank() ? new EmailAddress(addr) : null, Password.of(rs.getString(PASS)), rs.getString(THEME), "de", // TODO: save in DB