fix: address review findings — error handling, migration safety, CI audit
Backend: - migration 003: apply pixel→grid transform per-element (CASE WHEN > 50) instead of per-row, preventing double-conversion of mixed-scale rooms; skip empty arrays via json_array_length guard to avoid NULL assignment - attendance.rs: log layout JSON parse errors instead of silently swallowing them with .ok() - tutors.rs: check rows_affected() in set_tutor_active and return 404 for non-existent IDs; remap FK constraint errors on delete to 409 so concurrent inserts between conflict-check and DELETE don't surface as 500 Frontend: - live/[slotId]: expose polling failures to the tutor via error banner instead of only console.error - s/[code]: split checkin into two try/catch blocks so a successful POST followed by a failed reload doesn't report failure to the student; fix dead '409' string detection to match actual server error 'seat taken' - rooms/[roomId]: remove duplicate onMount fetch; add .catch() to $effect - tutors: expose loadTutors failures via error banner, not just console - rooms: fix bare catch in createRoom (captures error, shows message); add try/catch to onMount rooms load CI: - sync cargo audit --ignore RUSTSEC-2023-0071 with Makefile; the advisory is in rsa which sqlx-mysql retains in the lock file even when the mysql feature is disabled — aws_lc_rs correctly removes it from the active tree
This commit is contained in:
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Security audit
|
||||
run: |
|
||||
cargo install cargo-audit --locked
|
||||
cd backend && cargo audit
|
||||
cd backend && cargo audit --ignore RUSTSEC-2023-0071
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm --dir frontend build
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
-- Normalize room layout units: divide all coordinates/dimensions by 40 if they are in pixel-scale (e.g. > 50)
|
||||
-- Note: This is an idempotent approach by only updating rows with large values.
|
||||
-- Normalize room layout units: divide pixel-scale coordinates by 40.
|
||||
-- Applied per element with CASE WHEN so the transform is idempotent:
|
||||
-- coordinates already in grid units (≤50) are left untouched.
|
||||
UPDATE rooms
|
||||
SET layout_json = (
|
||||
SELECT json_group_array(
|
||||
json_object(
|
||||
'id', json_extract(value, '$.id'),
|
||||
'label', json_extract(value, '$.label'),
|
||||
'x', ROUND(CAST(json_extract(value, '$.x') AS REAL) / 40.0, 2),
|
||||
'y', ROUND(CAST(json_extract(value, '$.y') AS REAL) / 40.0, 2),
|
||||
'width', ROUND(CAST(json_extract(value, '$.width') AS REAL) / 40.0, 2),
|
||||
'height', ROUND(CAST(json_extract(value, '$.height') AS REAL) / 40.0, 2),
|
||||
'x', CASE WHEN CAST(json_extract(value, '$.x') AS REAL) > 50
|
||||
THEN ROUND(CAST(json_extract(value, '$.x') AS REAL) / 40.0, 2)
|
||||
ELSE json_extract(value, '$.x') END,
|
||||
'y', CASE WHEN CAST(json_extract(value, '$.y') AS REAL) > 50
|
||||
THEN ROUND(CAST(json_extract(value, '$.y') AS REAL) / 40.0, 2)
|
||||
ELSE json_extract(value, '$.y') END,
|
||||
'width', CASE WHEN CAST(json_extract(value, '$.width') AS REAL) > 50
|
||||
THEN ROUND(CAST(json_extract(value, '$.width') AS REAL) / 40.0, 2)
|
||||
ELSE json_extract(value, '$.width') END,
|
||||
'height', CASE WHEN CAST(json_extract(value, '$.height') AS REAL) > 50
|
||||
THEN ROUND(CAST(json_extract(value, '$.height') AS REAL) / 40.0, 2)
|
||||
ELSE json_extract(value, '$.height') END,
|
||||
'type', json_extract(value, '$.type')
|
||||
)
|
||||
)
|
||||
FROM json_each(rooms.layout_json)
|
||||
)
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM json_each(rooms.layout_json)
|
||||
WHERE json_extract(value, '$.x') > 50
|
||||
OR json_extract(value, '$.y') > 50
|
||||
OR json_extract(value, '$.width') > 50
|
||||
OR json_extract(value, '$.height') > 50
|
||||
);
|
||||
WHERE json_array_length(layout_json) > 0;
|
||||
|
||||
@@ -94,7 +94,13 @@ async fn get_session_attendance(
|
||||
let mut slots = Vec::new();
|
||||
for row in slot_rows {
|
||||
let layout: Option<Vec<crate::models::LayoutElement>> = match row.8 {
|
||||
Some(json_str) => serde_json::from_str(&json_str).ok(),
|
||||
Some(ref json_str) => match serde_json::from_str(json_str) {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
tracing::error!(slot_id = row.0, err = %e, "failed to deserialize room layout_json");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
slots.push(crate::models::Slot {
|
||||
|
||||
@@ -87,12 +87,16 @@ async fn set_tutor_active(
|
||||
return Err(AppError::Conflict("cannot deactivate yourself".into()));
|
||||
}
|
||||
|
||||
sqlx::query("UPDATE tutors SET is_active = ? WHERE id = ?")
|
||||
let result = sqlx::query("UPDATE tutors SET is_active = ? WHERE id = ?")
|
||||
.bind(req.is_active)
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -145,10 +149,19 @@ async fn delete_tutor(
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM tutors WHERE id = ?")
|
||||
match sqlx::query("DELETE FROM tutors WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(sqlx::Error::Database(e)) if e.message().contains("FOREIGN KEY") => {
|
||||
return Err(AppError::Conflict(
|
||||
"Tutor:in hat noch Verweise in der Datenbank.".into(),
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(AppError::Db(e)),
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
let loading = $state(true);
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let loadError = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
@@ -73,7 +74,8 @@
|
||||
if (found) break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
loadError = e instanceof Error ? e.message : 'Daten konnten nicht geladen werden.';
|
||||
console.error('[live/loadData]', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +139,12 @@
|
||||
|
||||
<div style="padding:28px 36px;display:flex;flex-direction:column;gap:22px">
|
||||
|
||||
{#if loadError}
|
||||
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px">
|
||||
Fehler beim Laden: {loadError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div style="padding:48px;text-align:center">
|
||||
<span class="small" style="color:var(--ink-4)">Wird geladen…</span>
|
||||
|
||||
@@ -5,19 +5,31 @@
|
||||
|
||||
let rooms = $state<Room[]>([]);
|
||||
let newRoomName = $state('');
|
||||
let errorMsg = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
rooms = await api.admin.rooms.list();
|
||||
try {
|
||||
rooms = await api.admin.rooms.list();
|
||||
} catch (e) {
|
||||
errorMsg = e instanceof Error ? e.message : 'Räume konnten nicht geladen werden.';
|
||||
}
|
||||
});
|
||||
|
||||
async function createRoom() {
|
||||
if (!newRoomName.trim()) return;
|
||||
errorMsg = null;
|
||||
try {
|
||||
await api.admin.rooms.create(newRoomName, []);
|
||||
newRoomName = '';
|
||||
} catch (e) {
|
||||
errorMsg = e instanceof Error ? e.message : 'Raum konnte nicht erstellt werden.';
|
||||
console.error('[rooms/createRoom]', e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
rooms = await api.admin.rooms.list();
|
||||
} catch {
|
||||
console.error('failed to fetch rooms');
|
||||
} catch (e) {
|
||||
console.error('[rooms/createRoom/list]', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +50,12 @@
|
||||
<h1 class="h1" style="font-family:var(--serif)">Raumlayout-Editor</h1>
|
||||
</div>
|
||||
|
||||
{#if errorMsg}
|
||||
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px;margin-bottom:12px">
|
||||
{errorMsg}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="card" style="overflow:hidden">
|
||||
<div style="padding:14px 18px;border-bottom:1px solid var(--rule);display:flex;align-items:center;justify-content:space-between">
|
||||
<div class="serif" style="font-size:18px;font-weight:500">Räume</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import RoomCanvas from '$lib/RoomCanvas.svelte';
|
||||
import type { Room, LayoutElement } from '$lib/types';
|
||||
@@ -12,13 +11,13 @@
|
||||
let errorMsg = $state<string | null>(null);
|
||||
let snapToGrid = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
room = await api.admin.rooms.get(roomId);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (roomId) {
|
||||
api.admin.rooms.get(roomId).then((r: Room) => { room = r; });
|
||||
api.admin.rooms.get(roomId)
|
||||
.then((r: Room) => { room = r; })
|
||||
.catch((e: unknown) => {
|
||||
errorMsg = e instanceof Error ? e.message : 'Raum konnte nicht geladen werden.';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
let tutors = $state<Tutor[]>([]);
|
||||
let loading = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
|
||||
let newTutor = $state({
|
||||
name: '',
|
||||
@@ -23,8 +24,10 @@
|
||||
async function loadTutors() {
|
||||
try {
|
||||
tutors = await api.admin.tutors.list();
|
||||
loadError = null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
loadError = e instanceof Error ? e.message : 'Tutor:innen konnten nicht geladen werden.';
|
||||
console.error('[tutors/loadTutors]', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +87,18 @@
|
||||
<UnderlineStroke width={120} />
|
||||
</div>
|
||||
|
||||
{#if loadError}
|
||||
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px;margin-bottom:12px">
|
||||
{loadError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="card" style="overflow:hidden">
|
||||
{#if loading}
|
||||
<div style="padding:32px;text-align:center">
|
||||
<span class="small" style="color:var(--ink-4)">Wird geladen…</span>
|
||||
</div>
|
||||
{:else if tutors.length === 0}
|
||||
{:else if tutors.length === 0 && !loadError}
|
||||
<div style="padding:32px;text-align:center">
|
||||
<span class="small" style="color:var(--ink-4)">Keine Tutor:innen gefunden.</span>
|
||||
</div>
|
||||
|
||||
@@ -82,17 +82,21 @@
|
||||
if (!selectedStudent) return;
|
||||
try {
|
||||
await api.checkin.post(code, selectedStudent.id, seatId);
|
||||
await loadInfo();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message?.includes('already checked in') || e.message?.includes('409')) {
|
||||
errorMsg = 'Dieser Platz ist bereits belegt.';
|
||||
} else {
|
||||
errorMsg = e.message ?? 'Einchecken fehlgeschlagen.';
|
||||
}
|
||||
errorMsg = e.message.includes('seat taken')
|
||||
? 'Dieser Platz ist bereits belegt.'
|
||||
: (e.message || 'Einchecken fehlgeschlagen.');
|
||||
} else {
|
||||
errorMsg = 'Einchecken fehlgeschlagen.';
|
||||
errorMsg = 'Einchecken fehlgeschlagen.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadInfo();
|
||||
} catch (e) {
|
||||
step = 'confirmed';
|
||||
console.error('[checkin/loadInfo]', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user