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" />
|
||||
<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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 { 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 @@
|
||||
<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>
|
||||
|
||||
@@ -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){
|
||||
if (response.ok){
|
||||
const json = await response.json();
|
||||
for (let key of Object.keys(json)) user[key] = json[key];
|
||||
|
||||
} else {
|
||||
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.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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} (
|
||||
|
||||
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),
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
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 {
|
||||
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
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