Browse Source

implemented dynamic theme loading, working on user edit

feature/document
Stephan Richter 4 months ago
parent
commit
684e0b00dd
  1. 1
      frontend/index.html
  2. 12
      frontend/src/App.svelte
  3. 37
      frontend/src/app.css
  4. 27
      frontend/src/routes/user/ClickInput.svelte
  5. 24
      frontend/src/routes/user/User.svelte
  6. 5
      frontend/src/user.svelte.js
  7. 56
      user/src/main/java/de/srsoftware/umbrella/user/UserModule.java
  8. 8
      user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java
  9. 4
      user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java
  10. 5
      web/build.gradle.kts
  11. 23
      web/src/main/java/de/srsoftware/umbrella/web/WebHandler.java
  12. 37
      web/src/main/resources/web/css/bloodshed.css

1
frontend/index.html

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte</title>
<style id="usercss"></style>
</head>
<body>
<div id="app"></div>

12
frontend/src/App.svelte

@ -9,12 +9,22 @@ @@ -9,12 +9,22 @@
import User from "./routes/user/User.svelte";
let translations_ready = false;
let translations_ready = $state(false);
onMount(async () => {
await loadTranslation('de','Login');
translations_ready = true;
});
function loadTheme(name){
if (!name) return;
console.log({theme:name});
const url = `${location.protocol}//${location.host.replace('5173','8080')}/css/${name}.css`;
fetch(url).then(resp => resp.text()).then(css => {
document.getElementById('usercss').innerText = css;
});
}
$effect(() => loadTheme(user.theme));
</script>
{#if translations_ready }

37
frontend/src/app.css

@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
a {
color: orange;
}
body {
background: black;
color: orange;
}
fieldset {
border: 1px solid orange;
border-radius: 4px;
}
input{
background: black;
border: 1px dotted orange;
border-radius: 4px;
padding: 3px;
margin: 3px;
color: orange;
}
button{
background: orange;
border-radius: 5px;
padding: 5px 7px;
border-width: 2px;
border-style: solid;
border-color: yellow red red yellow;
}
footer {
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
margin: 5px;
}

27
frontend/src/routes/user/ClickInput.svelte

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
<script>
import { t } from '../../translations.svelte.js';
import { checkUser } from '../../user.svelte.js';
let { key, onUpdate, value } = $props();
let input = $state(false);
function edit(){
input = true;
}
function check_key(evt){
if (evt.key === 'Enter'){
input = false;
let obj = {};
obj[key] = value;
onUpdate(obj);
}
}
</script>
{#if input}
<input type="text" bind:value onkeyup={check_key} />
{:else}
<span onclick={edit}>{value}</span>
{/if}

24
frontend/src/routes/user/User.svelte

@ -2,15 +2,20 @@ @@ -2,15 +2,20 @@
import { t } from '../../translations.svelte.js';
import { user } from '../../user.svelte.js';
import EditableField from './EditableField.svelte';
import ClickInput from './ClickInput.svelte';
async function patch(changeset){
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/${user.id}`;
await fetch(url,{
const response = await fetch(url,{
method: 'PATCH',
credentials: 'include',
body: JSON.stringify(changeset)
})
});
if (response.ok) {
const json = await response.json();
for (let key of Object.keys(json)) user[key] = json[key];
}
}
</script>
<h1>{t('user.user_module')}</h1>
@ -25,13 +30,22 @@ @@ -25,13 +30,22 @@
<th>{t('user.id')}</th>
<td>{user.id}</td>
</tr>
<EditableField key='user.name' value={user.name} onUpdate={patch} />
<tr>
<th>{t('user.name')}</th>
<td><ClickInput key='name' value={user.name} onUpdate={patch} /></td>
</tr>
<tr>
<th>{t('user.login')}</th>
<td>{user.login}</td>
</tr>
<EditableField key='user.email' value={user.email} onUpdate={patch} />
<EditableField key='user.language' value={user.language} onUpdate={patch} />
<tr>
<th>{t('user.email')}</th>
<td><ClickInput key='email' value={user.email} onUpdate={patch} /></td>
</tr>
<tr>
<th>{t('user.language')}</th>
<td><ClickInput key='language' value={user.language} onUpdate={patch} /></td>
</tr>
<tr>
<th>{t('user.theme')}</th>
<td>{user.theme}</td>

5
frontend/src/user.svelte.js

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
export const user = $state({
name : null
name : null,
theme : 'default'
})
export async function checkUser(){
console.log('checkUser()');
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/whoami`;
const response = await fetch(url,{
credentials: 'include'
@ -34,6 +36,7 @@ export async function tryLogin(credentials){ @@ -34,6 +36,7 @@ export async function tryLogin(credentials){
if (response.ok){
const json = await response.json();
for (let key of Object.keys(json)) user[key] = json[key];
} else {
alert("Login failed!");
}

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

@ -8,6 +8,8 @@ import static de.srsoftware.umbrella.core.ResponseCode.*; @@ -8,6 +8,8 @@ import static de.srsoftware.umbrella.core.ResponseCode.*;
import static de.srsoftware.umbrella.user.Constants.*;
import static de.srsoftware.umbrella.user.Paths.LOGIN;
import static de.srsoftware.umbrella.user.Paths.WHOAMI;
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.UPDATE_USERS;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;
import static java.time.temporal.ChronoUnit.DAYS;
@ -75,6 +77,38 @@ public class UserModule extends PathHandler { @@ -75,6 +77,38 @@ public class UserModule extends PathHandler {
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
addCors(ex);
var sessionToken = SessionToken.from(ex);
if (sessionToken.isEmpty()) return sendEmptyResponse(UNAUTHORIZED,ex);
UmbrellaUser requestingUser;
try {
Session session = users.load(Token.of(sessionToken.get()));
requestingUser = users.load(session);
} catch (UmbrellaException e) {
return sendContent(ex,e.statusCode(),e.getMessage());
}
var head = path.pop();
long userId;
try {
if (head == null || head.isBlank()) return sendContent(ex,UNPROCESSABLE,"User id missing!");
userId = Long.parseLong(head);
} catch (NumberFormatException e) {
return sendContent(ex,UNPROCESSABLE,"Invalid user id: "+head);
}
DbUser editedUser;
try {
editedUser = (DbUser) users.load(userId);
} catch (UmbrellaException e) {
return sendContent(ex,e.statusCode(),e.getMessage());
}
if (requestingUser.id() != userId && (!(requestingUser instanceof DbUser dbUser) || !dbUser.permissions().contains(UPDATE_USERS))){
return sendContent(ex,FORBIDDEN,"You are not allowed to update user "+editedUser.name());
}
JSONObject json;
try {
json = json(ex);
@ -82,14 +116,10 @@ public class UserModule extends PathHandler { @@ -82,14 +116,10 @@ public class UserModule extends PathHandler {
LOG.log(WARNING,"Request does not contain valid JSON",e);
return sendContent(ex,BAD_REQUEST,"Body contains no JSON data");
}
var head = path.pop();
try {
if (head == null || head.isBlank()) return sendContent(ex,UNPROCESSABLE,"User id missing!");
long userId = Long.parseLong(head);
var user = (DbUser) users.load(userId);
return update(ex, user,json);
} catch (NumberFormatException e) {
return sendContent(ex,UNPROCESSABLE,"Invalid user id: "+head);
return update(ex, editedUser,json);
} catch (UmbrellaException e) {
return sendContent(ex,e.statusCode(),e.getMessage());
}
@ -153,12 +183,12 @@ public class UserModule extends PathHandler { @@ -153,12 +183,12 @@ public class UserModule extends PathHandler {
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
var id = user.id();
var name = json.has("user.name") && json.get("user.name") instanceof String s && !s.isBlank() ? s : user.name();
var email = json.has("user.email") && json.get("user.email") instanceof String e && !e.isBlank() ? e : user.email();
var pass = json.has("user.password") && json.get("user.password") instanceof String p && !p.isBlank() ? Password.of(BAD_HASHER.hash(p,null)) : user.hashedPassword();
var theme = json.has("user.theme") && json.get("user.theme") instanceof String t && !t.isBlank() ? t : user.theme();
var lang = json.has("user.language") && json.get("user.language") instanceof String l && !l.isBlank() ? l : user.language();
var saved = users.save(new DbUser(id,name,email,pass,theme,lang, Set.of(),null));
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 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();
var saved = users.save(new DbUser(id,name,email,pass,theme,lang, user.permissions(),null));
return sendContent(ex,OK,saved);
}
}

8
user/src/main/java/de/srsoftware/umbrella/user/model/DbUser.java

@ -5,16 +5,24 @@ package de.srsoftware.umbrella.user.model; @@ -5,16 +5,24 @@ package de.srsoftware.umbrella.user.model;
import java.util.Map;
import java.util.Set;
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.*;
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;
public class DbUser extends UmbrellaUser {
public enum PERMISSION {
CREATE_USERS,
UPDATE_USERS,
DELETE_USERS,
LIST_USERS,
IMPERSONATE,
MANAGE_LOGIN_SERVICES
}
public static Set<PERMISSION> ADMIN_PERMISSIONS = Set.of(CREATE_USERS, UPDATE_USERS, DELETE_USERS, IMPERSONATE, MANAGE_LOGIN_SERVICES,LIST_USERS);
private final Set<PERMISSION> permissions;
private final Password hashedPass;
private final Long lastLogoff;

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

@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Query.*; @@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.user.Constants.*;
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.*;
import static de.srsoftware.umbrella.user.model.DbUser.ADMIN_PERMISSIONS;
import static java.lang.System.Logger.Level.*;
import static java.text.MessageFormat.format;
@ -471,7 +471,7 @@ CREATE TABLE IF NOT EXISTS {0} ( @@ -471,7 +471,7 @@ CREATE TABLE IF NOT EXISTS {0} (
private DbUser toUser(ResultSet rs) throws SQLException {
long id = rs.getLong(ID);
Set<DbUser.PERMISSION> perms = id == 1 ? Set.of(CREATE_USERS,DELETE_USERS,IMPERSONATE, MANAGE_LOGIN_SERVICES,LIST_USERS) : Set.of();
Set<DbUser.PERMISSION> perms = id == 1 ? ADMIN_PERMISSIONS : Set.of();
return new DbUser(
id,
rs.getString(LOGIN),

5
web/build.gradle.kts

@ -1,5 +1,10 @@ @@ -1,5 +1,10 @@
description = "Umbrella : Web"
dependencies{
implementation(project(":core"))
implementation("de.srsoftware:tools.optionals:1.0.0")
}
tasks.processResources {
System.out.println("Copying from dist…")
from("../frontend/dist") {

23
web/src/main/java/de/srsoftware/umbrella/web/WebHandler.java

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.web;
import static de.srsoftware.tools.Optionals.nullable;
import static de.srsoftware.umbrella.core.ResponseCode.NOT_FOUND;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;
@ -9,8 +11,25 @@ import de.srsoftware.tools.Path; @@ -9,8 +11,25 @@ import de.srsoftware.tools.Path;
import de.srsoftware.tools.PathHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
public class WebHandler extends PathHandler {
private HttpExchange addCors(HttpExchange ex){
var headers = ex.getRequestHeaders();
var origin = nullable(headers.get("Origin")).orElse(List.of()).stream().filter(url -> url.contains("://localhost")||url.contains("://127.0.0.1")).findAny();
if (origin.isPresent()) {
var url = origin.get();
headers = ex.getResponseHeaders();
headers.add("Allow-Origin", url);
headers.add("Access-Control-Allow-Origin", url);
headers.add("Access-Control-Allow-Headers", "Content-Type");
headers.add("Access-Control-Allow-Credentials", "true");
headers.add("Access-Control-Allow-Methods","GET, POST, PATCH");
}
return ex;
}
@Override
public boolean doGet(Path path, HttpExchange ex) throws IOException {
LOG.log(DEBUG,"doGet({0},ex)",path);
@ -30,10 +49,10 @@ public class WebHandler extends PathHandler { @@ -30,10 +49,10 @@ public class WebHandler extends PathHandler {
try (var stream = conn.getInputStream()){
stream.transferTo(bos);
ex.getResponseHeaders().add(CONTENT_TYPE,mime);
return sendContent(ex,bos.toByteArray());
return sendContent(addCors(ex),bos.toByteArray());
} catch (Exception e) {
LOG.log(WARNING,"Failed to load {0}",url);
return false;
return sendContent(ex,NOT_FOUND,"Failed to load "+url);
}
}
}

37
web/src/main/resources/web/css/bloodshed.css

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
a {
color: red;
}
body {
background: black;
color: red;
}
fieldset {
border: 1px solid red;
border-radius: 4px;
}
input{
background: black;
border: 1px dotted red;
border-radius: 4px;
padding: 3px;
margin: 3px;
color: red;
}
button{
background: red;
border-radius: 5px;
padding: 5px 7px;
border-width: 2px;
border-style: solid;
border-color: yellow red red yellow;
}
footer {
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
margin: 5px;
}
Loading…
Cancel
Save