fix: superadmin access for sessions/rooms; redesign room editor
Some checks failed
Release / release (push) Failing after 1m42s
Some checks failed
Release / release (push) Failing after 1m42s
Backend: - sessions.rs: add is_superadmin bypass to list_sessions, create_session, create_slot, update_slot_status, delete_slot - rooms.rs: allow empty layout on create_room (layout is built in editor after creation) Frontend room editor: - Fix drag coordinate sync: replace window mousemove delta calc with SVG CTM inverse transform (getScreenCTM), eliminating the 3x movement ratio bug - Switch to pointer capture (setPointerCapture) per element; remove svelte:window handlers - Positions always snap to 0.5 grid on drop, preventing backend validation errors - Left floating sidebar toolbar (collapsible) with Sitz/Tisch/ Tür/Lücke buttons and inline SVG icons - Room dimensions bar (Breite × Tiefe, 5–50 grid units) - Zoom controls (25%–400%) via scroll wheel or +/- buttons; SVG scales via width/height attrs so scrollbars work correctly - Property panel inputs snap to 0.5 on blur (prevents save errors) - Canvas bounded to room dimensions during drag
This commit is contained in:
@@ -104,7 +104,9 @@ async fn create_room(
|
||||
State(pool): State<SqlitePool>,
|
||||
Json(req): Json<CreateRoom>,
|
||||
) -> Result<(StatusCode, Json<Value>), AppError> {
|
||||
validate_layout(&req.layout)?;
|
||||
if !req.layout.is_empty() {
|
||||
validate_layout(&req.layout)?;
|
||||
}
|
||||
let layout_json = serde_json::to_string(&req.layout)
|
||||
.map_err(|e| AppError::BadRequest(format!("layout serialization error: {e}")))?;
|
||||
let id = sqlx::query("INSERT INTO rooms (name, layout_json) VALUES (?, ?)")
|
||||
|
||||
@@ -33,7 +33,9 @@ async fn list_sessions(
|
||||
State(pool): State<SqlitePool>,
|
||||
Query(q): Query<SessionQuery>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
super::verify_tutor_course_access(&pool, claims.sub, q.course_id).await?;
|
||||
if !claims.is_superadmin {
|
||||
super::verify_tutor_course_access(&pool, claims.sub, q.course_id).await?;
|
||||
}
|
||||
|
||||
let sessions = sqlx::query_as::<_, Session>(
|
||||
"SELECT id, course_id, week_nr, date FROM sessions WHERE course_id = ? ORDER BY date, week_nr",
|
||||
@@ -74,7 +76,9 @@ async fn create_session(
|
||||
chrono::NaiveDate::parse_from_str(&req.date, "%Y-%m-%d")
|
||||
.map_err(|_| AppError::BadRequest("date must be YYYY-MM-DD".into()))?;
|
||||
|
||||
super::verify_tutor_course_access(&pool, claims.sub, req.course_id).await?;
|
||||
if !claims.is_superadmin {
|
||||
super::verify_tutor_course_access(&pool, claims.sub, req.course_id).await?;
|
||||
}
|
||||
|
||||
let id = sqlx::query("INSERT INTO sessions (course_id, week_nr, date) VALUES (?, ?, ?)")
|
||||
.bind(req.course_id)
|
||||
@@ -105,23 +109,25 @@ async fn create_slot(
|
||||
.await?;
|
||||
let (course_id,) = session_row.ok_or(AppError::NotFound)?;
|
||||
|
||||
// Verify requesting tutor has access to the course
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
if !claims.is_superadmin {
|
||||
// Verify requesting tutor has access to the course
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
|
||||
// Verify the slot's tutor_id belongs to this course AND is active
|
||||
let member: Option<(i64,)> = sqlx::query_as(
|
||||
"SELECT 1 FROM tutor_courses tc
|
||||
JOIN tutors t ON t.id = tc.tutor_id
|
||||
WHERE tc.tutor_id = ? AND tc.course_id = ? AND t.is_active = 1",
|
||||
)
|
||||
.bind(req.tutor_id)
|
||||
.bind(course_id)
|
||||
.fetch_optional(&pool)
|
||||
.await?;
|
||||
if member.is_none() {
|
||||
return Err(AppError::BadRequest(
|
||||
"Tutor:in ist in diesem Kurs nicht aktiv oder existiert nicht.".into(),
|
||||
));
|
||||
// Verify the slot's tutor_id belongs to this course AND is active
|
||||
let member: Option<(i64,)> = sqlx::query_as(
|
||||
"SELECT 1 FROM tutor_courses tc
|
||||
JOIN tutors t ON t.id = tc.tutor_id
|
||||
WHERE tc.tutor_id = ? AND tc.course_id = ? AND t.is_active = 1",
|
||||
)
|
||||
.bind(req.tutor_id)
|
||||
.bind(course_id)
|
||||
.fetch_optional(&pool)
|
||||
.await?;
|
||||
if member.is_none() {
|
||||
return Err(AppError::BadRequest(
|
||||
"Tutor:in ist in diesem Kurs nicht aktiv oder existiert nicht.".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally verify room exists
|
||||
@@ -183,7 +189,9 @@ async fn update_slot_status(
|
||||
.await?;
|
||||
let (course_id, existing_code) = row.ok_or(AppError::NotFound)?;
|
||||
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
if !claims.is_superadmin {
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
}
|
||||
|
||||
// If transitioning to open and no code yet, generate one (retry on UNIQUE collision)
|
||||
let code: Option<String> = if req.status == "open" {
|
||||
@@ -262,7 +270,9 @@ async fn delete_slot(
|
||||
));
|
||||
}
|
||||
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
if !claims.is_superadmin {
|
||||
super::verify_tutor_course_access(&pool, claims.sub, course_id).await?;
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM slots WHERE id = ?")
|
||||
.bind(slot_id)
|
||||
|
||||
Reference in New Issue
Block a user