f052_admin_area #27
45
package-lock.json
generated
45
package-lock.json
generated
@@ -12,9 +12,9 @@
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jssha": "^3.3.1",
|
||||
"minio": "^8.0.5",
|
||||
"postcss": "^8.5.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
@@ -2483,6 +2483,29 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt/node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz",
|
||||
@@ -4617,15 +4640,6 @@
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jssha": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
|
||||
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
@@ -5309,6 +5323,17 @@
|
||||
"node": ">= 10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jssha": "^3.3.1",
|
||||
"minio": "^8.0.5",
|
||||
"postcss": "^8.5.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import jsSHA from 'jssha';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
const db = new Database('./src/lib/data/tatort.db');
|
||||
|
||||
let createSQLStmt = `CREATE TABLE IF NOT EXISTS users
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
pw TEXT NOT NULL)`;
|
||||
db.exec(createSQLStmt);
|
||||
|
||||
// check if there are any users; if not add one default admin one
|
||||
const userPassword = 'A-InnoHUB_2025!';
|
||||
const hashedUserPassword = new jsSHA('SHA-512', 'TEXT').update(userPassword).getHash('HEX');
|
||||
const saltRounds = 12;
|
||||
const hashedUserPassword = bcrypt.hashSync(userPassword, saltRounds);
|
||||
const checkInsertSQLStmt = `INSERT INTO users (name, pw) SELECT 'admin', '${hashedUserPassword}'
|
||||
WHERE NOT EXISTS (SELECT * FROM users);`;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jsSHA from 'jssha';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { db } from '$lib/server/dbService';
|
||||
|
||||
import config from '$lib/config';
|
||||
@@ -7,7 +7,6 @@ import config from '$lib/config';
|
||||
const SECRET = config.jwt.secret;
|
||||
const EXPIRES_IN = config.jwt.expiresIn;
|
||||
|
||||
|
||||
export function createToken(userData) {
|
||||
return jwt.sign(userData, SECRET, { expiresIn: EXPIRES_IN });
|
||||
}
|
||||
@@ -19,14 +18,16 @@ export function decryptToken(token: string) {
|
||||
export function authenticate(user, password) {
|
||||
let JWTToken;
|
||||
|
||||
// hash user password
|
||||
const hashedPW = new jsSHA('SHA-512', 'TEXT').update(password).getHash('HEX');
|
||||
|
||||
const getUserSQLStmt = 'SELECT name, pw FROM users WHERE name = ?';
|
||||
const row = db.prepare(getUserSQLStmt).get(user);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
const storedPW = row.pw;
|
||||
|
||||
if (hashedPW && hashedPW === storedPW) {
|
||||
const isValid = bcrypt.compareSync(password, storedPW)
|
||||
if (isValid) {
|
||||
JWTToken = createToken({ id: user, admin: true });
|
||||
}
|
||||
|
||||
|
||||
51
src/lib/server/userService.ts
Normal file
51
src/lib/server/userService.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { db } from '$lib/server/dbService';
|
||||
|
||||
export const getUsers = (): { userId: string; userName: string }[] => {
|
||||
const getUsersSQLStmt = `SELECT id, name
|
||||
FROM users;`;
|
||||
const statement = db.prepare(getUsersSQLStmt);
|
||||
const result = statement.all() as { id: string; name: string }[];
|
||||
const userList: { userId: string; userName: string }[] = [];
|
||||
|
||||
for (const resultItem of result) {
|
||||
const user = { userId: resultItem.id, userName: resultItem.name };
|
||||
userList.push(user);
|
||||
}
|
||||
|
||||
return userList;
|
||||
};
|
||||
|
||||
export const addUser = (userName: string, userPassword: string) => {
|
||||
const addUserSQLStmt = `INSERT into users(name, pw)
|
||||
values (?, ?)`;
|
||||
const statement = db.prepare(addUserSQLStmt);
|
||||
|
||||
let rowInfo;
|
||||
try {
|
||||
rowInfo = statement.run(userName, userPassword);
|
||||
return rowInfo;
|
||||
} catch (error) {
|
||||
console.error('ERROR: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUser = (userId: string) => {
|
||||
// make sure to not delete the last entry
|
||||
const deleteUserSQLStmt = `DELETE
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
AND (SELECT COUNT(*) FROM users) > 1;`;
|
||||
|
||||
const statement = db.prepare(deleteUserSQLStmt);
|
||||
|
||||
let rowCount;
|
||||
try {
|
||||
const info = statement.run(userId);
|
||||
rowCount = info.changes;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
rowCount = 0;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
};
|
||||
@@ -42,6 +42,18 @@
|
||||
<p class="mt-1 text-gray-600">Fügen Sie einem Tatort Bilder hinzu.</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
|
||||
<div
|
||||
class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white"
|
||||
>
|
||||
<FileRect class=" group-hover:text-indigo-600" {outline} />
|
||||
</div>
|
||||
<a href="/user-management" class="mt-6 block font-semibold text-gray-900">
|
||||
Benutzerverwaltung
|
||||
<span class="absolute inset-0"></span>
|
||||
</a>
|
||||
<p class="mt-1 text-gray-600">Füge neue Benutzer hinzu oder entferne welche.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
212
src/routes/(angemeldet)/user-management/+page.svelte
Normal file
212
src/routes/(angemeldet)/user-management/+page.svelte
Normal file
@@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
|
||||
import jsSHA from 'jssha';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
let userName = $state('');
|
||||
let userPassword = $state('');
|
||||
let userList: { userId: string; userName: string }[] = $state([]);
|
||||
let addUserError = $state(false);
|
||||
let addUserSuccess = $state(false);
|
||||
const currentUser: string = data.user.id;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
userList = await getUsers();
|
||||
} catch (error) {
|
||||
console.log(`An error occured while retrieving users: ${error}`);
|
||||
|
trachi93 marked this conversation as resolved
Outdated
|
||||
}
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
sowas kann man ganz gut in eine constante auslagern also ein enum oder objekt, welches alle routen hält und ich diese dann nur referenzieren muss. das sind solche "magic strings" sowas kann man ganz gut in eine constante auslagern also ein enum oder objekt, welches alle routen hält und ich diese dann nur referenzieren muss. das sind solche "magic strings"
trachi93
commented
müsste ganzheitlich getan werden -> Ticket angelegt müsste ganzheitlich getan werden -> Ticket angelegt
|
||||
});
|
||||
|
||||
async function getUsers() {
|
||||
const URL = '/api/users';
|
||||
|
||||
try {
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
Try Catch block Try Catch block
|
||||
const response = await fetch(URL);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.log(`Error fetching users: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addUser() {
|
||||
if (userName == '') {
|
||||
alert('Der Benutzername darf nicht leer sein.');
|
||||
|
jared marked this conversation as resolved
Outdated
jared
commented
s.o. s.o.
|
||||
return;
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
Das hashen von Passwörtern solle stets im Backend erfolgen. Hier sollte auch auf eine sichere Methode geachtet werden die noch eine Salt wert einfügt z.B. bycrpty o.ä. Das hashen von Passwörtern solle stets im Backend erfolgen. Hier sollte auch auf eine sichere Methode geachtet werden die noch eine Salt wert einfügt z.B. bycrpty o.ä.
trachi93
commented
manuelles hashen wurde durch bcrypt ausgetauscht. manuelles hashen wurde durch bcrypt ausgetauscht.
|
||||
}
|
||||
|
||||
if (userPassword == '') {
|
||||
alert('Das Passwort darf nicht leer sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const URL = '/api/users';
|
||||
const userData = { userName: userName, userPassword: userPassword };
|
||||
|
||||
try {
|
||||
const response = await fetch(URL, {
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
Performanter wäre es wenn du dir nach dem Erstellen des Users, dieses zurück liefern lässt. Damit reduzierst du Traffic sowas wie Performanter wäre es wenn du dir nach dem Erstellen des Users, dieses zurück liefern lässt. Damit reduzierst du Traffic
sowas wie
const newUser = await response.json();
userList = [...userList, newUser];
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newUser = await response.json();
|
||||
userList = [...userList, newUser];
|
||||
addUserSuccess = true;
|
||||
resetInput();
|
||||
} else {
|
||||
addUserError = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error creating user: ${error}`);
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
try catch try catch
|
||||
addUserError = true;
|
||||
}
|
||||
}
|
||||
|
||||
function resetInput() {
|
||||
userName = '';
|
||||
userPassword = '';
|
||||
addUserError = false;
|
||||
setInterval(() => {
|
||||
addUserSuccess = false;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function deleteUser(userId: string) {
|
||||
const URL = `/api/users/${userId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(URL, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status == 204) {
|
||||
userList = await getUsers();
|
||||
} else {
|
||||
alert('Nutzer konnte nicht gelöscht werden');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Error deleting users: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<h1 class="flex justify-center text-3xl md:text-4xl font-bold text-gray-800 dark:text-white tracking-tight mb-4">
|
||||
Benutzerverwaltung
|
||||
</h1>
|
||||
|
||||
<h1 class="flex justify-center text-lg md:text-xl font-medium text-gray-800 dark:text-white tracking-tight mb-2">
|
||||
Benutzerliste
|
||||
</h1>
|
||||
|
||||
<div class="w-1/4 mx-auto">
|
||||
<table class="min-w-full border border-gray-300 rounded overflow-hidden">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2">Benutzername</th>
|
||||
<th class="text-center px-4 py-2">Entfernen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each userList as userItem}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-600">
|
||||
<td class="px-4 py-2 text-gray-800 dark:text-white">{userItem.userName}</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<button
|
||||
class="text-red-600 hover:text-red-800 focus:outline-none focus:ring-2 focus:ring-red-500 rounded-full p-1 transition"
|
||||
on:click={() => deleteUser(userItem.userId)}
|
||||
aria-label="Delete user"
|
||||
>
|
||||
{#if (userItem.userName != currentUser)}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h1 style="margin-top: 50px;"
|
||||
class="flex justify-center text-lg md:text-xl font-medium text-gray-800 dark:text-white tracking-tight mb-2">
|
||||
Neuer Nutzer
|
||||
</h1>
|
||||
|
||||
<div class="mx-auto flex justify-center">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="username">Benutzername</label>
|
||||
<input bind:value={userName} type="text" id="username" placeholder="Namen eingeben" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input bind:value={userPassword} type="password" id="password" placeholder="Passwort vergeben" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto flex flex-col items-center space-y-4" style="margin-top: 20px;">
|
||||
{#if addUserError}
|
||||
<div class="flex items-center bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-sm"
|
||||
role="alert">
|
||||
<svg class="h-5 w-5 mr-3 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01M12 5a7 7 0 100 14 7 7 0 000-14z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Der Benutzer konnte nicht hinzugefügt werden.</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if addUserSuccess}
|
||||
<div class="flex items-center bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-sm"
|
||||
role="alert">
|
||||
<svg class="h-5 w-5 mr-3 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01M12 5a7 7 0 100 14 7 7 0 000-14z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Der Benutzer wurde hinzugefügt.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button on:click={addUser}>Benutzer hinzufügen</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
38
src/routes/api/users/+server.ts
Normal file
38
src/routes/api/users/+server.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { addUser, getUsers } from '$lib/server/userService';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
const saltRounds = 12;
|
||||
|
||||
export function GET({ locals }) {
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userList = getUsers();
|
||||
|
||||
return new Response(JSON.stringify(userList));
|
||||
}
|
||||
|
||||
export async function POST({ request, locals }) {
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const userName = data.userName;
|
||||
const userPassword = data.userPassword;
|
||||
|
||||
if (!userName || !userPassword) {
|
||||
return json({ error: 'Missing input' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
trachi93 marked this conversation as resolved
Outdated
jared
commented
status code für created ist 201 status code für created ist 201
|
||||
const hashedPassword = bcrypt.hashSync(userPassword, saltRounds);
|
||||
const rowInfo = addUser(userName, hashedPassword);
|
||||
|
||||
if (rowInfo?.changes == 1) {
|
||||
return json({ userId: rowInfo.lastInsertRowid, userName: userName }, { status: 201 });
|
||||
} else {
|
||||
return new Response(null, { status: 400 });
|
||||
}
|
||||
}
|
||||
13
src/routes/api/users/[user]/+server.ts
Normal file
13
src/routes/api/users/[user]/+server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { deleteUser } from '$lib/server/userService';
|
||||
|
||||
export async function DELETE({ params, locals }) {
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = params.user;
|
||||
const rowCount = deleteUser(userId);
|
||||
|
||||
return new Response(null, { status: rowCount == 1 ? 204 : 400 });
|
||||
}
|
||||
Reference in New Issue
Block a user
try catch block benutzen