implemented dynamic theme loading, working on user edit
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Svelte</title>
|
<title>Vite + Svelte</title>
|
||||||
|
<style id="usercss"></style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -9,12 +9,22 @@
|
|||||||
import User from "./routes/user/User.svelte";
|
import User from "./routes/user/User.svelte";
|
||||||
|
|
||||||
|
|
||||||
let translations_ready = false;
|
let translations_ready = $state(false);
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadTranslation('de','Login');
|
await loadTranslation('de','Login');
|
||||||
translations_ready = true;
|
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>
|
</script>
|
||||||
|
|
||||||
{#if translations_ready }
|
{#if translations_ready }
|
||||||
|
|||||||
@@ -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
Normal file
27
frontend/src/routes/user/ClickInput.svelte
Normal file
@@ -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}
|
||||||
@@ -2,15 +2,20 @@
|
|||||||
import { t } from '../../translations.svelte.js';
|
import { t } from '../../translations.svelte.js';
|
||||||
import { user } from '../../user.svelte.js';
|
import { user } from '../../user.svelte.js';
|
||||||
import EditableField from './EditableField.svelte';
|
import EditableField from './EditableField.svelte';
|
||||||
|
import ClickInput from './ClickInput.svelte';
|
||||||
|
|
||||||
async function patch(changeset){
|
async function patch(changeset){
|
||||||
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/${user.id}`;
|
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/${user.id}`;
|
||||||
|
|
||||||
await fetch(url,{
|
const response = await fetch(url,{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify(changeset)
|
body: JSON.stringify(changeset)
|
||||||
})
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const json = await response.json();
|
||||||
|
for (let key of Object.keys(json)) user[key] = json[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<h1>{t('user.user_module')}</h1>
|
<h1>{t('user.user_module')}</h1>
|
||||||
@@ -25,13 +30,22 @@
|
|||||||
<th>{t('user.id')}</th>
|
<th>{t('user.id')}</th>
|
||||||
<td>{user.id}</td>
|
<td>{user.id}</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<th>{t('user.login')}</th>
|
<th>{t('user.login')}</th>
|
||||||
<td>{user.login}</td>
|
<td>{user.login}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<EditableField key='user.email' value={user.email} onUpdate={patch} />
|
<tr>
|
||||||
<EditableField key='user.language' value={user.language} onUpdate={patch} />
|
<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>
|
<tr>
|
||||||
<th>{t('user.theme')}</th>
|
<th>{t('user.theme')}</th>
|
||||||
<td>{user.theme}</td>
|
<td>{user.theme}</td>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export const user = $state({
|
export const user = $state({
|
||||||
name : null
|
name : null,
|
||||||
|
theme : 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function checkUser(){
|
export async function checkUser(){
|
||||||
|
console.log('checkUser()');
|
||||||
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/whoami`;
|
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/whoami`;
|
||||||
const response = await fetch(url,{
|
const response = await fetch(url,{
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -34,6 +36,7 @@ export async function tryLogin(credentials){
|
|||||||
if (response.ok){
|
if (response.ok){
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
for (let key of Object.keys(json)) user[key] = json[key];
|
for (let key of Object.keys(json)) user[key] = json[key];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
alert("Login failed!");
|
alert("Login failed!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import static de.srsoftware.umbrella.core.ResponseCode.*;
|
|||||||
import static de.srsoftware.umbrella.user.Constants.*;
|
import static de.srsoftware.umbrella.user.Constants.*;
|
||||||
import static de.srsoftware.umbrella.user.Paths.LOGIN;
|
import static de.srsoftware.umbrella.user.Paths.LOGIN;
|
||||||
import static de.srsoftware.umbrella.user.Paths.WHOAMI;
|
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.lang.System.Logger.Level.WARNING;
|
||||||
import static java.time.temporal.ChronoUnit.DAYS;
|
import static java.time.temporal.ChronoUnit.DAYS;
|
||||||
|
|
||||||
@@ -75,6 +77,38 @@ public class UserModule extends PathHandler {
|
|||||||
|
|
||||||
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
|
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
|
||||||
addCors(ex);
|
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;
|
JSONObject json;
|
||||||
try {
|
try {
|
||||||
json = json(ex);
|
json = json(ex);
|
||||||
@@ -82,14 +116,10 @@ public class UserModule extends PathHandler {
|
|||||||
LOG.log(WARNING,"Request does not contain valid JSON",e);
|
LOG.log(WARNING,"Request does not contain valid JSON",e);
|
||||||
return sendContent(ex,BAD_REQUEST,"Body contains no JSON data");
|
return sendContent(ex,BAD_REQUEST,"Body contains no JSON data");
|
||||||
}
|
}
|
||||||
var head = path.pop();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (head == null || head.isBlank()) return sendContent(ex,UNPROCESSABLE,"User id missing!");
|
return update(ex, editedUser,json);
|
||||||
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);
|
|
||||||
} catch (UmbrellaException e) {
|
} catch (UmbrellaException e) {
|
||||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -153,12 +183,12 @@ public class UserModule extends PathHandler {
|
|||||||
|
|
||||||
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
|
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
|
||||||
var id = user.id();
|
var id = user.id();
|
||||||
var name = json.has("user.name") && json.get("user.name") instanceof String s && !s.isBlank() ? s : user.name();
|
var name = json.has(NAME) && json.get(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 email = json.has(EMAIL) && json.get(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 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("user.theme") && json.get("user.theme") instanceof String t && !t.isBlank() ? t : user.theme();
|
var theme = json.has(THEME) && json.get(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 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, Set.of(),null));
|
var saved = users.save(new DbUser(id,name,email,pass,theme,lang, user.permissions(),null));
|
||||||
return sendContent(ex,OK,saved);
|
return sendContent(ex,OK,saved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,24 @@ package de.srsoftware.umbrella.user.model;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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 class DbUser extends UmbrellaUser {
|
||||||
|
|
||||||
public enum PERMISSION {
|
public enum PERMISSION {
|
||||||
CREATE_USERS,
|
CREATE_USERS,
|
||||||
|
UPDATE_USERS,
|
||||||
DELETE_USERS,
|
DELETE_USERS,
|
||||||
LIST_USERS,
|
LIST_USERS,
|
||||||
IMPERSONATE,
|
IMPERSONATE,
|
||||||
MANAGE_LOGIN_SERVICES
|
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 Set<PERMISSION> permissions;
|
||||||
private final Password hashedPass;
|
private final Password hashedPass;
|
||||||
private final Long lastLogoff;
|
private final Long lastLogoff;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Query.*;
|
|||||||
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||||
import static de.srsoftware.umbrella.core.Constants.*;
|
import static de.srsoftware.umbrella.core.Constants.*;
|
||||||
import static de.srsoftware.umbrella.user.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.lang.System.Logger.Level.*;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ CREATE TABLE IF NOT EXISTS {0} (
|
|||||||
|
|
||||||
private DbUser toUser(ResultSet rs) throws SQLException {
|
private DbUser toUser(ResultSet rs) throws SQLException {
|
||||||
long id = rs.getLong(ID);
|
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(
|
return new DbUser(
|
||||||
id,
|
id,
|
||||||
rs.getString(LOGIN),
|
rs.getString(LOGIN),
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
description = "Umbrella : Web"
|
description = "Umbrella : Web"
|
||||||
|
|
||||||
|
dependencies{
|
||||||
|
implementation(project(":core"))
|
||||||
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
System.out.println("Copying from dist…")
|
System.out.println("Copying from dist…")
|
||||||
from("../frontend/dist") {
|
from("../frontend/dist") {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* © SRSoftware 2025 */
|
/* © SRSoftware 2025 */
|
||||||
package de.srsoftware.umbrella.web;
|
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.DEBUG;
|
||||||
import static java.lang.System.Logger.Level.WARNING;
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
|
|
||||||
@@ -9,8 +11,25 @@ import de.srsoftware.tools.Path;
|
|||||||
import de.srsoftware.tools.PathHandler;
|
import de.srsoftware.tools.PathHandler;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class WebHandler extends PathHandler {
|
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
|
@Override
|
||||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||||
LOG.log(DEBUG,"doGet({0},ex)",path);
|
LOG.log(DEBUG,"doGet({0},ex)",path);
|
||||||
@@ -30,10 +49,10 @@ public class WebHandler extends PathHandler {
|
|||||||
try (var stream = conn.getInputStream()){
|
try (var stream = conn.getInputStream()){
|
||||||
stream.transferTo(bos);
|
stream.transferTo(bos);
|
||||||
ex.getResponseHeaders().add(CONTENT_TYPE,mime);
|
ex.getResponseHeaders().add(CONTENT_TYPE,mime);
|
||||||
return sendContent(ex,bos.toByteArray());
|
return sendContent(addCors(ex),bos.toByteArray());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.log(WARNING,"Failed to load {0}",url);
|
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
Normal file
37
web/src/main/resources/web/css/bloodshed.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user