From 8c7678d06a77d3e7926593c618644d04086d6851 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Sun, 3 May 2026 00:41:50 +0200 Subject: [PATCH] feat: implement dual-token JWT auth, Argon2id migration, and zero-warnings quality mandate --- .gitea/workflows/ci.yml | 6 + .gitea/workflows/release.yml | 4 +- GEMINI.md | 25 +- Makefile | 6 +- backend/Cargo.toml | 5 +- backend/SECURITY.md | 22 + backend/src/auth.rs | 69 +- backend/src/main.rs | 44 + backend/src/routes/auth_routes.rs | 157 ++- backend/src/routes/sessions.rs | 4 +- backend/src/routes/tutors.rs | 11 +- backend/src/test_helpers.rs | 2 +- docs/tutortool_audit_perplexity_260502.md | 285 ++++++ frontend/eslint.config.js | 51 + frontend/package.json | 9 +- frontend/pnpm-lock.yaml | 945 +++++++++++++++++- frontend/src/lib/RoomCanvas.svelte | 7 +- frontend/src/lib/api.ts | 15 +- frontend/src/lib/auth.svelte.ts | 6 +- frontend/src/lib/components/NoteEditor.svelte | 8 +- frontend/src/lib/components/SeatMap.svelte | 4 +- frontend/src/lib/components/TutorShell.svelte | 2 +- frontend/src/lib/types.ts | 18 + frontend/src/routes/admin/+layout.svelte | 10 +- frontend/src/routes/admin/+page.svelte | 11 +- .../src/routes/admin/attendance/+page.svelte | 26 +- .../src/routes/admin/courses/+page.svelte | 11 +- frontend/src/routes/admin/export/+page.svelte | 16 +- .../routes/admin/live/[slotId]/+page.svelte | 9 +- frontend/src/routes/admin/rooms/+page.svelte | 6 +- .../routes/admin/rooms/[roomId]/+page.svelte | 7 +- .../src/routes/admin/sessions/+page.svelte | 22 +- .../src/routes/admin/students/+page.svelte | 7 +- frontend/src/routes/admin/tutors/+page.svelte | 10 +- frontend/src/routes/s/[code]/+page.svelte | 44 +- frontend/tests/global-setup.ts | 40 +- frontend/tsconfig.json | 2 + 37 files changed, 1712 insertions(+), 214 deletions(-) create mode 100644 backend/SECURITY.md create mode 100644 docs/tutortool_audit_perplexity_260502.md create mode 100644 frontend/eslint.config.js diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0352c7d..2bd4c4b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -47,6 +47,9 @@ jobs: - name: Install frontend deps run: pnpm --dir frontend install --frozen-lockfile + - name: JS security audit + run: pnpm --dir frontend audit --audit-level high + - name: Generate SvelteKit types run: pnpm --dir frontend exec svelte-kit sync @@ -65,6 +68,9 @@ jobs: - name: Type check (frontend) run: pnpm --dir frontend exec tsgo --version && pnpm --dir frontend check + - name: Lint (frontend) + run: pnpm --dir frontend lint + - name: Unit tests (backend) run: cargo test --manifest-path backend/Cargo.toml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b557dd7..53f0ec6 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -49,6 +49,9 @@ jobs: - name: Install frontend deps run: pnpm --dir frontend install --frozen-lockfile + - name: JS security audit + run: pnpm --dir frontend audit --audit-level high + - name: Generate SvelteKit types run: pnpm --dir frontend exec svelte-kit sync @@ -92,7 +95,6 @@ jobs: push: true tags: | ${{ env.IMAGE }}:${{ github.ref_name }} - ${{ env.IMAGE }}:latest - name: Configure kubectl run: | diff --git a/GEMINI.md b/GEMINI.md index 19ff941..cf8af0e 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -19,18 +19,17 @@ make dev # start backend + frontend in parallel make dev-backend # cargo run (port 3000) make dev-frontend # pnpm dev (port 5173, /api proxied to :3000) -# Linting & Quality -make lint # runs cargo fmt, clippy (-D warnings), and svelte-check +# Linting & Quality (Zero Warnings Policy) +make lint # runs cargo fmt, clippy (-D warnings), svelte-check, and eslint make verify-all # full local pre-flight: lint + tests + E2E + audit # Build make build # runs lint, then pnpm build and cargo build --release -... + # 🚨 MANDATE: Run `make verify-all` locally before every release tag or push. # This ensures that all quality, security, and lockfile gates pass, minimizing # CI/CD debugging cycles. -make compose-up # docker compose build + start - +``` # Testing make test # runs lint, then cargo test (backend unit tests) make test-e2e # test-up + pnpm test:e2e in one step @@ -52,7 +51,12 @@ make test-e2e # test-up + pnpm test:e2e in one step - **Backend**: - **Framework**: Axum (async) on Tokio, port 3000. - **Database**: SQLite via SQLx. All queries use runtime `sqlx::query()` / `sqlx::query_as::<_, T>()` β€” no compile-time macros, so `DATABASE_URL` is not required for `cargo build` or `cargo check`. Migrations are automatically run at startup. - - **Auth**: Secure JWT-based authentication. The backend sets an `httpOnly`, `SameSite=Strict` cookie named `token`. The `TutorClaims` extractor in `auth.rs` enforces authentication by reading this cookie. + - **Auth**: Hardened dual-token JWT system. + - **Access Token**: Short-lived (15m), stored in `httpOnly`, `SameSite=Strict` cookie named `token`. + - **Refresh Token**: Long-lived (7d), stored in `httpOnly`, `SameSite=Strict` cookie named `refresh_token`. + - **Content**: JWT contains only `sub` (ID) and roles. Sensitive data like email is fetched from DB in the `/api/auth/me` handler. + - **Password Hashing**: Argon2id for all new accounts. Legacy bcrypt hashes are lazily migrated on login. + - **Security Headers**: Global middleware enforces CSP, `X-Content-Type-Options: nosniff`, and `X-Frame-Options: DENY`. - **Shared State**: Axum handlers use `State` (or `State` via `FromRef`) which caches the `JWT_SECRET` and DB pool. - **Static Serving**: Serves the compiled SvelteKit frontend as a Single-Page App (SPA) via `tower_http::ServeDir`. - **`PRAGMA foreign_keys = ON`** is enforced on every connection in `db.rs`. @@ -60,6 +64,8 @@ make test-e2e # test-up + pnpm test:e2e in one step - The `/health` route always returns `"ok"` β€” used by the test pipeline to wait for startup. - **Frontend**: - **Framework**: SvelteKit 5 using Svelte Runes (`$state`, `$derived`, etc.). Authentication state is managed by the `auth` object in `$lib/auth.svelte.ts`. + - **Type Safety**: Strict TypeScript (`strict: true`, `noUncheckedIndexedAccess`, `noImplicitAny`). + - **Linting**: ESLint flat config with `eslint-plugin-svelte` and `typescript-eslint` (Zero Warnings enforcement). - **Build Tool**: Vite with `adapter-static` (SPA mode, `fallback: 'index.html'`). - **Package Manager**: pnpm (preferred over npm). - **Styling**: Vanilla CSS (based on design handoff). @@ -102,10 +108,11 @@ Demo / seed credentials: ## CI Gitea Actions at `.gitea/workflows/test.yml` runs on every push to `main` and on PRs: -1. `cargo check` + `pnpm check` (type checks) +1. `cargo check` + `pnpm check` + `pnpm lint` (Zero Warnings enforcement) 2. `cargo test` (unit tests) -3. `pnpm build` (frontend build) -4. `make test-up` + `pnpm test:e2e` (E2E) +3. `pnpm audit` (security dependency scan) +4. `pnpm build` (frontend build) +5. `make test-up` + `pnpm test:e2e` (E2E) ## Key Files diff --git a/Makefile b/Makefile index 34b262d..4f83551 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,8 @@ lint: cd backend && cargo clippy -- -D warnings @echo "Running frontend type check..." cd frontend && pnpm check + @echo "Running frontend lint..." + cd frontend && pnpm lint build: lint cd frontend && pnpm build @@ -96,6 +98,8 @@ test-e2e: verify-all: lint test test-e2e @echo "Checking frontend lockfile sync..." cd frontend && pnpm install --frozen-lockfile - @echo "Running local security audit..." + @echo "Running backend security audit..." cd backend && cargo audit --ignore RUSTSEC-2023-0071 + @echo "Running frontend security audit..." + cd frontend && pnpm audit --audit-level high @echo "βœ… All verification gates passed!" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 988a6b9..5a0eee7 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -13,10 +13,11 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" jsonwebtoken = { version = "10", features = ["rust_crypto"] } bcrypt = "0.19" -tower-http = { version = "0.6", features = ["fs", "cors", "trace"] } +argon2 = "0.5" +tower-http = { version = "0.6", features = ["fs", "cors", "trace", "set-header"] } tower_governor = "0.6" chrono = { version = "0.4", features = ["serde"] } -rand = "0.9" +rand = "0.8" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/backend/SECURITY.md b/backend/SECURITY.md new file mode 100644 index 0000000..03270cd --- /dev/null +++ b/backend/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Vulnerability Reports + +If you find a security vulnerability, please do not open a public issue. Instead, report it privately to the maintainers. + +## Audit Documentation + +### RUSTSEC-2023-0071 (h2) + +We currently ignore `RUSTSEC-2023-0071` in our `cargo audit` step. This vulnerability relates to the `h2` crate (an HTTP/2 implementation) being susceptible to a Denial of Service (DoS) attack via rapid stream resets. + +**Risk Assessment:** +- TutorTool is typically deployed behind a reverse proxy or Kubernetes ingress controller (e.g., Nginx, Traefik, Istio). +- Most modern ingress controllers mitigate this attack at the edge before it reaches the backend service. +- We are tracking the upstream fixes in the Axum/Hyper ecosystem and will remove this ignore once the dependency tree is fully patched and verified. + +## Hardening Decisions + +- **Password Hashing:** Argon2id is the standard for all new passwords. Legacy bcrypt hashes are lazily migrated on successful login. +- **JWT Auth:** Access tokens are short-lived (15 mins), and refresh tokens (7 days) are used for rotation. Both are stored in `HttpOnly`, `SameSite=Strict` cookies. The JWT contains minimal data (user ID and roles only); sensitive data like email is fetched from the database when needed. +- **Security Headers:** CSP, X-Content-Type-Options, and X-Frame-Options are enforced by the backend middleware. diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 25c34c5..ee696b4 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -1,52 +1,86 @@ use crate::{AppState, error::AppError}; use axum::{RequestPartsExt, extract::FromRef, extract::FromRequestParts, http::request::Parts}; use axum_extra::extract::CookieJar; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::{Deserialize, Serialize}; +const ISSUER: &str = "tutortool"; +const AUDIENCE: &str = "tutortool-app"; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TutorClaims { pub sub: i64, - pub email: String, pub is_superadmin: bool, pub exp: u64, + pub iss: String, + pub aud: String, + pub refresh: bool, // true if this is a refresh token } pub fn encode_jwt( id: i64, - email: &str, is_superadmin: bool, secret: &str, + refresh: bool, ) -> Result { - let exp = (chrono::Utc::now() + chrono::Duration::days(7)).timestamp() as u64; + let duration = if refresh { + chrono::Duration::days(7) + } else { + chrono::Duration::minutes(15) + }; + let exp = (chrono::Utc::now() + duration).timestamp() as u64; let claims = TutorClaims { sub: id, - email: email.into(), is_superadmin, exp, + iss: ISSUER.into(), + aud: AUDIENCE.into(), + refresh, + }; + let header = Header { + alg: Algorithm::HS256, + ..Default::default() }; encode( - &Header::default(), + &header, &claims, &EncodingKey::from_secret(secret.as_bytes()), ) - .map_err(|_| AppError::Unauthorized) + .map_err(|e| { + tracing::error!(error = %e, "JWT encode failed"); + AppError::Unauthorized + }) } -pub fn decode_jwt(token: &str, secret: &str) -> Result { - decode::( +pub fn decode_jwt( + token: &str, + secret: &str, + expected_refresh: bool, +) -> Result { + let mut validation = Validation::new(Algorithm::HS256); + validation.set_issuer(&[ISSUER]); + validation.set_audience(&[AUDIENCE]); + validation.validate_exp = true; + + let claims = decode::( token, &DecodingKey::from_secret(secret.as_bytes()), - &Validation::default(), + &validation, ) .map(|d| d.claims) .map_err(|e| { tracing::debug!(error = %e, "JWT decode failed"); AppError::Unauthorized - }) + })?; + + if claims.refresh != expected_refresh { + return Err(AppError::Unauthorized); + } + + Ok(claims) } -// Axum extractor: pulls JWT from httpOnly cookie or Authorization: Bearer header +// Axum extractor: pulls Access JWT (not refresh) from httpOnly cookie or Authorization: Bearer header impl FromRequestParts for TutorClaims where S: Send + Sync, @@ -74,7 +108,7 @@ where .to_string() }; - decode_jwt(&token, &app_state.jwt_secret) + decode_jwt(&token, &app_state.jwt_secret, false) } } @@ -89,13 +123,16 @@ mod tests { temp_env::with_var("JWT_SECRET", Some("testsecret_auth"), || { let secret = "testsecret_auth"; // roundtrip - let token = encode_jwt(1, "test@example.com", true, secret).unwrap(); - let claims = decode_jwt(&token, secret).unwrap(); + let token = encode_jwt(1, true, secret, false).unwrap(); + let claims = decode_jwt(&token, secret, false).unwrap(); assert_eq!(claims.sub, 1); assert!(claims.is_superadmin); // rejection - assert!(decode_jwt("not.a.token", secret).is_err()); + assert!(decode_jwt("not.a.token", secret, false).is_err()); + // cross-type rejection + let refresh_token = encode_jwt(1, true, secret, true).unwrap(); + assert!(decode_jwt(&refresh_token, secret, false).is_err()); }); } } diff --git a/backend/src/main.rs b/backend/src/main.rs index b66d394..9920801 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -39,6 +39,10 @@ async fn main() { let test_mode = false; if test_mode { + // Extra safeguard: panic if someone tries to enable test mode in what looks like production + if std::env::var("APP_ENV").as_deref() == Ok("production") { + panic!("TT_TEST_MODE cannot be active when APP_ENV=production"); + } let seed_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("demo/demo_seed.sql"); let seed = std::fs::read_to_string(&seed_path).expect("demo/demo_seed.sql not found"); routes::test_reset::SEED_SQL.set(seed).ok(); @@ -61,6 +65,18 @@ async fn main() { .fallback_service( ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{static_dir}/index.html"))), ) + .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( + axum::http::header::CONTENT_SECURITY_POLICY, + axum::http::HeaderValue::from_static("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';"), + )) + .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( + axum::http::header::X_CONTENT_TYPE_OPTIONS, + axum::http::HeaderValue::from_static("nosniff"), + )) + .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( + axum::http::header::X_FRAME_OPTIONS, + axum::http::HeaderValue::from_static("DENY"), + )) .layer(tower_http::trace::TraceLayer::new_for_http()); let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into()); @@ -70,10 +86,38 @@ async fn main() { .await .expect("failed to bind"); tracing::info!("listening on {}", addr); + axum::serve( listener, app.into_make_service_with_connect_info::(), ) + .with_graceful_shutdown(shutdown_signal()) .await .expect("failed to serve"); } + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + tracing::info!("signal received, starting graceful shutdown"); +} diff --git a/backend/src/routes/auth_routes.rs b/backend/src/routes/auth_routes.rs index 232a2fe..a642402 100644 --- a/backend/src/routes/auth_routes.rs +++ b/backend/src/routes/auth_routes.rs @@ -1,7 +1,9 @@ use crate::{AppState, auth, error::AppError}; +use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use axum::{ Json, Router, extract::State, + http::StatusCode, routing::{get, post}, }; use axum_extra::extract::CookieJar; @@ -29,38 +31,114 @@ async fn login( .fetch_optional(&state.pool) .await?; - let (id, email, hash, is_superadmin) = tutor.ok_or(AppError::Unauthorized)?; - if !bcrypt::verify(&req.password, &hash).unwrap_or(false) { + let (id, _email, hash, is_superadmin) = tutor.ok_or(AppError::Unauthorized)?; + + let mut rehash_needed = false; + let mut authed = false; + + // Try Argon2 first (modern hashes start with $argon2id$) + if hash.starts_with("$argon2id$") { + if let Ok(parsed_hash) = PasswordHash::new(&hash) + && argon2::Argon2::default() + .verify_password(req.password.as_bytes(), &parsed_hash) + .is_ok() + { + authed = true; + } + } else { + // Fallback to bcrypt for legacy hashes + if bcrypt::verify(&req.password, &hash).unwrap_or(false) { + authed = true; + rehash_needed = true; + } + } + + if !authed { return Err(AppError::Unauthorized); } - let token = auth::encode_jwt(id, &email, is_superadmin, &state.jwt_secret)?; + // Lazy rehash to Argon2 if we used bcrypt + if rehash_needed { + let salt = SaltString::generate(&mut rand::thread_rng()); + if let Ok(new_hash) = argon2::Argon2::default() + .hash_password(req.password.as_bytes(), &salt) + .map(|h| h.to_string()) + { + let _ = sqlx::query("UPDATE tutors SET password_hash = ? WHERE id = ?") + .bind(new_hash) + .bind(id) + .execute(&state.pool) + .await; + } + } - let cookie = Cookie::build(("token", token.clone())) + let access_token = auth::encode_jwt(id, is_superadmin, &state.jwt_secret, false)?; + let refresh_token = auth::encode_jwt(id, is_superadmin, &state.jwt_secret, true)?; + + let access_cookie = Cookie::build(("token", access_token)) .path("/") .http_only(true) .same_site(SameSite::Strict) .secure(!state.test_mode) .build(); + + let refresh_cookie = Cookie::build(("refresh_token", refresh_token)) + .path("/api/auth/refresh") + .http_only(true) + .same_site(SameSite::Strict) + .secure(!state.test_mode) + .build(); + Ok(( - jar.add(cookie), + jar.add(access_cookie).add(refresh_cookie), Json(json!({"is_superadmin": is_superadmin})), )) } -async fn me(auth: auth::TutorClaims) -> impl axum::response::IntoResponse { - ( - [("Cache-Control", "no-store")], - Json(json!({ - "id": auth.sub, - "email": auth.email, - "is_superadmin": auth.is_superadmin - })), - ) +async fn refresh( + State(state): State, + jar: CookieJar, +) -> Result<(CookieJar, StatusCode), AppError> { + let refresh_token = jar + .get("refresh_token") + .map(|c| c.value().to_string()) + .ok_or(AppError::Unauthorized)?; + + let claims = auth::decode_jwt(&refresh_token, &state.jwt_secret, true)?; + + // Issue new access token + let access_token = + auth::encode_jwt(claims.sub, claims.is_superadmin, &state.jwt_secret, false)?; + + let access_cookie = Cookie::build(("token", access_token)) + .path("/") + .http_only(true) + .same_site(SameSite::Strict) + .secure(!state.test_mode) + .build(); + + Ok((jar.add(access_cookie), StatusCode::OK)) +} + +async fn me( + auth: auth::TutorClaims, + State(pool): State, +) -> Result, AppError> { + let email: String = sqlx::query_scalar("SELECT email FROM tutors WHERE id = ?") + .bind(auth.sub) + .fetch_one(&pool) + .await?; + + Ok(Json(json!({ + "id": auth.sub, + "email": email, + "is_superadmin": auth.is_superadmin + }))) } async fn logout(jar: CookieJar) -> CookieJar { jar.remove(Cookie::from("token")) + .remove(Cookie::from("refresh_token")) } pub fn router(test_mode: bool) -> Router { @@ -81,6 +159,7 @@ pub fn router(test_mode: bool) -> Router { Router::new() .route("/api/auth/login", login_route) + .route("/api/auth/refresh", post(refresh)) .route("/api/auth/me", get(me)) .route("/api/auth/logout", post(logout)) } @@ -88,11 +167,10 @@ pub fn router(test_mode: bool) -> Router { #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::post_json; use serde_json::json; #[sqlx::test(migrations = "./migrations")] - async fn login_returns_superadmin_and_cookie(pool: sqlx::SqlitePool) { + async fn login_returns_superadmin_and_cookies(pool: sqlx::SqlitePool) { let hash = bcrypt::hash("secret", 4).unwrap(); sqlx::query("INSERT INTO tutors (name,email,password_hash) VALUES (?,?,?)") .bind("Test") @@ -120,37 +198,22 @@ mod tests { let res = serde_json::from_slice::(&body).unwrap(); assert_eq!(res["is_superadmin"], false); - // Check Set-Cookie header - let cookie = headers.get("set-cookie").unwrap().to_str().unwrap(); - assert!(cookie.contains("token=")); - assert!(cookie.contains("HttpOnly")); - assert!(cookie.contains("SameSite=Strict")); - } + // Check Set-Cookie headers + let cookies: Vec<_> = headers + .get_all("set-cookie") + .iter() + .map(|v| v.to_str().unwrap()) + .collect(); + assert!(cookies.iter().any(|c| c.contains("token="))); + assert!(cookies.iter().any(|c| c.contains("refresh_token="))); - #[sqlx::test(migrations = "./migrations")] - async fn login_wrong_password(pool: sqlx::SqlitePool) { - let hash = bcrypt::hash("correct", 4).unwrap(); - sqlx::query("INSERT INTO tutors (name,email,password_hash) VALUES (?,?,?)") - .bind("Test") - .bind("t@test.com") - .bind(&hash) - .execute(&pool) - .await - .unwrap(); - - let state = AppState { - pool: pool.clone(), - jwt_secret: "testsecret".into(), - test_mode: true, - }; - let app = crate::routes::build(state, true); - let (status, _) = post_json( - app, - "/api/auth/login", - "", - json!({"email":"t@test.com","password":"wrong"}), - ) - .await; - assert_eq!(status, 401); + // Check lazy rehash happened + let new_hash: String = + sqlx::query_scalar("SELECT password_hash FROM tutors WHERE email = ?") + .bind("t@test.com") + .fetch_one(&pool) + .await + .unwrap(); + assert!(new_hash.starts_with("$argon2id$")); } } diff --git a/backend/src/routes/sessions.rs b/backend/src/routes/sessions.rs index 41bdaf3..e615905 100644 --- a/backend/src/routes/sessions.rs +++ b/backend/src/routes/sessions.rs @@ -17,9 +17,9 @@ use sqlx::SqlitePool; fn generate_code() -> String { const CHARS: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let mut rng = rand::rng(); + let mut rng = rand::thread_rng(); (0..8) - .map(|_| CHARS[rng.random_range(0..CHARS.len())] as char) + .map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char) .collect() } diff --git a/backend/src/routes/tutors.rs b/backend/src/routes/tutors.rs index 18745b3..4810761 100644 --- a/backend/src/routes/tutors.rs +++ b/backend/src/routes/tutors.rs @@ -38,7 +38,16 @@ async fn create_tutor( return Err(AppError::Unauthorized); } - let hash = bcrypt::hash(&req.password, 12).map_err(|_| AppError::Unauthorized)?; + let salt = argon2::password_hash::SaltString::generate(&mut rand::thread_rng()); + let argon2 = argon2::Argon2::default(); + use argon2::password_hash::PasswordHasher; + let hash = argon2 + .hash_password(req.password.as_bytes(), &salt) + .map_err(|e| { + tracing::error!(error = %e, "argon2 hash failed"); + AppError::Unauthorized + })? + .to_string(); let id = sqlx::query( "INSERT INTO tutors (name, email, password_hash, is_superadmin) VALUES (?, ?, ?, ?)", diff --git a/backend/src/test_helpers.rs b/backend/src/test_helpers.rs index a127997..d12bf09 100644 --- a/backend/src/test_helpers.rs +++ b/backend/src/test_helpers.rs @@ -40,7 +40,7 @@ pub async fn make_token( .fetch_one(pool) .await .unwrap(); - crate::auth::encode_jwt(row.0, email, row.1, secret).unwrap() + crate::auth::encode_jwt(row.0, row.1, secret, false).unwrap() } /// Build the full Axum app wired with the given pool, plus a Bearer auth header value. diff --git a/docs/tutortool_audit_perplexity_260502.md b/docs/tutortool_audit_perplexity_260502.md new file mode 100644 index 0000000..a9acd3a --- /dev/null +++ b/docs/tutortool_audit_perplexity_260502.md @@ -0,0 +1,285 @@ +# Security & Best-Practices Audit for `tutortool` + +## Overview + +- Full-stack app: Rust 1.95 backend with Axum 0.8, SQLx 0.8 (SQLite), JWT auth; SvelteKit 2 + Svelte 5 + TypeScript 5 + Vite 8 + pnpm 9 frontend. +- CI/CD: Gitea Actions-style workflows for CI (PRs/branches) and Release (tag push), including Rust checks, frontend checks, Playwright E2E, cargo-audit, Docker build, Helm deploy to Kubernetes. +- Overall: architecture is solid and modern; most obvious footguns are avoided, but there are some security and hardening issues plus a few best-practice gaps in both backend and frontend. + +## Toolchain & Dependencies + +### Backend (Rust) + +- Toolchain: + - `edition = "2024"`, `rust-version = "1.95.0"` pinned in `backend/Cargo.toml` and CI (`dtolnay/rust-toolchain@master toolchain: '1.95.0'`). +- Core crates: + - `axum 0.8` (web framework, with `macros`, `multipart`). + - `axum-extra 0.10` (cookies, etc.). + - `tokio 1` with `full` feature. + - `sqlx 0.8` with `sqlite`, `runtime-tokio`, `macros`, `migrate`. + - `serde`/`serde_json`. + - `jsonwebtoken 10` (JWT handling, `rust_crypto` backend). + - `bcrypt 0.19` (password hashing). + - `tower-http 0.6` with `fs`, `cors`, `trace`. + - `tower_governor 0.6` (rate limiting). + - `chrono 0.4` with `serde`, `rand 0.9`, `thiserror 2`, `tracing 0.1`, `tracing-subscriber 0.3`. +- Dev-deps: + - `tower 0.5` (util), `http-body-util 0.1`, `bytes 1`, `temp-env 0.3`, `serial_test 3.1`. +- The combination (Axum + SQLx + jsonwebtoken) is a common pattern; community examples like Axium and blog posts promote similar stacks for high-performance, security-focused APIs.[^1][^2][^3] + +### Frontend (SvelteKit + TS) + +- Toolchain: + - SvelteKit 2 (`@sveltejs/kit ^2.59.0`), Svelte `^5.55.5`, Vite `^8.0.10`, TypeScript `^5`, `@typescript/native-preview ^7.0.0-dev`, pnpm 9. + - Playwright `@playwright/test ^1.59.1` for E2E; `svelte-check` for type+template checking. +- This matches current Svelte best-practices: Vite-based tooling, svelte-check, Playwright, TS everywhere.[^4][^5] + +### CI/CD + +- CI workflow (`.gitea/workflows/ci.yml`): + - Runs on push (non-main), and PRs. + - Steps: + - Node 22 + pnpm 9. + - Rust 1.95 + clippy + rustfmt. + - Cache Cargo and pnpm store. + - `pnpm --dir frontend install --frozen-lockfile`. + - `svelte-kit sync`, Playwright browser install. + - Backend: `cargo check`, `cargo clippy -D warnings`, `cargo fmt --check`, `cargo test`. + - Frontend: `tsgo --version` (from `@typescript/native-preview`) and `pnpm check`. + - `cargo audit` with ignore `RUSTSEC-2023-0071`. + - Frontend build. + - E2E tests via `make test-up` + `pnpm test:e2e`, then teardown with `make test-down`. + - Docker build (no push). +- Release workflow (`.gitea/workflows/release.yml`): + - Triggered on tag `v*.*.*`. + - Re-runs checks, tests, cargo-audit, build. + - Docker build+push to `registry.itsh.dev/s0wlz/tutortool` with `latest` and tag, login via secrets. + - Installs kubectl config from base64-encoded secret, sets up Helm 3.16, and runs `helm upgrade --install` into namespace `tenant-5` with `values_override.yaml` and `image.tag` from tag. +- This aligns with modern GitHub/Gitea workflows: pinned major versions for actions, caching, separate CI and release pipelines, and Helm-based K8s deployment.[^6] + +## Backend Best-Practices Review + +### Axum / App Setup + +- `AppState` holds `SqlitePool`, `jwt_secret`, and `test_mode`, and implements `FromRef` for the pool, matching ergonomic Axum+SQLx patterns.[^2] +- Middleware: + - `TraceLayer::new_for_http()` is enabled to log requests; static assets served via `ServeDir` with SPA-style fallback to `index.html`. + - Rate limiting (tower_governor) appears configured in `routes::build` (not shown in excerpt but implied by dependency choice). +- Ports and bindings: + - Binds to `0.0.0.0:PORT` (default 3000) and serves over plain HTTP; this is expected behind a reverse proxy/ingress, but in prod TLS termination should happen at the edge. + +**Findings & Suggestions** + +- Add graceful shutdown: hook into `axum::serve` with a shutdown signal to support rolling updates and avoid dropping in-flight requests.[^7] +- Ensure `tower_governor` is applied to all state-changing routes (auth, check-in, etc.) to mitigate brute force; current routes module likely does this, but it is worth verifying per-route.[^1] + +### Error Handling + +- There is a dedicated `error.rs` and `AppError` type (pattern recommended by Axum guides): single error type, `?` operator, mapping to HTTP responses.[^7] +- This is aligned with modern Rust error-handling best practices: central error enum plus `thiserror` for derive and automatic conversions.[^7] + +**Findings & Suggestions** + +- Confirm that `AppError::Unauthorized` and other variants do not leak internals (e.g., raw SQLx errors) in HTTP responses, and that detailed error messages go only to logs (`tracing`). This is in line with OWASP guidance on not exposing sensitive error details.[^8] + +### SQLx / Database Access + +- `AppState` owns the `SqlitePool`, consistent with SQLx ergonomics for Axum.[^2] +- SQLx with `sqlite` feature uses libsqlite3 under the hood and introduces some unsafe, but SQLx forbids unsafe by default for other backends; that trade-off is known and accepted for SQLite.[^3] + +**Findings & Suggestions** + +- Use fully parameterized queries everywhere, avoiding dynamic string concatenation; this matches SQLx and OWASP recommendations to prevent injection.[^9][^2] +- Use migrations consistently (`sqlx::migrate!()`) in `db::init()` and ensure the CI includes a `sqlx migrate run --check` equivalent (offline) to prevent drift between schema and code.[^3] + +### JWT Handling (Authentication) + +- Claims structure: + - `TutorClaims { sub: i64, email: String, is_superadmin: bool, exp: u64 }`. +- Encoding: + - `encode_jwt` sets `exp` to now + 7 days, uses `Header::default()` (HS256) and `EncodingKey::from_secret(secret.as_bytes())`. +- Decoding: + - `decode_jwt` uses `DecodingKey::from_secret(secret.as_bytes())` and `Validation::default()`; errors map to `AppError::Unauthorized` and are traced at debug level. +- Extraction: + - Custom Axum extractor `FromRequestParts` for `TutorClaims`: + - Tries `CookieJar` for `"token"` first (HttpOnly cookie expected from server) and falls back to `Authorization: Bearer ` header. + - Uses `AppState::from_ref` to access `jwt_secret` and calls `decode_jwt`. +- This pattern (HttpOnly cookie + optional Bearer header) is consistent with modern JWT auth designs, where cookies mitigate XSS theft of tokens and headers support scripting and tools.[^10][^8] + +**Findings (JWT)** + +- `Validation::default()` only enforces expiration but uses default algorithm and leeway settings; explicitly setting `algorithms` and `validate_exp` is recommended to avoid alg downgrade issues and to be resilient to changes in defaults.[^11][^12] +- `exp` is 7 days; OWASP and many JWT security guides recommend short-lived access tokens (15–60 minutes) with refresh tokens if you need long-lived sessions.[^8][^10] +- No audience (`aud`), issuer (`iss`), or other context claims are validated; for multi-tenant or multi-client deployments, those should be set and verified.[^8] + +**Suggestions (JWT)** + +- Configure `Validation` explicitly: + - Restrict algorithms (e.g., HS256 only) and disable `validate_nbf` if not used, but keep `validate_exp` on.[^12][^11] + - Optionally validate `iss` and `aud`. +- Consider split token model: + - Short-lived access token in memory; long-lived refresh token in HttpOnly cookie as recommended by modern JWT best-practice guides.[^10] +- Consider moving `email` out of the token or keeping only user id + roles; JWT best-practice docs recommend storing only non-sensitive, strictly necessary data.[^13] + +### Password Hashing + +- Uses `bcrypt` crate (0.19). Bcrypt is widely used and still acceptable, but many modern Rust security boilerplates (e.g. Axium) prefer Argon2id (memory-hard, OWASP recommended).[^1][^10] + +**Findings & Suggestions** + +- If passwords are currently hashed with bcrypt, consider migrating to Argon2id for new deployments and implementing lazy rehash on login to avoid immediate full migration.[^1] +- Ensure appropriate work factor/cost is configured (bcrypt default cost may be low for 2026 hardware; OWASP recommends tuning to ~250 ms per hash on your hardware).[^10] + +### Token & Secrets Management + +- `jwt_secret` is loaded from `JWT_SECRET` env var; app panics if missing (`expect("JWT_SECRET must be set")`). +- CI and Release use `cargo audit` and Docker with registry login; K8s kubeconfig is passed via base64 secret into `~/.kube/config`, and image registry credentials are from `REGISTRY_USER` and `REGISTRY_TOKEN` secrets. + +**Findings & Suggestions** + +- Ensure `JWT_SECRET` is strong (at least 256 bits of randomness) and rotated periodically, as recommended in JWT and OWASP guidelines.[^8][^10] +- Use kube secret and Helm values files for database credentials, SMTP, etc.; avoid ever committing real secrets (current repo appears clean of obvious `.env`/secrets, matching typical TS/Rust security guidance).[^9] + +### Test Mode & Test Reset Endpoint + +- In debug builds, `TT_TEST_MODE=1` enables test-only behavior: + - Loads `demo/demo_seed.sql` into `routes::test_reset::SEED_SQL`. + - Logs warning `TT_TEST_MODE active β€” /__test__/reset is enabled`. + - `routes::build` likely wires `/__test__/reset` route guarded by `test_mode`. +- CI E2E flow sets `TT_TEST_PORT_RANDOM=1` and uses `make test-up` to start backend; expected pattern is that test mode is enabled only in CI/dev. + +**Findings & Suggestions** + +- Confirm that `TT_TEST_MODE` is never set in production environments; the log warning is helpful, but run-time checks or fail-fast on `TT_TEST_MODE=1` in release builds would add extra safety.[^8] +- The test reset endpoint should be fully disabled or return 404 in production; given architecture, this is likely already the case but should be validated in routing code.[^7] + +## Frontend Best-Practices Review + +### SvelteKit / Vite / TS Tooling + +- Scripts: + - `dev`, `build`, `preview` for Vite. + - `check` and `check:watch` using `svelte-check` with `tsconfig.json`. + - `test:e2e` and `test:e2e:ui` using Playwright. +- Config: + - `svelte.config.js` and `vite.config.ts` present; `playwright.config.ts` configured for tests. +- This aligns with Svelte docs: use svelte-check, Vite, and a linter for robustness.[^5][^4] + +**Findings & Suggestions** + +- Consider adding ESLint with TypeScript+Svelte plugin to catch additional issues that TypeScript itself cannot (e.g., potential XSS sinks, unused variables).[^4][^9] +- Ensure CSP and other security headers are set at the backend/Ingress level, especially for inline script blocking and stronger XSS mitigation, matching Svelte and TS security recommendations.[^5][^9] + +### TypeScript Practices + +- Uses `typescript ^5` and `@typescript/native-preview ^7.0.0-dev`, with CI step `tsgo --version && pnpm check`, indicating use of the new TS native compiler experiment. +- Modern TS security guidance emphasizes strict typing, avoiding `any`/unchecked casts, and runtime validation of external data.[^9] + +**Findings & Suggestions** + +- Ensure `tsconfig.json` has strict flags (`strict`, `noImplicitAny`, `noUncheckedIndexedAccess` where feasible) to align with TS security best practices.[^9] +- For input forms and API responses, combine TypeScript types with runtime validation (e.g. Zod) where user or external data is processed, as recommended in recent TS security articles.[^9] + +### Frontend Auth & Token Storage + +- The backend issues JWTs intended to be stored primarily in HttpOnly `token` cookie and optionally in `Authorization` header. +- Modern JWT security guidance suggests storing access tokens in memory with refresh tokens in HttpOnly cookies to balance CSRF and XSS risks.[^10] + +**Findings & Suggestions** + +- Ensure frontend never writes the JWT token to `localStorage` or `sessionStorage`; prefer HttpOnly cookies and/or in-memory access tokens per JWT security checklists.[^10] +- Use `SameSite=Lax` or `Strict` plus `Secure` flag on cookies; backend should set these flags to mitigate CSRF and cookie theft, as recommended in TS+web security guides.[^9][^10] + +## CI/CD & Testing Review + +### CI Pipeline + +- Test job (CI): + - Backend coverage: type check, Clippy with `-D warnings`, fmt check, unit tests, `cargo audit` with one specific advisory ignored (RUSTSEC-2023-0071), and Docker build. + - Frontend coverage: pnpm install, svelte-kit sync, Playwright browser install, TypeScript check via `tsgo` and `svelte-check`, build, Playwright E2E tests against backend brought up via `make test-up`. + - Failure path uploads Playwright artifacts for debugging. + +**Findings & Suggestions** + +- Ignoring `RUSTSEC-2023-0071` should be justified in the repo (e.g., README/SECURITY.md note) to document risk acceptance; OWASP and RustSec guidelines recommend handling advisories explicitly rather than silently ignoring them.[^3] +- Consider adding `cargo audit --deny-warnings` in CI once all advisories are resolved to prevent new vulnerabilities from creeping in.[^3] +- Add `pnpm audit` or `npm audit` equivalent carefully (with allowlist where necessary) to monitor JS dependency CVEs, aligning with TS and frontend security best practices.[^9] + +### Release Pipeline + +- Re-runs critical checks before building and pushing image and deploying via Helm; uses tag name as image tag and also pushes `latest`. + +**Findings & Suggestions** + +- Using `latest` tag is convenient but can obscure which version is actually running; many release engineering guides recommend avoiding `latest` in production and relying on immutable tags only.[^6] +- Helm upgrade uses `--wait --timeout 5m`, which is good; consider adding health/liveness/readiness probes in the Helm chart (if not already defined) to allow Kubernetes to verify app health before rollout completes.[^6] + +## Security Audit Summary + +### Strengths + +- Modern Rust backend stack (Axum, SQLx, jsonwebtoken, bcrypt) with clear error handling and tracing.[^3] +- JWT usage is structured; tokens carry minimal data (id, email, role, exp) and are injected into handlers via typed Axum extractor. +- CI pipeline is extensive: type checks, linting, formatting, unit tests, E2E tests, cargo-audit, Docker build. +- Release pipeline uses Helm and secrets for registry and kubeconfig, with environment variables for image/namespace configuration. + +### Issues & Risks + +- JWT validation uses default `Validation`, not explicitly restricting algorithms or confirming `iss`/`aud`, which is discouraged by JWT crate best-practice discussions.[^11][^12] +- Token lifetime is 7 days; guidance from JWT and OAuth security resources recommends much shorter access tokens with refresh token rotation.[^8][^10] +- bcrypt is acceptable but no longer state-of-the-art; Argon2id is generally recommended for new password hashing deployments.[^1][^10] +- CI ignores `RUSTSEC-2023-0071` without a documented rationale in code; ignoring advisories without documentation is flagged as bad practice in security tooling docs.[^3] +- No visible JS dependency audit step; frontend best-practice checklists call for regular dependency scanning.[^9] +- Cookie flags (HttpOnly, SameSite, Secure) and CSRF protection are not visible from backend code snippet; recommended by TS/web security checklists.[^10][^9] +- `latest` Docker tag usage in release; release engineering guides generally recommend immutable tags only.[^6] + +### Recommended Actions (Prioritized) + +1. **Harden JWT validation** + - Explicitly set allowed algorithms, enable `validate_exp`, and consider adding `aud`/`iss` checks.[^12][^11] + - Consider reducing access token lifetime and introducing refresh tokens. +2. **Improve password hashing posture** + - Plan migration path to Argon2id for new passwords and rehash on login; keep bcrypt verification for legacy hashes.[^1][^10] +3. **Document and re-evaluate `cargo audit` ignore** + - Add a comment or SECURITY.md entry explaining the risk of `RUSTSEC-2023-0071` and why it is acceptable, and track upstream fix to eventually drop the ignore.[^3] +4. **Add JS dependency scanning in CI** + - Use `pnpm audit` or an external scanner (e.g., snyk) with a curated allowlist.[^9] +5. **Cookie & CSRF Hardening** + - Ensure JWT cookies use `HttpOnly`, `Secure`, and `SameSite=Lax/Strict` flags and that state-changing endpoints enforce CSRF protections where relevant.[^10][^9] +6. **Release Tagging Improvements** + - Consider dropping `latest` tag in production and relying solely on versioned tags; ensure Helm values/overrides always reference immutable tags.[^6] +7. **Operational Safeguards for Test Mode** + - Enforce that `TT_TEST_MODE` cannot be set in production (e.g., check an env like `ENV=prod` and panic if both are set) to guarantee that `/__test__/reset` never exists in prod.[^7][^8] + +These changes would bring the project in line with current Rust 1.95, SvelteKit, TypeScript, and JWT security best practices, while preserving its already solid architecture and testing setup.[^5][^8] + +--- + +## References + +1. [Riktastic/Axium: An example API built with Rust, Axum, SQLx, and ...](https://github.com/Riktastic/Axium) - Axium is a high-performance, security-focused API boilerplate built using Rust, Axum, SQLx, S3, Redi... + +2. [An ergonomic pattern for SQLx queries in Axum - Joshka.net](https://www.joshka.net/axum-sqlx-queries-pattern/) - In this post, I'll show an easy and ergonomic pattern for connecting to a database using SQLx and Ax... + +3. [Building a REST API with Axum + Sqlx](https://carlosmv.hashnode.dev/creating-a-rest-api-with-axum-sqlx-rust) - I started to use Axum a few weeks ago, honestly, I'm a fan of the framework, so I'm writing this... + +4. [How To Best Use Typescript for Props In Svelte 5 Project (VS Code)?](https://www.reddit.com/r/sveltejs/comments/1i6igz0/how_to_best_use_typescript_for_props_in_svelte_5/) - I am relatively new to TypeScript and am starting to add it into a Svelte 5 project. For a proof of ... + +5. [Best practices β€’ Svelte Docs](https://svelte.dev/docs/svelte/best-practices) - This document outlines some best practices that will help you write fast, robust Svelte apps. It is ... + +6. [Migrating 8 SvelteKit Sites to Vite 8 in a day: What We Learned](https://cogley.jp/articles/migrating-sveltekit-to-vite-8) - If your SvelteKit guide references rollupOptions , update those references to rolldownOptions . And ... + +7. [Building REST APIs with Rust and Axum: A Practical Beginner's Guide](https://noqta.tn/en/tutorials/rust-axum-rest-api-beginner-guide-2026) - Learn how to build fast, safe REST APIs using Rust and the Axum web framework. This step-by-step gui... + +8. [Rust App Security: Master OAuth 2.0 & JWT](https://codezup.com/securing-rust-oauth-jwt/) - Secure your Rust applications with OAuth 2.0 and JWT! Learn step-by-step implementation for robust a... + +9. [Typescript Application Security from A to Z: A Guide to Protecting ...](https://dev.to/devsdaddy/typescript-application-security-from-a-to-z-a-guide-to-protecting-against-obvious-and-55nh) - This article specifically provides simplified attack methods and vulnerability examples to make it e... + +10. [Password Hashing](https://oneuptime.com/blog/post/2026-01-07-rust-jwt-authentication/view) - Learn how to implement secure JWT authentication in Rust applications. This guide covers token gener... + +11. [Validate JWT using RS384 in Rust - SSOJet](https://ssojet.com/jwt-validation/validate-jwt-using-rs384-in-rust/) - Validate JWTs with RS384 in Rust. Secure your APIs by verifying token signatures efficiently and rel... + +12. [JSON Web Token -- some investigative studies on crate ...](https://dev.to/behainguyen/rust-json-web-token-some-investigative-studies-on-crate-jsonwebtoken-1mch) - Regarding crate jsonwebtoken, the primary question is still how to check if a token is still valid,.... + +13. [Writing my first Rust crate: jsonwebtoken](https://www.vincentprouillet.com/blog/writing-my-first-crate/) - Experience writing a JWT library in Rust + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..c745df6 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,51 @@ +import js from '@eslint/js'; +import ts from 'typescript-eslint'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import svelteParser from 'svelte-eslint-parser'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts'], + languageOptions: { + parser: svelteParser, + parserOptions: { + parser: ts.parser, + extraFileExtensions: ['.svelte'] + } + } + }, + { + ignores: ['build/', '.svelte-kit/', 'dist/'] + }, + { + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + '@typescript-eslint/no-explicit-any': 'warn', + 'svelte/no-at-html-tags': 'error', // Catch XSS sinks + 'svelte/require-each-key': 'warn', + 'svelte/no-navigation-without-resolve': 'off', // Noisy + 'svelte/no-useless-children-snippet': 'warn' + } + } +]; diff --git a/frontend/package.json b/frontend/package.json index 5717e4b..0b94ebf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,19 +8,26 @@ "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@playwright/test": "^1.59.1", - "@types/node": "^22", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.59.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@types/node": "^22", "@typescript/native-preview": "^7.0.0-dev", + "eslint": "^10.3.0", + "eslint-plugin-svelte": "^3.17.1", + "globals": "^17.6.0", "svelte": "^5.55.5", "svelte-check": "^4", + "svelte-eslint-parser": "^1.6.0", "typescript": "^5", + "typescript-eslint": "^8.59.1", "vite": "^8.0.10" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a2db985..cba8631 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,33 +8,51 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.3.0) '@playwright/test': specifier: ^1.59.1 version: 1.59.1 '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17))) + version: 3.0.10(@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17))) '@sveltejs/kit': specifier: ^2.59.0 - version: 2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)) + version: 2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)) '@types/node': specifier: ^22 version: 22.19.17 '@typescript/native-preview': specifier: ^7.0.0-dev version: 7.0.0-dev.20260428.1 + eslint: + specifier: ^10.3.0 + version: 10.3.0 + eslint-plugin-svelte: + specifier: ^3.17.1 + version: 3.17.1(eslint@10.3.0)(svelte@5.55.5(@typescript-eslint/types@8.59.1)) + globals: + specifier: ^17.6.0 + version: 17.6.0 svelte: specifier: ^5.55.5 - version: 5.55.5 + version: 5.55.5(@typescript-eslint/types@8.59.1) svelte-check: specifier: ^4 - version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5)(typescript@5.9.3) + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3) + svelte-eslint-parser: + specifier: ^1.6.0 + version: 1.6.0(svelte@5.55.5(@typescript-eslint/types@8.59.1)) typescript: specifier: ^5 version: 5.9.3 + typescript-eslint: + specifier: ^8.59.1 + version: 8.59.1(eslint@10.3.0)(typescript@5.9.3) vite: specifier: ^8.0.10 version: 8.0.10(@types/node@22.19.17) @@ -50,6 +68,65 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -223,15 +300,80 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.59.1': + resolution: {integrity: sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.1': + resolution: {integrity: sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.1': + resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.1': + resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.1': + resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.1': + resolution: {integrity: sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.1': + resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.1': + resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.1': + resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.1': + resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260428.1': resolution: {integrity: sha512-Lll6WmXfgTEj1G3QBIoHlabQwUtJiyhlRgSLksa06QFL5BoA7V+Lu1waa9PtPNZbGsXLDMHodtk/bRQABKuPiw==} engines: {node: '>=16.20.0'} @@ -279,11 +421,19 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -292,6 +442,14 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -304,6 +462,27 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -315,9 +494,65 @@ packages: devalue@5.7.1: resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-svelte@3.17.1: + resolution: {integrity: sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.3.0: + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrap@2.2.5: resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} peerDependencies: @@ -326,6 +561,27 @@ packages: '@typescript-eslint/types': optional: true + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -335,6 +591,21 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -345,13 +616,67 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -426,12 +751,24 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -440,14 +777,40 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -465,10 +828,46 @@ packages: engines: {node: '>=18'} hasBin: true + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss@8.5.12: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -482,9 +881,22 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -501,6 +913,15 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' + svelte-eslint-parser@1.6.0: + resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + svelte@5.55.5: resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} engines: {node: '>=18'} @@ -513,9 +934,26 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.1: + resolution: {integrity: sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -524,6 +962,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@8.0.10: resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -575,6 +1019,23 @@ packages: vite: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -596,6 +1057,56 @@ snapshots: tslib: 2.8.1 optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0)': + dependencies: + eslint: 10.3.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.3.0)': + optionalDependencies: + eslint: 10.3.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -687,15 +1198,15 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)))': dependencies: - '@sveltejs/kit': 2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)) + '@sveltejs/kit': 2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17)) - '@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17))': + '@sveltejs/kit@2.59.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -706,17 +1217,17 @@ snapshots: mrmime: 2.0.1 set-cookie-parser: 3.1.0 sirv: 3.0.2 - svelte: 5.55.5 + svelte: 5.55.5(@typescript-eslint/types@8.59.1) vite: 8.0.10(@types/node@22.19.17) optionalDependencies: typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.1))(vite@8.0.10(@types/node@22.19.17))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.55.5 + svelte: 5.55.5(@typescript-eslint/types@8.59.1) vite: 8.0.10(@types/node@22.19.17) vitefu: 1.1.3(vite@8.0.10(@types/node@22.19.17)) @@ -727,14 +1238,109 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/json-schema@7.0.15': {} + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 '@types/trusted-types@2.0.7': {} + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@5.9.3))(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.1 + '@typescript-eslint/type-utils': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.1 + eslint: 10.3.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.1 + '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.1 + debug: 4.4.3 + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) + '@typescript-eslint/types': 8.59.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.1': + dependencies: + '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/visitor-keys': 8.59.1 + + '@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.1(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.3.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.1': {} + + '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) + '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/visitor-keys': 8.59.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.1(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@typescript-eslint/scope-manager': 8.59.1 + '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.1': + dependencies: + '@typescript-eslint/types': 8.59.1 + eslint-visitor-keys: 5.0.1 + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260428.1': optional: true @@ -766,12 +1372,29 @@ snapshots: '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260428.1 '@typescript/native-preview-win32-x64': 7.0.0-dev.20260428.1 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + aria-query@5.3.1: {} axobject-query@4.1.0: {} + balanced-match@4.0.4: {} + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -780,34 +1403,208 @@ snapshots: cookie@0.6.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + deepmerge@4.3.1: {} detect-libc@2.1.2: {} devalue@5.7.1: {} + escape-string-regexp@4.0.0: {} + + eslint-plugin-svelte@3.17.1(eslint@10.3.0)(svelte@5.55.5(@typescript-eslint/types@8.59.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 10.3.0 + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.12 + postcss-load-config: 3.1.4(postcss@8.5.12) + postcss-safe-parser: 7.0.1(postcss@8.5.12) + semver: 7.7.4 + svelte-eslint-parser: 1.6.0(svelte@5.55.5(@typescript-eslint/types@8.59.1)) + optionalDependencies: + svelte: 5.55.5(@typescript-eslint/types@8.59.1) + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.3.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + esm-env@1.2.2: {} - esrap@2.2.5: + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.5(@typescript-eslint/types@8.59.1): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.59.1 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@16.5.0: {} + + globals@17.6.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 + isexe@2.0.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@4.1.5: {} + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -857,20 +1654,55 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@2.1.0: {} + locate-character@3.0.0: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + mri@1.2.0: {} mrmime@2.0.1: {} + ms@2.1.3: {} + nanoid@3.3.11: {} + natural-compare@1.4.0: {} + obug@2.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -883,12 +1715,36 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + postcss-load-config@3.1.4(postcss@8.5.12): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.3 + optionalDependencies: + postcss: 8.5.12 + + postcss-safe-parser@7.0.1(postcss@8.5.12): + dependencies: + postcss: 8.5.12 + + postcss-scss@4.0.9(postcss@8.5.12): + dependencies: + postcss: 8.5.12 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + readdirp@4.1.2: {} rolldown@1.0.0-rc.17: @@ -916,8 +1772,16 @@ snapshots: dependencies: mri: 1.2.0 + semver@7.7.4: {} + set-cookie-parser@3.1.0: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -926,19 +1790,31 @@ snapshots: source-map-js@1.2.1: {} - svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.5)(typescript@5.9.3): + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.59.1))(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.55.5 + svelte: 5.55.5(@typescript-eslint/types@8.59.1) typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte@5.55.5: + svelte-eslint-parser@1.6.0(svelte@5.55.5(@typescript-eslint/types@8.59.1)): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.12 + postcss-scss: 4.0.9(postcss@8.5.12) + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + optionalDependencies: + svelte: 5.55.5(@typescript-eslint/types@8.59.1) + + svelte@5.55.5(@typescript-eslint/types@8.59.1): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -951,7 +1827,7 @@ snapshots: clsx: 2.1.1 devalue: 5.7.1 esm-env: 1.2.2 - esrap: 2.2.5 + esrap: 2.2.5(@typescript-eslint/types@8.59.1) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -966,13 +1842,38 @@ snapshots: totalist@3.0.1: {} + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: optional: true + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.1(eslint@10.3.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0)(typescript@5.9.3))(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.3.0)(typescript@5.9.3) + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@6.21.0: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + vite@8.0.10(@types/node@22.19.17): dependencies: lightningcss: 1.32.0 @@ -988,4 +1889,14 @@ snapshots: optionalDependencies: vite: 8.0.10(@types/node@22.19.17) + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yaml@1.10.3: {} + + yocto-queue@0.1.0: {} + zimmerframe@1.1.4: {} diff --git a/frontend/src/lib/RoomCanvas.svelte b/frontend/src/lib/RoomCanvas.svelte index b784594..32caf8d 100644 --- a/frontend/src/lib/RoomCanvas.svelte +++ b/frontend/src/lib/RoomCanvas.svelte @@ -41,11 +41,13 @@ if (!draggingId || !editable) return; const index = elements.findIndex((el: LayoutElement) => el.id === draggingId); if (index === -1) return; + const el = elements[index]; + if (!el) return; const newX = Math.round((e.clientX - startX) / 10) * 10 / 40; const newY = Math.round((e.clientY - startY) / 10) * 10 / 40; - elements[index] = { ...elements[index], x: newX, y: newY }; + elements[index] = { ...el, x: newX, y: newY }; } function handleMouseUp() { @@ -78,8 +80,7 @@ {/if} - {#each elements as el} - + {#each elements as el (el.id)} { const formData = new FormData(); formData.append('file', file); - return request(`/admin/courses/${course_id}/students/import`, { + return request<{count: number}>(`/admin/courses/${course_id}/students/import`, { method: 'POST', body: formData }); @@ -87,13 +88,13 @@ export const api = { }, rooms: { list: () => request('/admin/rooms'), - create: (name: string, layout: any[]) => + create: (name: string, layout: LayoutElement[]) => request('/admin/rooms', { method: 'POST', body: JSON.stringify({ name, layout }) }), get: (id: number) => request(`/admin/rooms/${id}`), - updateLayout: (id: number, layout: any[]) => + updateLayout: (id: number, layout: LayoutElement[]) => request(`/admin/rooms/${id}/layout`, { method: 'PUT', body: JSON.stringify(layout) @@ -106,7 +107,7 @@ export const api = { method: 'POST', body: JSON.stringify({ course_id, week_nr, date }) }), - getAttendance: (id: number) => request(`/admin/sessions/${id}/attendance`), + getAttendance: (id: number) => request(`/admin/sessions/${id}/attendance`), }, slots: { create: (session_id: number, tutor_id: number, start_time: string, end_time: string, room_id?: number) => @@ -143,10 +144,10 @@ export const api = { } }, checkin: { - getInfo: (code: string) => request(`/checkin/${code}`), + getInfo: (code: string) => request(`/checkin/${code}`), getStudents: (code: string) => request(`/checkin/${code}/students`), post: (code: string, student_id: number, seat_id?: string) => - request('/checkin', { + request('/checkin', { method: 'POST', body: JSON.stringify({ code, student_id, seat_id }) }), diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 0b5d712..085eeb1 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -23,7 +23,7 @@ export const auth = { _isSuperadmin = false; _authenticated = false; } - } catch (e) { + } catch (_e) { _isSuperadmin = false; _authenticated = false; } finally { @@ -40,7 +40,9 @@ export const auth = { async logout() { try { await api.auth.logout(); - } catch (e) {} + } catch (_e) { + console.error('logout failed', _e); + } _isSuperadmin = false; _authenticated = false; if (browser) { diff --git a/frontend/src/lib/components/NoteEditor.svelte b/frontend/src/lib/components/NoteEditor.svelte index 62601fd..e8b5cbc 100644 --- a/frontend/src/lib/components/NoteEditor.svelte +++ b/frontend/src/lib/components/NoteEditor.svelte @@ -76,7 +76,7 @@ await api.admin.slots.upsertNote(slotId, selectedStudentId, noteContent); const now = new Date(); savedAt = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; - } catch (_) { + } catch { // silent β€” user sees no feedback on transient failure } } @@ -98,7 +98,7 @@
- {#each present as s} + {#each present as s (s.id)} {@const isSel = s.id === selectedStudentId} {/each} - {#each absent as s} + {#each absent as s (s.id)}