fix(server): Code-Review-Korrekturen Inventory Sharing
- UserRow: inventoryId-Feld ergaenzt
- AdminRoutes: Delete nutzt findById() statt listAll().find{}
- AdminRoutes: PUT/POST inventory/new geben 404 wenn User nicht existiert
- index.html: Doppelten alten HTML-Content entfernt (277 Zeilen)
This commit is contained in:
parent
c03475e7e5
commit
2d4ebd63b0
3 changed files with 15 additions and 272 deletions
|
|
@ -16,7 +16,8 @@ internal data class UserRow(
|
||||||
val username: String,
|
val username: String,
|
||||||
val passwordHash: String,
|
val passwordHash: String,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val isAdmin: Boolean
|
val isAdmin: Boolean,
|
||||||
|
val inventoryId: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
internal class UserRepository {
|
internal class UserRepository {
|
||||||
|
|
@ -83,7 +84,8 @@ internal class UserRepository {
|
||||||
username = this[Users.username],
|
username = this[Users.username],
|
||||||
passwordHash = this[Users.passwordHash],
|
passwordHash = this[Users.passwordHash],
|
||||||
createdAt = this[Users.createdAt],
|
createdAt = this[Users.createdAt],
|
||||||
isAdmin = this[Users.isAdmin]
|
isAdmin = this[Users.isAdmin],
|
||||||
|
inventoryId = this[Users.inventoryId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ internal fun Route.adminRoutes(userRepository: UserRepository, inventoryReposito
|
||||||
call.respond(HttpStatusCode.Conflict, ErrorResponse(status = 409, message = "Cannot delete your own account"))
|
call.respond(HttpStatusCode.Conflict, ErrorResponse(status = 409, message = "Cannot delete your own account"))
|
||||||
return@delete
|
return@delete
|
||||||
}
|
}
|
||||||
val userDto = userRepository.listAll().find { it.id == id }
|
val userRow = userRepository.findById(id)
|
||||||
val oldInventoryId = userDto?.inventoryId
|
val oldInventoryId = userRow?.inventoryId
|
||||||
val deleted = userRepository.deleteById(id)
|
val deleted = userRepository.deleteById(id)
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "User not found"))
|
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "User not found"))
|
||||||
|
|
@ -111,6 +111,10 @@ internal fun Route.adminRoutes(userRepository: UserRepository, inventoryReposito
|
||||||
HttpStatusCode.BadRequest, ErrorResponse(status = 400, message = "Missing user id")
|
HttpStatusCode.BadRequest, ErrorResponse(status = 400, message = "Missing user id")
|
||||||
)
|
)
|
||||||
val request = call.receive<AssignInventoryRequest>()
|
val request = call.receive<AssignInventoryRequest>()
|
||||||
|
if (userRepository.findById(id) == null) {
|
||||||
|
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "User not found"))
|
||||||
|
return@put
|
||||||
|
}
|
||||||
if (!inventoryRepository.inventoryExists(request.inventoryId)) {
|
if (!inventoryRepository.inventoryExists(request.inventoryId)) {
|
||||||
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "Inventory not found"))
|
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "Inventory not found"))
|
||||||
return@put
|
return@put
|
||||||
|
|
@ -133,6 +137,10 @@ internal fun Route.adminRoutes(userRepository: UserRepository, inventoryReposito
|
||||||
val id = call.parameters["id"] ?: return@post call.respond(
|
val id = call.parameters["id"] ?: return@post call.respond(
|
||||||
HttpStatusCode.BadRequest, ErrorResponse(status = 400, message = "Missing user id")
|
HttpStatusCode.BadRequest, ErrorResponse(status = 400, message = "Missing user id")
|
||||||
)
|
)
|
||||||
|
if (userRepository.findById(id) == null) {
|
||||||
|
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "User not found"))
|
||||||
|
return@post
|
||||||
|
}
|
||||||
val newInventoryId = inventoryRepository.createInventory()
|
val newInventoryId = inventoryRepository.createInventory()
|
||||||
val oldInventoryId = inventoryRepository.assignUserToInventory(id, newInventoryId)
|
val oldInventoryId = inventoryRepository.assignUserToInventory(id, newInventoryId)
|
||||||
if (oldInventoryId != null && oldInventoryId != newInventoryId) {
|
if (oldInventoryId != null && oldInventoryId != newInventoryId) {
|
||||||
|
|
|
||||||
|
|
@ -376,271 +376,4 @@
|
||||||
</script>
|
</script>
|
||||||
<footer>Krisenvorrat Server v0.2 · © 2026 faenocasul</footer>
|
<footer>Krisenvorrat Server v0.2 · © 2026 faenocasul</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Krisenvorrat – Admin</title>
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body { font-family: system-ui, sans-serif; background: #f5f5f5; color: #222; }
|
|
||||||
header { background: #1a1a2e; color: #fff; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
|
|
||||||
header h1 { font-size: 1.2rem; }
|
|
||||||
#logout-btn { background: transparent; border: 1px solid #ccc; color: #ccc; padding: 6px 14px; border-radius: 4px; cursor: pointer; }
|
|
||||||
main { max-width: 900px; margin: 32px auto; padding: 0 16px; }
|
|
||||||
#login-section, #admin-section { background: #fff; border-radius: 8px; padding: 32px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
|
|
||||||
h2 { margin-bottom: 20px; font-size: 1.1rem; }
|
|
||||||
label { display: block; margin-bottom: 4px; font-size: .875rem; color: #555; }
|
|
||||||
input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 14px; font-size: 1rem; }
|
|
||||||
button.primary { background: #1a1a2e; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
|
||||||
button.primary:hover { background: #2d2d5e; }
|
|
||||||
button.danger { background: #c0392b; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; }
|
|
||||||
button.secondary { background: #eee; color: #333; border: 1px solid #ccc; padding: 6px 12px; border-radius: 4px; cursor: pointer; }
|
|
||||||
.error { color: #c0392b; margin-bottom: 12px; font-size: .875rem; }
|
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
|
|
||||||
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: .9rem; }
|
|
||||||
th { background: #f9f9f9; font-weight: 600; }
|
|
||||||
.actions { display: flex; gap: 8px; }
|
|
||||||
#add-user-form { display: none; background: #f9f9f9; border: 1px solid #ddd; border-radius: 6px; padding: 20px; margin-top: 16px; }
|
|
||||||
#add-user-form.open { display: block; }
|
|
||||||
#add-user-form h3 { margin-bottom: 14px; font-size: 1rem; }
|
|
||||||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 100; justify-content: center; align-items: center; }
|
|
||||||
.modal-overlay.open { display: flex; }
|
|
||||||
.modal { background: #fff; border-radius: 8px; padding: 28px; width: 360px; }
|
|
||||||
.modal h3 { margin-bottom: 16px; }
|
|
||||||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
|
|
||||||
footer { text-align: center; padding: 24px 16px; margin-top: 40px; font-size: .8rem; color: #999; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>Krisenvorrat Admin</h1>
|
|
||||||
<button id="logout-btn" style="display:none" onclick="logout()">Abmelden</button>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section id="login-section">
|
|
||||||
<h2>Anmelden</h2>
|
|
||||||
<div id="login-error" class="error" style="display:none"></div>
|
|
||||||
<label>Benutzername</label>
|
|
||||||
<input id="login-username" type="text" autocomplete="username">
|
|
||||||
<label>Passwort</label>
|
|
||||||
<input id="login-password" type="password" autocomplete="current-password">
|
|
||||||
<button class="primary" onclick="login()">Anmelden</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-section" style="display:none">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px">
|
|
||||||
<h2>Benutzerverwaltung</h2>
|
|
||||||
<button class="primary" onclick="toggleAddForm()">+ Benutzer anlegen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="add-user-form">
|
|
||||||
<h3>Neuen Benutzer anlegen</h3>
|
|
||||||
<div id="add-user-error" class="error" style="display:none"></div>
|
|
||||||
<label>Benutzername</label>
|
|
||||||
<input id="new-username" type="text">
|
|
||||||
<label>Passwort</label>
|
|
||||||
<input id="new-password" type="password">
|
|
||||||
<div style="display:flex; gap:10px">
|
|
||||||
<button class="primary" onclick="createUser()">Anlegen</button>
|
|
||||||
<button class="secondary" onclick="toggleAddForm()">Abbrechen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table id="users-table">
|
|
||||||
<thead><tr><th>Benutzername</th><th>Erstellt</th><th>Admin</th><th>Aktionen</th></tr></thead>
|
|
||||||
<tbody id="users-body"></tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Change Password Modal -->
|
|
||||||
<div id="pw-modal" class="modal-overlay">
|
|
||||||
<div class="modal">
|
|
||||||
<h3>Passwort ändern</h3>
|
|
||||||
<div id="pw-error" class="error" style="display:none"></div>
|
|
||||||
<label>Neues Passwort</label>
|
|
||||||
<input id="pw-input" type="password">
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="secondary" onclick="closeModal()">Abbrechen</button>
|
|
||||||
<button class="primary" onclick="confirmPasswordChange()">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete Confirm Modal -->
|
|
||||||
<div id="del-modal" class="modal-overlay">
|
|
||||||
<div class="modal">
|
|
||||||
<h3>Benutzer löschen?</h3>
|
|
||||||
<p id="del-confirm-text" style="margin-bottom:16px; font-size:.9rem; color:#555"></p>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="secondary" onclick="closeModal()">Abbrechen</button>
|
|
||||||
<button class="danger" onclick="confirmDelete()">Löschen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let accessToken = sessionStorage.getItem('accessToken') || '';
|
|
||||||
let pendingUserId = null;
|
|
||||||
|
|
||||||
if (accessToken) tryLoadUsers();
|
|
||||||
|
|
||||||
async function login() {
|
|
||||||
const username = document.getElementById('login-username').value;
|
|
||||||
const password = document.getElementById('login-password').value;
|
|
||||||
const err = document.getElementById('login-error');
|
|
||||||
err.style.display = 'none';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password })
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
err.textContent = (await res.json()).message || 'Login fehlgeschlagen';
|
|
||||||
err.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
accessToken = data.accessToken;
|
|
||||||
sessionStorage.setItem('accessToken', accessToken);
|
|
||||||
showAdmin();
|
|
||||||
} catch (e) {
|
|
||||||
err.textContent = 'Verbindungsfehler';
|
|
||||||
err.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAdmin() {
|
|
||||||
document.getElementById('login-section').style.display = 'none';
|
|
||||||
document.getElementById('admin-section').style.display = 'block';
|
|
||||||
document.getElementById('logout-btn').style.display = 'inline-block';
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryLoadUsers() {
|
|
||||||
fetch('/api/admin/users', { headers: { 'Authorization': 'Bearer ' + accessToken } })
|
|
||||||
.then(r => r.ok ? (showAdmin(), r.json()) : Promise.reject())
|
|
||||||
.then(renderUsers)
|
|
||||||
.catch(() => { accessToken = ''; sessionStorage.removeItem('accessToken'); });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers() {
|
|
||||||
const res = await fetch('/api/admin/users', { headers: { 'Authorization': 'Bearer ' + accessToken } });
|
|
||||||
if (!res.ok) { logout(); return; }
|
|
||||||
renderUsers(await res.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderUsers(users) {
|
|
||||||
const tbody = document.getElementById('users-body');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
users.forEach(u => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
const tdName = document.createElement('td');
|
|
||||||
tdName.textContent = u.username;
|
|
||||||
const tdDate = document.createElement('td');
|
|
||||||
tdDate.textContent = new Date(u.createdAt).toLocaleDateString('de-DE');
|
|
||||||
const tdAdmin = document.createElement('td');
|
|
||||||
tdAdmin.textContent = u.isAdmin ? '✓' : '';
|
|
||||||
const tdActions = document.createElement('td');
|
|
||||||
tdActions.className = 'actions';
|
|
||||||
const btnPw = document.createElement('button');
|
|
||||||
btnPw.className = 'secondary';
|
|
||||||
btnPw.textContent = 'PW ändern';
|
|
||||||
btnPw.onclick = () => openPasswordModal(u.id);
|
|
||||||
const btnDel = document.createElement('button');
|
|
||||||
btnDel.className = 'danger';
|
|
||||||
btnDel.textContent = 'Löschen';
|
|
||||||
btnDel.onclick = () => openDeleteModal(u.id, u.username);
|
|
||||||
tdActions.append(btnPw, btnDel);
|
|
||||||
tr.append(tdName, tdDate, tdAdmin, tdActions);
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAddForm() {
|
|
||||||
document.getElementById('add-user-form').classList.toggle('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser() {
|
|
||||||
const username = document.getElementById('new-username').value;
|
|
||||||
const password = document.getElementById('new-password').value;
|
|
||||||
const err = document.getElementById('add-user-error');
|
|
||||||
err.style.display = 'none';
|
|
||||||
const res = await fetch('/api/admin/users', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + accessToken },
|
|
||||||
body: JSON.stringify({ username, password })
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
err.textContent = (await res.json()).message || 'Fehler';
|
|
||||||
err.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById('new-username').value = '';
|
|
||||||
document.getElementById('new-password').value = '';
|
|
||||||
toggleAddForm();
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPasswordModal(userId) {
|
|
||||||
pendingUserId = userId;
|
|
||||||
document.getElementById('pw-input').value = '';
|
|
||||||
document.getElementById('pw-error').style.display = 'none';
|
|
||||||
document.getElementById('pw-modal').classList.add('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmPasswordChange() {
|
|
||||||
const password = document.getElementById('pw-input').value;
|
|
||||||
const err = document.getElementById('pw-error');
|
|
||||||
err.style.display = 'none';
|
|
||||||
const res = await fetch('/api/admin/users/' + pendingUserId, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + accessToken },
|
|
||||||
body: JSON.stringify({ password })
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
err.textContent = (await res.json()).message || 'Fehler';
|
|
||||||
err.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeleteModal(userId, username) {
|
|
||||||
pendingUserId = userId;
|
|
||||||
document.getElementById('del-confirm-text').textContent = `Benutzer "${username}" wirklich löschen?`;
|
|
||||||
document.getElementById('del-modal').classList.add('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmDelete() {
|
|
||||||
await fetch('/api/admin/users/' + pendingUserId, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Authorization': 'Bearer ' + accessToken }
|
|
||||||
});
|
|
||||||
closeModal();
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('open'));
|
|
||||||
pendingUserId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function logout() {
|
|
||||||
accessToken = '';
|
|
||||||
sessionStorage.removeItem('accessToken');
|
|
||||||
document.getElementById('login-username').value = '';
|
|
||||||
document.getElementById('login-password').value = '';
|
|
||||||
document.getElementById('login-section').style.display = 'block';
|
|
||||||
document.getElementById('admin-section').style.display = 'none';
|
|
||||||
document.getElementById('logout-btn').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('login-password').addEventListener('keydown', e => { if (e.key === 'Enter') login(); });
|
|
||||||
</script>
|
|
||||||
<footer>Krisenvorrat Server v0.2 · © 2026 faenocasul</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in a new issue