Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 536638b594 | |||
| 6cb5968b7b |
@@ -23,7 +23,7 @@ FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN useradd -u 1000 -m app
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/backend/target/release/attendance ./server
|
||||
COPY --from=backend-builder /app/backend/target/release/tutortool ./server
|
||||
COPY --from=backend-builder /app/backend/demo ./backend/demo
|
||||
COPY --from=frontend-builder /app/frontend/build ./frontend/build
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -56,7 +56,7 @@ test-up:
|
||||
exit 0; \
|
||||
fi; \
|
||||
[ -f "$$TT_TEST_DB" ] || $(MAKE) test-rebuild; \
|
||||
DATABASE_URL="sqlite:$$TT_TEST_DB" PORT=$$TT_TEST_PORT TT_TEST_MODE=1 \
|
||||
DATABASE_URL="sqlite:$$TT_TEST_DB" PORT=$$TT_TEST_PORT TT_TEST_MODE=1 JWT_SECRET=testsecret STATIC_DIR=frontend/build \
|
||||
cargo run --manifest-path backend/Cargo.toml &>/tmp/tutortool-test.log & \
|
||||
echo $$! > data/test/.pid; \
|
||||
echo "[test-up] Backend PID $$(cat data/test/.pid) starting on port $$TT_TEST_PORT..."; \
|
||||
@@ -91,4 +91,4 @@ test-reset:
|
||||
|
||||
test-e2e:
|
||||
$(MAKE) test-up
|
||||
pnpm --dir frontend test:e2e
|
||||
@. scripts/test-env.sh; cd frontend && pnpm test:e2e
|
||||
|
||||
@@ -13,7 +13,7 @@ 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"] }
|
||||
tower-http = { version = "0.6", features = ["fs", "cors", "trace"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rand = "0.9"
|
||||
thiserror = "2"
|
||||
|
||||
@@ -15,6 +15,7 @@ use tracing_subscriber::EnvFilter;
|
||||
pub struct AppState {
|
||||
pub pool: sqlx::SqlitePool,
|
||||
pub jwt_secret: String,
|
||||
pub test_mode: bool,
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<AppState> for sqlx::SqlitePool {
|
||||
@@ -46,7 +47,11 @@ async fn main() {
|
||||
let pool = db::init().await.expect("db init failed");
|
||||
db::maybe_seed_demo(&pool).await;
|
||||
|
||||
let state = AppState { pool, jwt_secret };
|
||||
let state = AppState {
|
||||
pool,
|
||||
jwt_secret,
|
||||
test_mode,
|
||||
};
|
||||
|
||||
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/build".into());
|
||||
|
||||
@@ -54,7 +59,8 @@ async fn main() {
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.fallback_service(
|
||||
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{static_dir}/index.html"))),
|
||||
);
|
||||
)
|
||||
.layer(tower_http::trace::TraceLayer::new_for_http());
|
||||
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into());
|
||||
let addr = format!("0.0.0.0:{port}");
|
||||
|
||||
@@ -38,22 +38,23 @@ async fn login(
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Strict)
|
||||
.secure(true) // Should be true in prod, but for local dev we might need to be careful.
|
||||
// Actually, most local setups use http, but we can stick to secure(true) and assume production-first.
|
||||
.secure(!state.test_mode)
|
||||
.build();
|
||||
|
||||
Ok((
|
||||
jar.add(cookie),
|
||||
Json(json!({"is_superadmin": is_superadmin})),
|
||||
))
|
||||
}
|
||||
|
||||
async fn me(auth: auth::TutorClaims) -> Json<Value> {
|
||||
Json(json!({
|
||||
"id": auth.sub,
|
||||
"email": auth.email,
|
||||
"is_superadmin": auth.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 logout(jar: CookieJar) -> CookieJar {
|
||||
|
||||
@@ -3,7 +3,7 @@ httpRoute:
|
||||
- tutor.puchstein.dev
|
||||
|
||||
image:
|
||||
tag: v0.1.8
|
||||
tag: v0.1.9
|
||||
|
||||
env:
|
||||
extra:
|
||||
|
||||
@@ -2,9 +2,10 @@ import { browser } from '$app/environment';
|
||||
|
||||
const BASE = '/api';
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(BASE + path, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init?.headers,
|
||||
|
||||
@@ -2,45 +2,49 @@ import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from './api';
|
||||
|
||||
class AuthState {
|
||||
#isSuperadmin = $state(false);
|
||||
#initialized = $state(false);
|
||||
#authenticated = $state(false);
|
||||
let _isSuperadmin = $state(false);
|
||||
let _initialized = $state(false);
|
||||
let _authenticated = $state(false);
|
||||
|
||||
get isSuperadmin() { return this.#isSuperadmin; }
|
||||
get initialized() { return this.#initialized; }
|
||||
get authenticated() { return this.#authenticated; }
|
||||
export const auth = {
|
||||
get isSuperadmin() { return _isSuperadmin; },
|
||||
get initialized() { return _initialized; },
|
||||
get authenticated() { return _authenticated; },
|
||||
|
||||
async init() {
|
||||
if (!browser || this.#initialized) return;
|
||||
if (!browser || _initialized) return;
|
||||
try {
|
||||
const me = await api.auth.me();
|
||||
this.#isSuperadmin = me.is_superadmin;
|
||||
this.#authenticated = true;
|
||||
const res = await fetch(`/api/auth/me?t=${Date.now()}`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const me = await res.json();
|
||||
_isSuperadmin = me.is_superadmin;
|
||||
_authenticated = true;
|
||||
} else {
|
||||
_isSuperadmin = false;
|
||||
_authenticated = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.#isSuperadmin = false;
|
||||
this.#authenticated = false;
|
||||
_isSuperadmin = false;
|
||||
_authenticated = false;
|
||||
} finally {
|
||||
this.#initialized = true;
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setAuthenticated(isSuperadmin: boolean) {
|
||||
this.#isSuperadmin = isSuperadmin;
|
||||
this.#authenticated = true;
|
||||
this.#initialized = true;
|
||||
}
|
||||
_isSuperadmin = isSuperadmin;
|
||||
_authenticated = true;
|
||||
_initialized = true;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
} catch (e) {}
|
||||
this.#isSuperadmin = false;
|
||||
this.#authenticated = false;
|
||||
_isSuperadmin = false;
|
||||
_authenticated = false;
|
||||
if (browser) {
|
||||
goto('/admin/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const auth = new AuthState();
|
||||
};
|
||||
|
||||
@@ -31,16 +31,22 @@
|
||||
const activePath = $derived($page.url.pathname);
|
||||
</script>
|
||||
|
||||
{#if auth.authenticated}
|
||||
<TutorShell
|
||||
{activePath}
|
||||
courseName={course?.name ?? ''}
|
||||
semester={course?.semester ?? ''}
|
||||
>
|
||||
{#snippet children()}
|
||||
{@render children()}
|
||||
{/snippet}
|
||||
</TutorShell>
|
||||
{#if auth.initialized}
|
||||
{#if auth.authenticated}
|
||||
<TutorShell
|
||||
{activePath}
|
||||
courseName={course?.name ?? ''}
|
||||
semester={course?.semester ?? ''}
|
||||
>
|
||||
{#snippet children()}
|
||||
{@render children()}
|
||||
{/snippet}
|
||||
</TutorShell>
|
||||
{:else}
|
||||
<div style="padding: 2rem; text-align: center;">Redirecting to login...</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{@render children()}
|
||||
<div style="padding: 2rem; text-align: center; font-family: var(--serif);">
|
||||
Wird geladen...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -43,7 +43,7 @@ async function globalSetup() {
|
||||
domain: new URL(baseURL).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
secure: baseURL.startsWith('https'),
|
||||
sameSite: 'Strict' as const,
|
||||
}
|
||||
],
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe('Superadmin CRUD & UI Consistency', () => {
|
||||
|
||||
await expect(page.locator('text=Neuen Kurs anlegen')).toBeVisible();
|
||||
|
||||
const tutorSelect = page.locator('select >> text=+ Hinzufügen').first();
|
||||
const tutorSelect = page.locator('select.tiny').first();
|
||||
await expect(tutorSelect).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user