working on messagesystem, password reset form
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -53,13 +53,16 @@ public class Application {
|
|||||||
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 messageSystem = new MessageSystem(messageDb);
|
var translationModule = new Translations();
|
||||||
|
|
||||||
|
var messageSystem = new MessageSystem(messageDb,translationModule,config.subset("umbrella.modules.message").orElseThrow());
|
||||||
var server = HttpServer.create(new InetSocketAddress(port), 0);
|
var server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
server.setExecutor(Executors.newFixedThreadPool(threads));
|
server.setExecutor(Executors.newFixedThreadPool(threads));
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
new Translations().bindPath("/api/translations").on(server);
|
translationModule.bindPath("/api/translations").on(server);
|
||||||
new UserModule(userDb,loginServicedb).bindPath("/api/user").on(server);
|
new UserModule(userDb,loginServicedb).bindPath("/api/user").on(server);
|
||||||
new WebHandler().bindPath("/").on(server);
|
new WebHandler().bindPath("/").on(server);
|
||||||
server.start();
|
server.start();
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* © SRSoftware 2025 */
|
||||||
|
package de.srsoftware.umbrella.core.api;
|
||||||
|
|
||||||
|
public interface Translator {
|
||||||
|
public String translate(String language, String text);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import Login from "./Components/Login.svelte";
|
import Login from "./Components/Login.svelte";
|
||||||
import Messages from "./routes/message/Messages.svelte";
|
import Messages from "./routes/message/Messages.svelte";
|
||||||
import Menu from "./Components/Menu.svelte";
|
import Menu from "./Components/Menu.svelte";
|
||||||
|
import ResetPw from "./routes/user/ResetPw.svelte";
|
||||||
import Search from "./routes/search/Search.svelte";
|
import Search from "./routes/search/Search.svelte";
|
||||||
import User from "./routes/user/User.svelte";
|
import User from "./routes/user/User.svelte";
|
||||||
import UserEdit from "./routes/user/Edit.svelte";
|
import UserEdit from "./routes/user/Edit.svelte";
|
||||||
@@ -42,8 +43,11 @@
|
|||||||
<Route path="/user/oidc/edit/:serviceName" component={EditService} />
|
<Route path="/user/oidc/edit/:serviceName" component={EditService} />
|
||||||
<Route component={User} />
|
<Route component={User} />
|
||||||
{:else}
|
{:else}
|
||||||
<Login />
|
<Route path="/user/reset/pw" component={ResetPw} />
|
||||||
<Route path="/oidc_callback" component={Callback} />
|
<Route path="/oidc_callback" component={Callback} />
|
||||||
|
<Route>
|
||||||
|
<Login />
|
||||||
|
</Route>
|
||||||
{/if}
|
{/if}
|
||||||
</Router>
|
</Router>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from '../translations.svelte.js';
|
import { t } from '../translations.svelte.js';
|
||||||
import { checkUser, tryLogin } from '../user.svelte.js';
|
import { checkUser, tryLogin } from '../user.svelte.js';
|
||||||
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
let credentials = { username : null, password : null }
|
let credentials = { username : null, password : null }
|
||||||
let services = $state([]);
|
let services = $state([]);
|
||||||
|
const router = useTinyRouter();
|
||||||
|
|
||||||
function doLogin(ev){
|
function doLogin(ev){
|
||||||
|
ev.preventDefault();
|
||||||
tryLogin(credentials);
|
tryLogin(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetPW(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
router.navigate('/user/reset/pw');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -49,9 +57,13 @@
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
a{
|
||||||
|
display: block;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<form on:submit|preventDefault={doLogin}>
|
<form onsubmit={doLogin}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t('login.Login')}</legend>
|
<legend>{t('login.Login')}</legend>
|
||||||
<label>
|
<label>
|
||||||
@@ -63,11 +75,12 @@
|
|||||||
<span>{t('login.Password')}</span>
|
<span>{t('login.Password')}</span>
|
||||||
</label>
|
</label>
|
||||||
<button>{t('login.do_login')}</button>
|
<button>{t('login.do_login')}</button>
|
||||||
|
<a onclick={resetPW}>{t('login.forgot_pass')}</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t('login.OIDC_Login')}</legend>
|
<legend>{t('login.OIDC_Login')}</legend>
|
||||||
{#each services as service,i}
|
{#each services as service,i}
|
||||||
<button on:click={() => redirectTo(service)}>{service}</button>
|
<button onclick={() => redirectTo(service)}>{service}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ async function fetchModules(){
|
|||||||
const resp = await fetch(url,{credentials:'include'});
|
const resp = await fetch(url,{credentials:'include'});
|
||||||
if (resp.ok){
|
if (resp.ok){
|
||||||
const arr = await resp.json();
|
const arr = await resp.json();
|
||||||
for (let entry of arr) modules.push({name:t('module.'+entry.module),url:entry.url});
|
for (let entry of arr) modules.push({name:t('menu.'+entry.module),url:entry.url});
|
||||||
console.log(modules);
|
console.log(modules);
|
||||||
} else {
|
} else {
|
||||||
console.log('error');
|
console.log('error');
|
||||||
|
|||||||
39
frontend/src/routes/user/ResetPw.svelte
Normal file
39
frontend/src/routes/user/ResetPw.svelte
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import { t } from '../../translations.svelte.js';
|
||||||
|
import { useTinyRouter } from 'svelte-tiny-router';
|
||||||
|
|
||||||
|
let mail = "";
|
||||||
|
let caption = t('user.send_mail');
|
||||||
|
const router = useTinyRouter();
|
||||||
|
|
||||||
|
async function submit(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
caption = t('user.sent_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
label{display:block}
|
||||||
|
fieldset{
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
width: 200px;
|
||||||
|
margin-left: -100px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form onsubmit={submit}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t('user.reset_pw')}</legend>
|
||||||
|
<label>
|
||||||
|
<input type="email" bind:value={mail}/>
|
||||||
|
{t('user.enter_email')}
|
||||||
|
</label>
|
||||||
|
<button type="submit">{caption}</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
@@ -3,6 +3,7 @@ description = "Umbrella : Message subsystem"
|
|||||||
dependencies{
|
dependencies{
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":user"))
|
implementation(project(":user"))
|
||||||
|
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")
|
||||||
implementation("de.srsoftware:tools.mime:1.1.2")
|
implementation("de.srsoftware:tools.mime:1.1.2")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.message;
|
|||||||
|
|
||||||
public class Constants {
|
public class Constants {
|
||||||
public static final String AUTH = "mail.smtp.auth";
|
public static final String AUTH = "mail.smtp.auth";
|
||||||
|
public static final String DEBUG_ADDREESS = "debug_addres";
|
||||||
public static final String ENVELOPE_FROM = "mail.smtp.from";
|
public static final String ENVELOPE_FROM = "mail.smtp.from";
|
||||||
public static final String FIELD_MESSAGES = "messages";
|
public static final String FIELD_MESSAGES = "messages";
|
||||||
public static final String FIELD_HOST = "host";
|
public static final String FIELD_HOST = "host";
|
||||||
@@ -12,6 +13,7 @@ public class Constants {
|
|||||||
public static final String JSONOBJECT = "json object";
|
public static final String JSONOBJECT = "json object";
|
||||||
public static final String PORT = "mail.smtp.port";
|
public static final String PORT = "mail.smtp.port";
|
||||||
public static final String RECEIVERS = "receivers";
|
public static final String RECEIVERS = "receivers";
|
||||||
|
public static final String SMTP = "smtp";
|
||||||
public static final String SSL = "mail.smtp.ssl.enable";
|
public static final String SSL = "mail.smtp.ssl.enable";
|
||||||
public static final String SUBMISSION = "submission";
|
public static final String SUBMISSION = "submission";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,195 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.message;
|
package de.srsoftware.umbrella.message;
|
||||||
|
|
||||||
public class MessageSystem {
|
import static de.srsoftware.tools.PathHandler.CONTENT_TYPE;
|
||||||
private final SqliteMessageDb db;
|
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.*;
|
||||||
|
|
||||||
public MessageSystem(SqliteMessageDb messageDb) {
|
import de.srsoftware.configuration.Configuration;
|
||||||
|
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||||
|
import de.srsoftware.umbrella.core.api.Translator;
|
||||||
|
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;
|
||||||
|
import jakarta.mail.Session;
|
||||||
|
import jakarta.mail.Transport;
|
||||||
|
import jakarta.mail.internet.MimeBodyPart;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import jakarta.mail.internet.MimeMultipart;
|
||||||
|
import jakarta.mail.util.ByteArrayDataSource;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
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 class SubmissionTask extends TimerTask{
|
||||||
|
|
||||||
|
|
||||||
|
private final Integer hour;
|
||||||
|
|
||||||
|
public SubmissionTask(Integer hour) {
|
||||||
|
this.hour = hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
processMessages(hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void schedule() {
|
||||||
|
var date = Calendar.getInstance();
|
||||||
|
date.set(Calendar.HOUR_OF_DAY, hour);
|
||||||
|
date.set(Calendar.MINUTE, 0);
|
||||||
|
date.set(Calendar.SECOND, 0);
|
||||||
|
date.set(Calendar.MILLISECOND,0);
|
||||||
|
if (date.before(Calendar.getInstance())) date.add(Calendar.HOUR, 24);
|
||||||
|
timer.schedule(this,date.getTime());
|
||||||
|
LOG.log(INFO,"Scheduled {0} at {1}",getClass().getSimpleName(),date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
private final String from,host,user,pass;
|
||||||
|
private final int port;
|
||||||
|
private final SqliteMessageDb db;
|
||||||
|
private Session session;
|
||||||
|
private List<Envelope> queue = new CopyOnWriteArrayList<>();
|
||||||
|
private String debugAddress;
|
||||||
|
private final HashMap<Receiver,List<Exception>> exceptions = new HashMap<>();
|
||||||
|
private final Translator translator;
|
||||||
|
|
||||||
|
public MessageSystem(SqliteMessageDb messageDb, Translator translator, Configuration config) {
|
||||||
db = messageDb;
|
db = messageDb;
|
||||||
|
this.translator = translator;
|
||||||
|
debugAddress = config.get(DEBUG_ADDREESS).map(Object::toString).orElse(null);
|
||||||
|
config = config.subset(SMTP).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp not configured!"));
|
||||||
|
port = config.get(FIELD_PORT,587);
|
||||||
|
host = config.get(FIELD_HOST).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.host not configured!"));
|
||||||
|
user = config.get(USER).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.user not configured!"));
|
||||||
|
pass = config.get(PASS).map(Object::toString).orElseThrow(() -> new RuntimeException("umbrella.modules.message.smtp.pass not configured!"));
|
||||||
|
from = user;
|
||||||
|
new SubmissionTask(8).schedule();
|
||||||
|
new SubmissionTask(10).schedule();
|
||||||
|
new SubmissionTask(12).schedule();
|
||||||
|
new SubmissionTask(14).schedule();
|
||||||
|
new SubmissionTask(16).schedule();
|
||||||
|
new SubmissionTask(18).schedule();
|
||||||
|
new SubmissionTask(20).schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Envelope envelope) {
|
||||||
|
queue.add(envelope);
|
||||||
|
new Thread(() -> processMessages(null)).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<User>();
|
||||||
|
List<User> 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) {
|
||||||
|
if (recv instanceof UmbrellaUser uu) {
|
||||||
|
try {
|
||||||
|
if (!db.getSettings(uu).sendAt(scheduledHour)) continue;
|
||||||
|
} catch (UmbrellaException ignored) {}
|
||||||
|
}
|
||||||
|
dueRecipients.add(recv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
var envelopes = queue.stream().filter(env -> env.isFor(receiver)).toList();
|
||||||
|
for (var envelope : envelopes) combined.merge(envelope.message());
|
||||||
|
|
||||||
|
try {
|
||||||
|
send(combined,receiver,date);
|
||||||
|
for (var envelope : envelopes){
|
||||||
|
var audience = envelope.receivers();
|
||||||
|
audience.remove(receiver);
|
||||||
|
if (audience.isEmpty()) queue.remove(envelope);
|
||||||
|
}
|
||||||
|
} catch (Exception ex){
|
||||||
|
LOG.log(WARNING,"Failed to deliver mail ({0}) to {1}.",combined.subject(),receiver,ex);
|
||||||
|
for (var message : combined.messages()) exceptions.computeIfAbsent(new Receiver(receiver,message), k -> new ArrayList<>()).add(ex);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledHour != null) new SubmissionTask(scheduledHour).schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Session session() {
|
||||||
|
if (session == null){
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.put(HOST, host);
|
||||||
|
props.put(PORT, port);
|
||||||
|
props.put(AUTH, true);
|
||||||
|
props.put(SSL, true);
|
||||||
|
props.put(ENVELOPE_FROM,from);
|
||||||
|
session = Session.getInstance(props);
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDebugAddress(String newVal) {
|
||||||
|
this.debugAddress = newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void send(CombinedMessage message, User 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.setSubject(message.subject(), UTF8);
|
||||||
|
msg.setSentDate(date);
|
||||||
|
var toEmail = debugAddress != null ? debugAddress : receiver.email();
|
||||||
|
msg.setRecipients(Message.RecipientType.TO, toEmail);
|
||||||
|
|
||||||
|
if (message.attachments().isEmpty()){
|
||||||
|
msg.setText(message.body(), UTF8);
|
||||||
|
} else {
|
||||||
|
var multipart = new MimeMultipart();
|
||||||
|
var body = new MimeBodyPart();
|
||||||
|
body.setText(message.body(),UTF8);
|
||||||
|
|
||||||
|
multipart.addBodyPart(body);
|
||||||
|
for (var attachment : message.attachments()){
|
||||||
|
var part = new MimeBodyPart();
|
||||||
|
part.setDataHandler(new DataHandler(new ByteArrayDataSource(attachment.content(),attachment.mime())));
|
||||||
|
part.setFileName(attachment.name());
|
||||||
|
multipart.addBodyPart(part);
|
||||||
|
msg.setContent(multipart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LOG.log(TRACE, "Message to {0} is ready…", receiver);
|
||||||
|
Transport.send(msg,user,pass);
|
||||||
|
LOG.log(DEBUG, "Sent message to {0}.", receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/* © SRSoftware 2025 */
|
|
||||||
package de.srsoftware.umbrella.message.model;
|
|
||||||
|
|
||||||
|
|
||||||
import de.srsoftware.umbrella.user.model.User;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This maps recipient email addresses to the pending messages of that recipient
|
|
||||||
*/
|
|
||||||
public class MailQueue extends ArrayList<Envelope>{
|
|
||||||
|
|
||||||
private record Receiver(User user, Message message){}
|
|
||||||
|
|
||||||
private final HashMap<Receiver,List<Exception>> exceptions = new HashMap<>();
|
|
||||||
|
|
||||||
public interface Listener{
|
|
||||||
public void messagesAdded();
|
|
||||||
public void setQueue(MailQueue queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Set<Listener> listeners = new HashSet<>();
|
|
||||||
|
|
||||||
public void addListener(Listener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
listener.setQueue(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void commit() {
|
|
||||||
listeners.forEach(Listener::messagesAdded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Envelope> envelopesFor(User recv) {
|
|
||||||
return stream().filter(env -> env.isFor(recv)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void failedAt(User receiver, CombinedMessage combined, Exception ex) {
|
|
||||||
for (var message : combined.messages()) exceptions.computeIfAbsent(new Receiver(receiver,message), k -> new ArrayList<>()).add(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return the email addresses of the recipients of all messages in the queue
|
|
||||||
*
|
|
||||||
* @return a list of email addresses
|
|
||||||
*/
|
|
||||||
public List<User> receivers() {
|
|
||||||
return stream().map(Envelope::receivers)
|
|
||||||
.flatMap(Set::stream)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* © SRSoftware 2025 */
|
||||||
|
package de.srsoftware.umbrella.message.model;
|
||||||
|
|
||||||
|
public interface PostBox {
|
||||||
|
public void send(Envelope envelope);
|
||||||
|
}
|
||||||
30
messages/src/test/java/TriggerTest.java
Normal file
30
messages/src/test/java/TriggerTest.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* © SRSoftware 2025 */
|
||||||
|
import static java.lang.Thread.sleep;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class TriggerTest {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSynchronization() throws InterruptedException {
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
new Thread(() -> writeTo(sb,"A")).start();
|
||||||
|
sleep(100);
|
||||||
|
new Thread(() -> writeTo(sb,"B")).start();
|
||||||
|
sleep(100);
|
||||||
|
new Thread(() -> writeTo(sb,"C")).start();
|
||||||
|
sleep(1000);
|
||||||
|
assertEquals("AABBCC",sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void writeTo(StringBuffer sb, String mark) {
|
||||||
|
sb.append(mark);
|
||||||
|
try {
|
||||||
|
sleep(200);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
sb.append(mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
description = "Umbrella : Translations"
|
description = "Umbrella : Translations"
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
implementation(project(":core"))
|
||||||
implementation("org.json:json:20240303")
|
implementation("org.json:json:20240303")
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,14 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import de.srsoftware.tools.Path;
|
import de.srsoftware.tools.Path;
|
||||||
import de.srsoftware.tools.PathHandler;
|
import de.srsoftware.tools.PathHandler;
|
||||||
|
import de.srsoftware.umbrella.core.api.Translator;
|
||||||
import java.io.ByteArrayOutputStream;
|
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 org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class Translations extends PathHandler {
|
public class Translations extends PathHandler implements Translator {
|
||||||
private static final System.Logger LOG = System.getLogger("Translations");
|
private static final System.Logger LOG = System.getLogger("Translations");
|
||||||
|
|
||||||
private HashMap<String, JSONObject> translations = new HashMap<>();
|
private HashMap<String, JSONObject> translations = new HashMap<>();
|
||||||
@@ -23,10 +24,13 @@ public class Translations extends PathHandler {
|
|||||||
allowOrigin(ex,"*");
|
allowOrigin(ex,"*");
|
||||||
if (path.empty())return sendContent(ex,501,"Language missing");
|
if (path.empty())return sendContent(ex,501,"Language missing");
|
||||||
var lang = path.pop();
|
var lang = path.pop();
|
||||||
var translationsForLang = translations.get(lang);
|
return sendContent(ex,getTranslations(lang));
|
||||||
if (translationsForLang == null) translations.put(lang,translationsForLang = loadTranslations(lang));
|
}
|
||||||
|
|
||||||
return sendContent(ex,translationsForLang);
|
private JSONObject getTranslations(String lang) throws IOException {
|
||||||
|
var result = translations.get(lang);
|
||||||
|
if (result == null) translations.put(lang,result = loadTranslations(lang));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JSONObject loadTranslations(String lang) throws IOException {
|
private JSONObject loadTranslations(String lang) throws IOException {
|
||||||
@@ -44,4 +48,14 @@ public class Translations extends PathHandler {
|
|||||||
return new JSONObject();
|
return new JSONObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String translate(String language, String text) {
|
||||||
|
try {
|
||||||
|
var translations = getTranslations(language);
|
||||||
|
return translations.has(text) ? translations.getString(text) : text;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,30 @@
|
|||||||
"login" : {
|
"login" : {
|
||||||
"do_login" : "anmelden",
|
"do_login" : "anmelden",
|
||||||
"Email_or_Username": "Email oder Nutzername",
|
"Email_or_Username": "Email oder Nutzername",
|
||||||
|
"forgot_pass" : "Password vergessen?",
|
||||||
"Login" : "Anmeldung",
|
"Login" : "Anmeldung",
|
||||||
"OIDC_Login" : "Anmeldung mit OIDC",
|
"OIDC_Login" : "Anmeldung mit OIDC",
|
||||||
"Password" : "Passwort"
|
"Password" : "Passwort"
|
||||||
},
|
},
|
||||||
"menu" : {
|
"menu" : {
|
||||||
|
"bookmark": "Lesezeichen",
|
||||||
|
"company": "Firma",
|
||||||
|
"contact": "Kontakte",
|
||||||
|
"document": "Dokumente",
|
||||||
|
"files": "Dateien",
|
||||||
|
"items": "Items",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
|
"message": "Benachrichtigungen",
|
||||||
|
"model": "Modelle",
|
||||||
|
"notes": "Notizen",
|
||||||
|
"project": "Projekte",
|
||||||
|
"stock": "Inventar",
|
||||||
|
"task": "Aufgaben",
|
||||||
|
"time": "Zeiterfassung",
|
||||||
|
"tutorial": "Tutorial",
|
||||||
|
"user": "Benutzer",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"tutorial": "Tutorial"
|
"wiki": "Wiki"
|
||||||
},
|
},
|
||||||
"status" : {
|
"status" : {
|
||||||
"403": "Zugriff verweigert",
|
"403": "Zugriff verweigert",
|
||||||
@@ -64,10 +80,13 @@
|
|||||||
"saved": "gespeichert",
|
"saved": "gespeichert",
|
||||||
"save_service": "Service speichern",
|
"save_service": "Service speichern",
|
||||||
"save_user": "Nutzer speichern",
|
"save_user": "Nutzer speichern",
|
||||||
|
"sent_email": "Email gesendet",
|
||||||
"service": "Service",
|
"service": "Service",
|
||||||
|
"settings" : "Eisntellungen",
|
||||||
"theme": "Design",
|
"theme": "Design",
|
||||||
"unlink": "Trennen",
|
"unlink": "Trennen",
|
||||||
"update": "aktualisieren",
|
"update": "aktualisieren",
|
||||||
|
"UPDATE_USERS" : "Nutzer aktualisieren",
|
||||||
"user_module" : "Umbrella User-Verwaltung",
|
"user_module" : "Umbrella User-Verwaltung",
|
||||||
"your_profile": "dein Profil"
|
"your_profile": "dein Profil"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user