diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java b/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java index a1b0d04..a055f2b 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java @@ -1,6 +1,11 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.api; +import java.util.Map; + public interface Translator { - public String translate(String language, String text); + public default String translate(String language, String text){ + return translate(language, text, Map.of()); + } + public String translate(String language, String text, Map fills); } 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 index 2137f1f..10451de 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java @@ -1,15 +1,18 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; +import de.srsoftware.umbrella.core.UmbrellaException; + import static de.srsoftware.tools.Optionals.allSet; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.text.MessageFormat.format; public class EmailAddress { private final String email; - public EmailAddress(String addr){ + public EmailAddress(String addr) throws UmbrellaException { 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)); + if (parts.length != 2 || !allSet(parts[0],parts[1])) throw new UmbrellaException(HTTP_BAD_REQUEST,"\"{0}\" is not a valid email address",addr); email = addr; } 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 index f48a135..22fb504 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java @@ -79,7 +79,7 @@ public class UmbrellaUser implements Mappable { 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(EMAIL,email() instanceof EmailAddress ea ? ea.toString() : null); 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 6dd254b..da762fd 100644 --- a/frontend/src/routes/user/ResetPw.svelte +++ b/frontend/src/routes/user/ResetPw.svelte @@ -1,25 +1,46 @@ @@ -44,5 +65,8 @@ {t('user.enter_email')} + {#if error} +
{error}
+ {/if} \ No newline at end of file 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 a86dcaf..aef7911 100644 --- a/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java +++ b/messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java @@ -24,6 +24,7 @@ import jakarta.mail.internet.MimeMultipart; import jakarta.mail.util.ByteArrayDataSource; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; import java.util.function.Function; public class MessageSystem implements PostBox { @@ -113,9 +114,9 @@ public class MessageSystem implements PostBox { var date = new Date(); for (var receiver : dueRecipients){ - Function translateFunction = receiver instanceof UmbrellaUser uu ? text -> translator.translate(uu.language(),text) : text -> text; - // combine messages for user - var combined = new CombinedMessage(translateFunction); + BiFunction,String> translateFunction = (text,fills) -> translator.translate(receiver.language(),text,fills); + + var combined = new CombinedMessage("Collected messages",translateFunction); var envelopes = queue.stream().filter(env -> env.isFor(receiver)).toList(); for (var envelope : envelopes) combined.merge(envelope.message()); 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 4f57b13..0fe80a7 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 @@ -6,43 +6,46 @@ import static java.lang.System.Logger.Level.TRACE; import static java.text.MessageFormat.format; import de.srsoftware.umbrella.core.model.UmbrellaUser; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; + +import java.util.*; +import java.util.function.BiFunction; import java.util.function.Function; public class CombinedMessage { private static final System.Logger LOG = System.getLogger(CombinedMessage.class.getSimpleName()); private final Set attachments = new HashSet<>(); - private final StringBuilder body = new StringBuilder(); + private final StringBuilder combinedBody = new StringBuilder(); + private String combinedSubject = null; private final List mergedMessages = new ArrayList<>(); - private final Function translate; + private final String subjectForCombinedMessage; + private final BiFunction,String> translate; private UmbrellaUser sender = null; - private String subject = null; - public CombinedMessage(Function translate){ + public CombinedMessage(String subjectForCombinedMessage, BiFunction,String> translateFunction){ LOG.log(DEBUG,"Creating combined message…"); - this.translate = translate; + this.subjectForCombinedMessage = subjectForCombinedMessage; + translate = translateFunction; } public void merge(Message message) { LOG.log(TRACE,"Merging {0} into combined message…",message); + var body = translate.apply(message.body(),message.fills()); + var subject = translate.apply(message.subject(),message.fills()); switch (mergedMessages.size()){ case 0: - body.append(message.body()); + combinedBody.append(body); sender = message.sender(); - subject = message.subject(); + combinedSubject = subject; break; case 1: - body.insert(0,format("# {0}:\n# {1}:\n\n",sender,subject)); // insert sender and subject of first message right before the body of the first message - subject = translate.apply("Collected messages"); + combinedBody.insert(0,format("# {0}:\n# {1}:\n\n",sender,subject)); // insert sender and subject of first message right before the body of the first message + combinedSubject = subjectForCombinedMessage; // no break here, we need to append the subject and content default: - body.append("\n\n# ").append(message.sender()).append(":\n"); - body.append("# ").append(message.subject()).append(":\n\n"); - body.append(message.body()); + combinedBody.append("\n\n# ").append(message.sender()).append(":\n"); + combinedBody.append("# ").append(subject).append(":\n\n"); + combinedBody.append(body); } if (message.attachments() != null) attachments.addAll(message.attachments()); mergedMessages.add(message); @@ -53,7 +56,7 @@ public class CombinedMessage { } public String body() { - return body.toString(); + return combinedBody.toString(); } public UmbrellaUser sender() { @@ -61,7 +64,7 @@ public class CombinedMessage { } public String subject() { - return subject; + return combinedSubject; } public List messages() { diff --git a/translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java b/translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java index caab7e8..1442592 100644 --- a/translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java +++ b/translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java @@ -12,6 +12,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.util.HashMap; +import java.util.Map; import org.json.JSONObject; public class Translations extends PathHandler implements Translator { @@ -34,7 +35,7 @@ public class Translations extends PathHandler implements Translator { } private JSONObject loadTranslations(String lang) throws IOException { - LOG.log(WARNING,"loadTranslations({0} not implemented!",lang); + LOG.log(WARNING,"loadTranslations({0}) not implemented!",lang); var filename = lang + ".json"; URL url = getClass().getClassLoader().getResource(filename); if (url == null) return new JSONObject(); @@ -50,10 +51,19 @@ public class Translations extends PathHandler implements Translator { } @Override - public String translate(String language, String text) { + public String translate(String language, String text, Map fills) { try { var translations = getTranslations(language); - return translations.has(text) ? translations.getString(text) : text; + var keys = text.split("\\."); + Object current = translations; + for (var key : keys){ + if (current instanceof JSONObject json && json.has(key)) { + current = json.get(key); + } else current = null; + } + var translated = current instanceof String translation ? translation : text; + for (var entry : fills.entrySet()) translated = translated.replaceAll("\\{"+entry.getKey()+"}",entry.getValue()); + return translated; } catch (IOException e) { return text; } diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index fe62cd7..dabf03a 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -58,6 +58,7 @@ "email": "E-Mail", "failed": "fehlgeschlagen", "foreign_id": "externe Kennung", + "go_to_url_to_reset_password": "Um ein neues Passwort zu erhalten, öffnen Sie bitte den folgenden Link: {url}", "id": "Id", "impersonate": "zu Nutzer wechseln", "IMPERSONATE": "NUTZER WECHSELN", @@ -88,6 +89,7 @@ "update": "aktualisieren", "UPDATE_USERS" : "Nutzer aktualisieren", "user_module" : "Umbrella User-Verwaltung", + "your_password_reset_token" : "Ihr Token zum Erstellen eines neuen Passworts", "your_profile": "dein Profil" } } \ No newline at end of file diff --git a/user/src/main/java/de/srsoftware/umbrella/user/Paths.java b/user/src/main/java/de/srsoftware/umbrella/user/Paths.java index 72226a6..9d8589d 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/Paths.java +++ b/user/src/main/java/de/srsoftware/umbrella/user/Paths.java @@ -18,6 +18,6 @@ public class Paths { 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 VALIDATE_TOKEN = "validateToken"; + public static final String VALIDATE_TOKEN = "validate"; public static final String WHOAMI = "whoami"; } diff --git a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java index 9a427fb..549f0a5 100644 --- a/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java +++ b/user/src/main/java/de/srsoftware/umbrella/user/UserModule.java @@ -158,6 +158,7 @@ public class UserModule extends BaseHandler { case LIST: return getUserList(ex, user); case LOGOUT: return logout(ex, sessionToken); case OIDC: return getOIDC(ex,user,path); + case VALIDATE_TOKEN: return validateToken(ex,path.pop()); case WHOAMI: return getUser(ex, user); }; @@ -477,12 +478,13 @@ public class UserModule extends BaseHandler { 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 url = url(ex).replace("/api/user/reset_pw","/user/reset/pw")+"?token="+token; + var fills = Map.of("url",url); var message = new Message(user,subject,content,fills,null); var envelope = new Envelope(message,user); messages.send(envelope); } catch (UmbrellaException e){ - + return send(ex,e); } return sendEmptyResponse(HTTP_OK,ex); } @@ -540,6 +542,21 @@ public class UserModule extends BaseHandler { return sendContent(ex,HTTP_OK,saved); } + private boolean validateToken(HttpExchange ex, String token) throws IOException { + if (token == null) return sendContent(ex,HTTP_UNPROCESSABLE,"No token provided!"); + var email = tokenMap.get(token); + tokenMap.remove(token); + if (email == null) return sendContent(ex,HTTP_UNAUTHORIZED,"Unknown token!"); + try { + var user = users.load(new EmailAddress(email)); + users.getSession(user).cookie().addTo(ex); + return sendContent(ex,user); + } catch (UmbrellaException e) { + return send(ex,e); + } + } + + private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException { var jwksEndpoint = state.config.getString(JWKS_ENDPOINT); var audience = state.loginService.clientId(); 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 495b29f..149e662 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 @@ -511,7 +511,7 @@ CREATE TABLE IF NOT EXISTS {0} ( ); } - private DbUser toUser(ResultSet rs) throws SQLException { + private DbUser toUser(ResultSet rs) throws SQLException, UmbrellaException { long id = rs.getLong(ID); Set perms = id == 1 ? ADMIN_PERMISSIONS : Set.of(); return new DbUser(