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:
Jens Reinemann 2026-05-17 00:35:15 +02:00
parent c03475e7e5
commit 2d4ebd63b0
3 changed files with 15 additions and 272 deletions

View file

@ -16,7 +16,8 @@ internal data class UserRow(
val username: String,
val passwordHash: String,
val createdAt: Long,
val isAdmin: Boolean
val isAdmin: Boolean,
val inventoryId: String? = null
)
internal class UserRepository {
@ -83,7 +84,8 @@ internal class UserRepository {
username = this[Users.username],
passwordHash = this[Users.passwordHash],
createdAt = this[Users.createdAt],
isAdmin = this[Users.isAdmin]
isAdmin = this[Users.isAdmin],
inventoryId = this[Users.inventoryId]
)
}

View file

@ -62,8 +62,8 @@ internal fun Route.adminRoutes(userRepository: UserRepository, inventoryReposito
call.respond(HttpStatusCode.Conflict, ErrorResponse(status = 409, message = "Cannot delete your own account"))
return@delete
}
val userDto = userRepository.listAll().find { it.id == id }
val oldInventoryId = userDto?.inventoryId
val userRow = userRepository.findById(id)
val oldInventoryId = userRow?.inventoryId
val deleted = userRepository.deleteById(id)
if (!deleted) {
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")
)
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)) {
call.respond(HttpStatusCode.NotFound, ErrorResponse(status = 404, message = "Inventory not found"))
return@put
@ -133,6 +137,10 @@ internal fun Route.adminRoutes(userRepository: UserRepository, inventoryReposito
val id = call.parameters["id"] ?: return@post call.respond(
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 oldInventoryId = inventoryRepository.assignUserToInventory(id, newInventoryId)
if (oldInventoryId != null && oldInventoryId != newInventoryId) {

View file

@ -376,271 +376,4 @@
</script>
<footer>Krisenvorrat Server v0.2 &middot; &copy; 2026 faenocasul</footer>
</body>
</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 &middot; &copy; 2026 faenocasul</footer>
</body>
</html>
</html>