Browse Source

implemented otp login

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
feature/document
Stephan Richter 4 months ago
parent
commit
92c6b154ea
  1. 7
      core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java
  2. 7
      core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java
  3. 2
      core/src/main/java/de/srsoftware/umbrella/core/model/UmbrellaUser.java
  4. 26
      frontend/src/routes/user/ResetPw.svelte
  5. 7
      messages/src/main/java/de/srsoftware/umbrella/message/MessageSystem.java
  6. 39
      messages/src/main/java/de/srsoftware/umbrella/message/model/CombinedMessage.java
  7. 16
      translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java
  8. 2
      translations/src/main/resources/de.json
  9. 2
      user/src/main/java/de/srsoftware/umbrella/user/Paths.java
  10. 21
      user/src/main/java/de/srsoftware/umbrella/user/UserModule.java
  11. 2
      user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java

7
core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java

@ -1,6 +1,11 @@ @@ -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<String,String> fills);
}

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

@ -1,15 +1,18 @@ @@ -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;
}

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

@ -79,7 +79,7 @@ public class UmbrellaUser implements Mappable { @@ -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;

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

@ -1,25 +1,46 @@ @@ -1,25 +1,46 @@
<script>
import { onMount } from 'svelte';
import { t } from '../../translations.svelte.js';
import { useTinyRouter } from 'svelte-tiny-router';
let mail = "";
let caption = t('user.send_mail');
let error = null;
const router = useTinyRouter();
async function submit(ev){
ev.preventDefault();
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) {
caption = t('user.data_sent');
} else {
caption = await resp.text();
}
}
async function checkToken(){
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
console.log({token:token,params:urlParams});
if (token) {
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/validate/${token}`;
const resp = await fetch(url,{
credentials: 'include'
});
if (resp.ok){
router.navigate('/user')
} else {
error = await resp.text();
}
}
}
onMount(checkToken);
</script>
@ -44,5 +65,8 @@ @@ -44,5 +65,8 @@
{t('user.enter_email')}
</label>
<button type="submit">{caption}</button>
{#if error}
<div class="error">{error}</div>
{/if}
</fieldset>
</form>

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

@ -24,6 +24,7 @@ import jakarta.mail.internet.MimeMultipart; @@ -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 { @@ -113,9 +114,9 @@ public class MessageSystem implements PostBox {
var date = new Date();
for (var receiver : dueRecipients){
Function<String,String> translateFunction = receiver instanceof UmbrellaUser uu ? text -> translator.translate(uu.language(),text) : text -> text;
// combine messages for user
var combined = new CombinedMessage(translateFunction);
BiFunction<String,Map<String,String>,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());

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

@ -6,43 +6,46 @@ import static java.lang.System.Logger.Level.TRACE; @@ -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<Attachment> attachments = new HashSet<>();
private final StringBuilder body = new StringBuilder();
private final StringBuilder combinedBody = new StringBuilder();
private String combinedSubject = null;
private final List<Message> mergedMessages = new ArrayList<>();
private final Function<String, String> translate;
private final String subjectForCombinedMessage;
private final BiFunction<String,Map<String,String>,String> translate;
private UmbrellaUser sender = null;
private String subject = null;
public CombinedMessage(Function<String,String> translate){
public CombinedMessage(String subjectForCombinedMessage, BiFunction<String, Map<String,String>,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 { @@ -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 { @@ -61,7 +64,7 @@ public class CombinedMessage {
}
public String subject() {
return subject;
return combinedSubject;
}
public List<Message> messages() {

16
translations/src/main/java/de/srsoftware/umbrella/translations/Translations.java

@ -12,6 +12,7 @@ import java.io.ByteArrayOutputStream; @@ -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 { @@ -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 { @@ -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<String,String> 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;
}

2
translations/src/main/resources/de.json

@ -58,6 +58,7 @@ @@ -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 @@ @@ -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"
}
}

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

@ -18,6 +18,6 @@ public class Paths { @@ -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";
}

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

@ -158,6 +158,7 @@ public class UserModule extends BaseHandler { @@ -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 { @@ -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 { @@ -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();

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

@ -511,7 +511,7 @@ CREATE TABLE IF NOT EXISTS {0} ( @@ -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<DbUser.PERMISSION> perms = id == 1 ? ADMIN_PERMISSIONS : Set.of();
return new DbUser(

Loading…
Cancel
Save