implemented otp login
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.core.api;
|
package de.srsoftware.umbrella.core.api;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface Translator {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.core.model;
|
package de.srsoftware.umbrella.core.model;
|
||||||
|
|
||||||
|
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||||
|
|
||||||
import static de.srsoftware.tools.Optionals.allSet;
|
import static de.srsoftware.tools.Optionals.allSet;
|
||||||
|
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
|
|
||||||
public class EmailAddress {
|
public class EmailAddress {
|
||||||
private final String email;
|
private final String email;
|
||||||
|
|
||||||
public EmailAddress(String addr){
|
public EmailAddress(String addr) throws UmbrellaException {
|
||||||
var parts = addr.split("@");
|
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;
|
email = addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class UmbrellaUser implements Mappable {
|
|||||||
map.put(ID,id);
|
map.put(ID,id);
|
||||||
map.put(LOGIN, name()); // this is still used by old umbrella modules
|
map.put(LOGIN, name()); // this is still used by old umbrella modules
|
||||||
map.put(NAME, name());
|
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(THEME,theme);
|
||||||
map.put(LANGUAGE,lang);
|
map.put(LANGUAGE,lang);
|
||||||
return map;
|
return map;
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
import { useTinyRouter } from 'svelte-tiny-router';
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
let mail = "";
|
let mail = "";
|
||||||
let caption = t('user.send_mail');
|
let caption = t('user.send_mail');
|
||||||
|
let error = null;
|
||||||
const router = useTinyRouter();
|
const router = useTinyRouter();
|
||||||
|
|
||||||
async function submit(ev){
|
async function submit(ev){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/reset_pw`;
|
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/reset_pw`;
|
||||||
caption = t('user.data_sent');
|
|
||||||
const resp = await fetch(url,{
|
const resp = await fetch(url,{
|
||||||
method : 'POST',
|
method : 'POST',
|
||||||
body : mail
|
body : mail
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
|
caption = t('user.data_sent');
|
||||||
} else {
|
} 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>
|
</script>
|
||||||
@@ -44,5 +65,8 @@
|
|||||||
{t('user.enter_email')}
|
{t('user.enter_email')}
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">{caption}</button>
|
<button type="submit">{caption}</button>
|
||||||
|
{#if error}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
@@ -24,6 +24,7 @@ import jakarta.mail.internet.MimeMultipart;
|
|||||||
import jakarta.mail.util.ByteArrayDataSource;
|
import jakarta.mail.util.ByteArrayDataSource;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class MessageSystem implements PostBox {
|
public class MessageSystem implements PostBox {
|
||||||
@@ -113,9 +114,9 @@ public class MessageSystem implements PostBox {
|
|||||||
var date = new Date();
|
var date = new Date();
|
||||||
|
|
||||||
for (var receiver : dueRecipients){
|
for (var receiver : dueRecipients){
|
||||||
Function<String,String> translateFunction = receiver instanceof UmbrellaUser uu ? text -> translator.translate(uu.language(),text) : text -> text;
|
BiFunction<String,Map<String,String>,String> translateFunction = (text,fills) -> translator.translate(receiver.language(),text,fills);
|
||||||
// combine messages for user
|
|
||||||
var combined = new CombinedMessage(translateFunction);
|
var combined = new CombinedMessage("Collected messages",translateFunction);
|
||||||
var envelopes = queue.stream().filter(env -> env.isFor(receiver)).toList();
|
var envelopes = queue.stream().filter(env -> env.isFor(receiver)).toList();
|
||||||
for (var envelope : envelopes) combined.merge(envelope.message());
|
for (var envelope : envelopes) combined.merge(envelope.message());
|
||||||
|
|
||||||
|
|||||||
@@ -6,43 +6,46 @@ import static java.lang.System.Logger.Level.TRACE;
|
|||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
|
|
||||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.function.BiFunction;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class CombinedMessage {
|
public class CombinedMessage {
|
||||||
private static final System.Logger LOG = System.getLogger(CombinedMessage.class.getSimpleName());
|
private static final System.Logger LOG = System.getLogger(CombinedMessage.class.getSimpleName());
|
||||||
|
|
||||||
private final Set<Attachment> attachments = new HashSet<>();
|
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 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 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…");
|
LOG.log(DEBUG,"Creating combined message…");
|
||||||
this.translate = translate;
|
this.subjectForCombinedMessage = subjectForCombinedMessage;
|
||||||
|
translate = translateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void merge(Message message) {
|
public void merge(Message message) {
|
||||||
LOG.log(TRACE,"Merging {0} into combined 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()){
|
switch (mergedMessages.size()){
|
||||||
case 0:
|
case 0:
|
||||||
body.append(message.body());
|
combinedBody.append(body);
|
||||||
sender = message.sender();
|
sender = message.sender();
|
||||||
subject = message.subject();
|
combinedSubject = subject;
|
||||||
break;
|
break;
|
||||||
case 1:
|
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
|
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
|
||||||
subject = translate.apply("Collected messages");
|
combinedSubject = subjectForCombinedMessage;
|
||||||
// no break here, we need to append the subject and content
|
// no break here, we need to append the subject and content
|
||||||
default:
|
default:
|
||||||
body.append("\n\n# ").append(message.sender()).append(":\n");
|
combinedBody.append("\n\n# ").append(message.sender()).append(":\n");
|
||||||
body.append("# ").append(message.subject()).append(":\n\n");
|
combinedBody.append("# ").append(subject).append(":\n\n");
|
||||||
body.append(message.body());
|
combinedBody.append(body);
|
||||||
}
|
}
|
||||||
if (message.attachments() != null) attachments.addAll(message.attachments());
|
if (message.attachments() != null) attachments.addAll(message.attachments());
|
||||||
mergedMessages.add(message);
|
mergedMessages.add(message);
|
||||||
@@ -53,7 +56,7 @@ public class CombinedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String body() {
|
public String body() {
|
||||||
return body.toString();
|
return combinedBody.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UmbrellaUser sender() {
|
public UmbrellaUser sender() {
|
||||||
@@ -61,7 +64,7 @@ public class CombinedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String subject() {
|
public String subject() {
|
||||||
return subject;
|
return combinedSubject;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Message> messages() {
|
public List<Message> messages() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class Translations extends PathHandler implements Translator {
|
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 {
|
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";
|
var filename = lang + ".json";
|
||||||
URL url = getClass().getClassLoader().getResource(filename);
|
URL url = getClass().getClassLoader().getResource(filename);
|
||||||
if (url == null) return new JSONObject();
|
if (url == null) return new JSONObject();
|
||||||
@@ -50,10 +51,19 @@ public class Translations extends PathHandler implements Translator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String translate(String language, String text) {
|
public String translate(String language, String text, Map<String,String> fills) {
|
||||||
try {
|
try {
|
||||||
var translations = getTranslations(language);
|
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) {
|
} catch (IOException e) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"failed": "fehlgeschlagen",
|
"failed": "fehlgeschlagen",
|
||||||
"foreign_id": "externe Kennung",
|
"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",
|
"id": "Id",
|
||||||
"impersonate": "zu Nutzer wechseln",
|
"impersonate": "zu Nutzer wechseln",
|
||||||
"IMPERSONATE": "NUTZER WECHSELN",
|
"IMPERSONATE": "NUTZER WECHSELN",
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
"update": "aktualisieren",
|
"update": "aktualisieren",
|
||||||
"UPDATE_USERS" : "Nutzer aktualisieren",
|
"UPDATE_USERS" : "Nutzer aktualisieren",
|
||||||
"user_module" : "Umbrella User-Verwaltung",
|
"user_module" : "Umbrella User-Verwaltung",
|
||||||
|
"your_password_reset_token" : "Ihr Token zum Erstellen eines neuen Passworts",
|
||||||
"your_profile": "dein Profil"
|
"your_profile": "dein Profil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,6 @@ public class Paths {
|
|||||||
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 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 = "validate";
|
||||||
public static final String WHOAMI = "whoami";
|
public static final String WHOAMI = "whoami";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ public class UserModule extends BaseHandler {
|
|||||||
case LIST: return getUserList(ex, user);
|
case LIST: return getUserList(ex, user);
|
||||||
case LOGOUT: return logout(ex, sessionToken);
|
case LOGOUT: return logout(ex, sessionToken);
|
||||||
case OIDC: return getOIDC(ex,user,path);
|
case OIDC: return getOIDC(ex,user,path);
|
||||||
|
case VALIDATE_TOKEN: return validateToken(ex,path.pop());
|
||||||
case WHOAMI: return getUser(ex, user);
|
case WHOAMI: return getUser(ex, user);
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -477,12 +478,13 @@ public class UserModule extends BaseHandler {
|
|||||||
tokenMap.put(email,token);
|
tokenMap.put(email,token);
|
||||||
var subject = "user.your_password_reset_token";
|
var subject = "user.your_password_reset_token";
|
||||||
var content = "user.go_to_url_to_reset_password";
|
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 message = new Message(user,subject,content,fills,null);
|
||||||
var envelope = new Envelope(message,user);
|
var envelope = new Envelope(message,user);
|
||||||
messages.send(envelope);
|
messages.send(envelope);
|
||||||
} catch (UmbrellaException e){
|
} catch (UmbrellaException e){
|
||||||
|
return send(ex,e);
|
||||||
}
|
}
|
||||||
return sendEmptyResponse(HTTP_OK,ex);
|
return sendEmptyResponse(HTTP_OK,ex);
|
||||||
}
|
}
|
||||||
@@ -540,6 +542,21 @@ public class UserModule extends BaseHandler {
|
|||||||
return sendContent(ex,HTTP_OK,saved);
|
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 {
|
private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException {
|
||||||
var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
|
var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
|
||||||
var audience = state.loginService.clientId();
|
var audience = state.loginService.clientId();
|
||||||
|
|||||||
@@ -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);
|
long id = rs.getLong(ID);
|
||||||
Set<DbUser.PERMISSION> perms = id == 1 ? ADMIN_PERMISSIONS : Set.of();
|
Set<DbUser.PERMISSION> perms = id == 1 ? ADMIN_PERMISSIONS : Set.of();
|
||||||
return new DbUser(
|
return new DbUser(
|
||||||
|
|||||||
Reference in New Issue
Block a user