diff --git a/backend/demo/demo_seed.sql b/backend/demo/demo_seed.sql index c7d4c02..68170c5 100644 --- a/backend/demo/demo_seed.sql +++ b/backend/demo/demo_seed.sql @@ -1,7 +1,7 @@ -- Demo Seed Data -- Tutor/Admin Account (Password: admin) -INSERT OR IGNORE INTO tutors (id, name, email, password_hash) -VALUES (1, 'Demo Admin', 'admin@tutortool.com', '$2b$12$ted9u9ZsxbjhnWvTYsijMul138qhIKQG1RVsY8wGA3RFKZl8EaAsm'); +INSERT OR IGNORE INTO tutors (id, name, email, password_hash, is_superadmin) +VALUES (1, 'Demo Admin', 'admin@tutortool.com', '$2b$12$ted9u9ZsxbjhnWvTYsijMul138qhIKQG1RVsY8wGA3RFKZl8EaAsm', 1); -- Courses INSERT OR IGNORE INTO courses (id, name, semester) diff --git a/backend/migrations/002_add_superadmin.sql b/backend/migrations/002_add_superadmin.sql new file mode 100644 index 0000000..0467029 --- /dev/null +++ b/backend/migrations/002_add_superadmin.sql @@ -0,0 +1,2 @@ +-- Add is_superadmin column to tutors table +ALTER TABLE tutors ADD COLUMN is_superadmin BOOLEAN NOT NULL DEFAULT 0; diff --git a/backend/src/auth.rs b/backend/src/auth.rs index a3a9031..dc000d8 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -4,7 +4,12 @@ use serde::{Deserialize, Serialize}; use crate::error::AppError; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct TutorClaims { pub sub: i64, pub email: String, pub exp: u64 } +pub struct TutorClaims { + pub sub: i64, + pub email: String, + pub is_superadmin: bool, + pub exp: u64, +} fn secret() -> Result { std::env::var("JWT_SECRET").map_err(|_| { @@ -13,11 +18,20 @@ fn secret() -> Result { }) } -pub fn encode_jwt(id: i64, email: &str) -> Result { +pub fn encode_jwt(id: i64, email: &str, is_superadmin: bool) -> Result { let exp = (chrono::Utc::now() + chrono::Duration::days(7)).timestamp() as u64; - let claims = TutorClaims { sub: id, email: email.into(), exp }; - encode(&Header::default(), &claims, &EncodingKey::from_secret(secret()?.as_bytes())) - .map_err(|_| AppError::Unauthorized) + let claims = TutorClaims { + sub: id, + email: email.into(), + is_superadmin, + exp, + }; + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret()?.as_bytes()), + ) + .map_err(|_| AppError::Unauthorized) } pub fn decode_jwt(token: &str) -> Result { @@ -52,9 +66,10 @@ mod tests { unsafe { std::env::set_var("JWT_SECRET", "testsecret_auth"); } // roundtrip - let token = encode_jwt(1, "test@example.com").unwrap(); + let token = encode_jwt(1, "test@example.com", true).unwrap(); let claims = decode_jwt(&token).unwrap(); assert_eq!(claims.sub, 1); + assert!(claims.is_superadmin); // rejection assert!(decode_jwt("not.a.token").is_err()); diff --git a/backend/src/models.rs b/backend/src/models.rs index ce3c8b1..1e11a77 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -5,7 +5,12 @@ use serde::{Deserialize, Serialize}; pub struct Course { pub id: i64, pub name: String, pub semester: String } #[derive(Debug, Clone, Serialize, sqlx::FromRow)] -pub struct Tutor { pub id: i64, pub name: String, pub email: String } +pub struct Tutor { + pub id: i64, + pub name: String, + pub email: String, + pub is_superadmin: bool, +} #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct Student { pub id: i64, pub course_id: i64, pub name: String } @@ -60,6 +65,13 @@ pub struct LayoutElement { } #[derive(Deserialize)] pub struct UpsertNote { pub content: String } #[derive(Deserialize)] pub struct ManualAttendance { pub student_id: i64 } +#[derive(Deserialize)] pub struct CreateTutor { + pub name: String, + pub email: String, + pub password: String, + pub is_superadmin: bool, +} +#[derive(Deserialize)] pub struct AssignTutor { pub tutor_id: i64 } #[derive(Deserialize)] pub struct CheckinRequest { pub code: String, pub student_id: i64, diff --git a/backend/src/routes/auth_routes.rs b/backend/src/routes/auth_routes.rs index 87b5669..1c8e912 100644 --- a/backend/src/routes/auth_routes.rs +++ b/backend/src/routes/auth_routes.rs @@ -11,16 +11,16 @@ async fn login( State(pool): State, Json(req): Json, ) -> Result, AppError> { - let tutor: Option<(i64, String, String)> = sqlx::query_as( - "SELECT id, email, password_hash FROM tutors WHERE email = ?" + let tutor: Option<(i64, String, String, bool)> = sqlx::query_as( + "SELECT id, email, password_hash, is_superadmin FROM tutors WHERE email = ?" ).bind(&req.email).fetch_optional(&pool).await?; - let (id, email, hash) = tutor.ok_or(AppError::Unauthorized)?; + let (id, email, hash, is_superadmin) = tutor.ok_or(AppError::Unauthorized)?; if !bcrypt::verify(&req.password, &hash).unwrap_or(false) { return Err(AppError::Unauthorized); } - let token = auth::encode_jwt(id, &email)?; - Ok(Json(json!({"token": token}))) + let token = auth::encode_jwt(id, &email, is_superadmin)?; + Ok(Json(json!({"token": token, "is_superadmin": is_superadmin}))) } pub fn router() -> Router { @@ -44,7 +44,9 @@ mod tests { let (status, body) = post_json(app, "/api/auth/login", "", json!({"email":"t@test.com","password":"secret"})).await; assert_eq!(status, 200); - assert!(serde_json::from_slice::(&body).unwrap()["token"].is_string()); + let res = serde_json::from_slice::(&body).unwrap(); + assert!(res["token"].is_string()); + assert_eq!(res["is_superadmin"], false); } #[sqlx::test(migrations = "./migrations")] diff --git a/backend/src/routes/checkin.rs b/backend/src/routes/checkin.rs index 2103ad0..ec162be 100644 --- a/backend/src/routes/checkin.rs +++ b/backend/src/routes/checkin.rs @@ -289,7 +289,7 @@ pub fn router() -> Router { #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{build_test_app, get, patch_json, post_json}; + use crate::test_helpers::{build_test_admin_app, build_test_app, get, patch_json, post_json}; use axum::http::StatusCode; use serde_json::{json, Value}; @@ -298,7 +298,7 @@ mod tests { async fn seed_open_slot_with_room( pool: &sqlx::SqlitePool, ) -> (axum::Router, String, String, i64, i64, i64) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; // Course let (_, body) = post_json( @@ -314,7 +314,7 @@ mod tests { // Tutor enrollment let (tutor_id,): (i64,) = - sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(pool) .await .unwrap(); diff --git a/backend/src/routes/courses.rs b/backend/src/routes/courses.rs index 44be139..a61cf94 100644 --- a/backend/src/routes/courses.rs +++ b/backend/src/routes/courses.rs @@ -13,34 +13,102 @@ use crate::{ models::{Course, CreateCourse, CreateStudent, Student}, }; -// Fix 2: filter courses to only those the tutor is a member of +// Fix 2: filter courses to only those the tutor is a member of, or all if superadmin async fn list_courses( State(pool): State, claims: TutorClaims, ) -> Result>, AppError> { - let courses = sqlx::query_as::<_, Course>( - "SELECT c.id, c.name, c.semester FROM courses c - JOIN tutor_courses tc ON tc.course_id = c.id - WHERE tc.tutor_id = ?" - ) - .bind(claims.sub) - .fetch_all(&pool) - .await?; + let courses = if claims.is_superadmin { + sqlx::query_as::<_, Course>("SELECT id, name, semester FROM courses ORDER BY name") + .fetch_all(&pool) + .await? + } else { + sqlx::query_as::<_, Course>( + "SELECT c.id, c.name, c.semester FROM courses c + JOIN tutor_courses tc ON tc.course_id = c.id + WHERE tc.tutor_id = ?" + ) + .bind(claims.sub) + .fetch_all(&pool) + .await? + }; Ok(Json(courses)) } async fn create_course( - _claims: TutorClaims, + claims: TutorClaims, State(pool): State, Json(req): Json, ) -> Result<(StatusCode, Json), AppError> { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } let id = sqlx::query("INSERT INTO courses (name, semester) VALUES (?, ?)") .bind(&req.name) .bind(&req.semester) .execute(&pool) .await? .last_insert_rowid(); - Ok((StatusCode::CREATED, Json(json!({"id": id, "name": req.name, "semester": req.semester})))) + Ok(( + StatusCode::CREATED, + Json(json!({"id": id, "name": req.name, "semester": req.semester})), + )) +} + +async fn assign_tutor( + claims: TutorClaims, + State(pool): State, + Path(course_id): Path, + Json(req): Json, +) -> Result { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } + sqlx::query("INSERT OR IGNORE INTO tutor_courses (tutor_id, course_id) VALUES (?, ?)") + .bind(req.tutor_id) + .bind(course_id) + .execute(&pool) + .await?; + Ok(StatusCode::OK) +} + +async fn unassign_tutor( + claims: TutorClaims, + State(pool): State, + Path((course_id, tutor_id)): Path<(i64, i64)>, +) -> Result { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } + sqlx::query("DELETE FROM tutor_courses WHERE tutor_id = ? AND course_id = ?") + .bind(tutor_id) + .bind(course_id) + .execute(&pool) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn list_assigned_tutors( + claims: TutorClaims, + State(pool): State, + Path(course_id): Path, +) -> Result>, AppError> { + // Only superadmins or assigned tutors can see who else is assigned? + // Let's allow superadmins or anyone assigned to the course. + if !claims.is_superadmin { + super::verify_tutor_course_access(&pool, claims.sub, course_id).await?; + } + + let tutors = sqlx::query_as::<_, crate::models::Tutor>( + "SELECT t.id, t.name, t.email, t.is_superadmin FROM tutors t + JOIN tutor_courses tc ON tc.tutor_id = t.id + WHERE tc.course_id = ?" + ) + .bind(course_id) + .fetch_all(&pool) + .await?; + + Ok(Json(tutors)) } // Fix 3: verify tutor has access to this course @@ -179,20 +247,22 @@ pub fn router() -> Router { get(list_students).post(add_student), ) .route("/api/admin/courses/{id}/students/import", post(import_students)) + .route("/api/admin/courses/{id}/tutors", get(list_assigned_tutors).post(assign_tutor)) + .route("/api/admin/courses/{id}/tutors/{tutor_id}", delete(unassign_tutor)) .route("/api/admin/students/{id}", delete(delete_student)) } #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{build_test_app, delete, get, post_json}; + use crate::test_helpers::{build_test_admin_app, build_test_app, delete, get, post_json}; use axum::http::StatusCode; use serde_json::{json, Value}; use sqlx::SqlitePool; // Fix 6: helper to seed tutor_courses membership async fn add_tutor_to_course(pool: &SqlitePool, course_id: i64) { - let tutor_id: (i64,) = sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + let tutor_id: (i64,) = sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(pool) .await .unwrap(); @@ -206,7 +276,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn create_and_list_courses(pool: SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; let (status, body) = post_json( app.clone(), "/api/admin/courses", @@ -234,7 +304,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn create_and_list_students(pool: SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; // Create course let (_, body) = post_json( app.clone(), @@ -300,7 +370,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn delete_student_returns_204(pool: SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; // Create course and seed membership let (_, body) = post_json( diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 2bcbd57..5d8435f 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -11,6 +11,7 @@ mod sessions; mod attendance; mod notes; mod export; +mod tutors; pub fn build(pool: SqlitePool) -> Router { Router::new() @@ -22,6 +23,7 @@ pub fn build(pool: SqlitePool) -> Router { .merge(attendance::router()) .merge(notes::router()) .merge(export::router()) + .merge(tutors::router()) .with_state(pool) } diff --git a/backend/src/routes/sessions.rs b/backend/src/routes/sessions.rs index 951b032..d5cbaf4 100644 --- a/backend/src/routes/sessions.rs +++ b/backend/src/routes/sessions.rs @@ -284,7 +284,7 @@ pub fn router() -> Router { #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{build_test_app, delete, get, patch_json, post_json}; + use crate::test_helpers::{build_test_admin_app, build_test_app, delete, get, patch_json, post_json}; use axum::http::StatusCode; use serde_json::{json, Value}; use std::collections::HashSet; @@ -310,7 +310,7 @@ mod tests { // DB tests #[sqlx::test(migrations = "./migrations")] async fn create_and_list_sessions(pool: sqlx::SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; // Create a course + enroll the tutor first let (status, body) = post_json( app.clone(), @@ -325,7 +325,7 @@ mod tests { .unwrap(); // Enroll tutor in course (needed for verify_tutor_course_access) let tutor_id: (i64,) = - sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(&pool) .await .unwrap(); @@ -365,7 +365,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn create_slot_and_open(pool: sqlx::SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; // Setup: course, enroll tutor, session, slot let (_, body) = post_json( app.clone(), @@ -378,7 +378,7 @@ mod tests { .as_i64() .unwrap(); let tutor_id: (i64,) = - sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(&pool) .await .unwrap(); @@ -428,7 +428,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn slot_start_time_must_be_before_end_time(pool: sqlx::SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; let (_, body) = post_json( app.clone(), "/api/admin/courses", @@ -440,7 +440,7 @@ mod tests { .as_i64() .unwrap(); let tutor_id: (i64,) = - sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(&pool) .await .unwrap(); @@ -481,7 +481,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn delete_closed_slot(pool: sqlx::SqlitePool) { - let (app, auth) = build_test_app(pool.clone()).await; + let (app, auth) = build_test_admin_app(pool.clone()).await; let (_, body) = post_json( app.clone(), "/api/admin/courses", @@ -493,7 +493,7 @@ mod tests { .as_i64() .unwrap(); let tutor_id: (i64,) = - sqlx::query_as("SELECT id FROM tutors WHERE email = 'tutor@test.com'") + sqlx::query_as("SELECT id FROM tutors WHERE email = 'admin@test.com'") .fetch_one(&pool) .await .unwrap(); diff --git a/backend/src/routes/tutors.rs b/backend/src/routes/tutors.rs new file mode 100644 index 0000000..3a17b00 --- /dev/null +++ b/backend/src/routes/tutors.rs @@ -0,0 +1,86 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + routing::{get, post, delete}, + Json, Router, +}; +use sqlx::SqlitePool; +use crate::{auth::TutorClaims, error::AppError, models::{CreateTutor, Tutor}}; + +async fn list_tutors( + claims: TutorClaims, + State(pool): State, +) -> Result>, AppError> { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } + + let tutors = sqlx::query_as::<_, Tutor>( + "SELECT id, name, email, is_superadmin FROM tutors ORDER BY name" + ) + .fetch_all(&pool) + .await?; + + Ok(Json(tutors)) +} + +async fn create_tutor( + claims: TutorClaims, + State(pool): State, + Json(req): Json, +) -> Result<(StatusCode, Json), AppError> { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } + + let hash = bcrypt::hash(&req.password, 12).map_err(|_| AppError::Unauthorized)?; + + let id = sqlx::query( + "INSERT INTO tutors (name, email, password_hash, is_superadmin) VALUES (?, ?, ?, ?)" + ) + .bind(&req.name) + .bind(&req.email) + .bind(hash) + .bind(req.is_superadmin) + .execute(&pool) + .await? + .last_insert_rowid(); + + Ok(( + StatusCode::CREATED, + Json(Tutor { + id, + name: req.name, + email: req.email, + is_superadmin: req.is_superadmin, + }), + )) +} + +async fn delete_tutor( + claims: TutorClaims, + State(pool): State, + Path(id): Path, +) -> Result { + if !claims.is_superadmin { + return Err(AppError::Unauthorized); + } + + // Don't allow deleting yourself + if claims.sub == id { + return Err(AppError::Conflict("cannot delete yourself".into())); + } + + sqlx::query("DELETE FROM tutors WHERE id = ?") + .bind(id) + .execute(&pool) + .await?; + + Ok(StatusCode::NO_CONTENT) +} + +pub fn router() -> Router { + Router::new() + .route("/api/admin/tutors", get(list_tutors).post(create_tutor)) + .route("/api/admin/tutors/{id}", delete(delete_tutor)) +} diff --git a/backend/src/test_helpers.rs b/backend/src/test_helpers.rs index aeb267e..5082d91 100644 --- a/backend/src/test_helpers.rs +++ b/backend/src/test_helpers.rs @@ -6,21 +6,34 @@ use axum::http::{Request, StatusCode}; use http_body_util::BodyExt; /// Insert a test tutor (if not exists), return a valid JWT for that tutor. -pub async fn make_token(pool: &SqlitePool, email: &str) -> String { +pub async fn make_token(pool: &SqlitePool, email: &str, is_superadmin: bool) -> String { let hash = bcrypt::hash("testpass", 4).unwrap(); - sqlx::query("INSERT OR IGNORE INTO tutors (name,email,password_hash) VALUES (?,?,?)") - .bind("Test Tutor").bind(email).bind(&hash) + sqlx::query("INSERT OR IGNORE INTO tutors (name,email,password_hash,is_superadmin) VALUES (?,?,?,?)") + .bind("Test Tutor").bind(email).bind(&hash).bind(is_superadmin) .execute(pool).await.unwrap(); - let id: (i64,) = sqlx::query_as("SELECT id FROM tutors WHERE email = ?") + + // Ensure the superadmin flag is correct even if it existed + sqlx::query("UPDATE tutors SET is_superadmin = ? WHERE email = ?") + .bind(is_superadmin).bind(email).execute(pool).await.unwrap(); + + let row: (i64, bool) = sqlx::query_as("SELECT id, is_superadmin FROM tutors WHERE email = ?") .bind(email).fetch_one(pool).await.unwrap(); unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } - crate::auth::encode_jwt(id.0, email).unwrap() + crate::auth::encode_jwt(row.0, email, row.1).unwrap() } /// Build the full Axum app wired with the given pool, plus a Bearer auth header value. pub async fn build_test_app(pool: SqlitePool) -> (Router, String) { unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } - let token = make_token(&pool, "tutor@test.com").await; + let token = make_token(&pool, "tutor@test.com", false).await; + let app = crate::routes::build(pool); + (app, format!("Bearer {token}")) +} + +/// Build the full Axum app wired with a superadmin Bearer auth header. +pub async fn build_test_admin_app(pool: SqlitePool) -> (Router, String) { + unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } + let token = make_token(&pool, "admin@test.com", true).await; let app = crate::routes::build(pool); (app, format!("Bearer {token}")) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ba6b19a..8b3739d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -31,7 +31,7 @@ async function request(path: string, init?: RequestInit): Promise { export const api = { auth: { login: (email: string, password: string) => - request<{token: string}>('/auth/login', { + request<{token: string, is_superadmin: boolean}>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }), @@ -61,6 +61,23 @@ export const api = { body: formData }).then(res => res.json()); }, + listTutors: (course_id: number) => request(`/admin/courses/${course_id}/tutors`), + assignTutor: (course_id: number, tutor_id: number) => + request(`/admin/courses/${course_id}/tutors`, { + method: 'POST', + body: JSON.stringify({ tutor_id }) + }), + unassignTutor: (course_id: number, tutor_id: number) => + request(`/admin/courses/${course_id}/tutors/${tutor_id}`, { method: 'DELETE' }), + }, + tutors: { + list: () => request('/admin/tutors'), + create: (tutor: any) => + request('/admin/tutors', { + method: 'POST', + body: JSON.stringify(tutor) + }), + delete: (id: number) => request(`/admin/tutors/${id}`, { method: 'DELETE' }), }, students: { delete: (id: number) => request(`/admin/students/${id}`, { method: 'DELETE' }), diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index f93adc5..0da7510 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -5,6 +5,10 @@ export const token = writable( browser ? localStorage.getItem('token') : null ); +export const isSuperadmin = writable( + browser ? localStorage.getItem('is_superadmin') === 'true' : false +); + if (browser) { token.subscribe((value) => { if (value) { @@ -13,8 +17,12 @@ if (browser) { localStorage.removeItem('token'); } }); + isSuperadmin.subscribe((value) => { + localStorage.setItem('is_superadmin', value ? 'true' : 'false'); + }); } export function logout() { token.set(null); + isSuperadmin.set(false); } diff --git a/frontend/src/lib/components/TutorShell.svelte b/frontend/src/lib/components/TutorShell.svelte index a2784c0..ce79fba 100644 --- a/frontend/src/lib/components/TutorShell.svelte +++ b/frontend/src/lib/components/TutorShell.svelte @@ -1,5 +1,6 @@ @@ -36,24 +63,26 @@ - -
-
-
Neuen Kurs anlegen
- -
-
-
- - + + {#if $isSuperadmin} +
+
+
Neuen Kurs anlegen
+
-
- - -
- - -
+
+
+ + +
+
+ + +
+ +
+
+ {/if}
@@ -65,9 +94,10 @@ - - - + + {#if $isSuperadmin} + + {/if} @@ -75,12 +105,39 @@ {#each courses as course, i} - - + + {#if $isSuperadmin} + + {/if} +
#NameSemesterName / SemesterTutor:innenAktionen
- {i + 1} - {course.name} - {course.semester} +
{course.name}
+
{course.semester}
+
+ {#each assignedTutors[course.id] ?? [] as tutor} + + {tutor.name} + + + {/each} + + +
+
Studierende diff --git a/frontend/src/routes/admin/login/+page.svelte b/frontend/src/routes/admin/login/+page.svelte index ecea79a..e621c55 100644 --- a/frontend/src/routes/admin/login/+page.svelte +++ b/frontend/src/routes/admin/login/+page.svelte @@ -1,6 +1,6 @@ + +
+ + +
+
+
Systemverwaltung
+
+ Tutor:innen +
+
+
+ +
+ + +
+
+
Alle Benutzer:innen
+ +
+ +
+ {#if loading} +
+ Wird geladen… +
+ {:else if tutors.length === 0} +
+ Keine Tutor:innen gefunden. +
+ {:else} + + + + + + + + + + {#each tutors as tutor, i} + + + + + + {/each} + +
Name / E-MailRolleAktionen
+
+ + {initials(tutor.name)} + +
+
{tutor.name}
+
{tutor.email}
+
+
+
+ {#if tutor.is_superadmin} + Superadmin + {:else} + Tutor:in + {/if} + + +
+ {/if} +
+
+ + +
+
+
Hinzufügen
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+ +
+