Files
EventSnap/backend/src/main.rs
MechaCat02 71a2987a3e feat: implement host dashboard
Add Host Dashboard for event and guest management, accessible at /host.

Backend — new /api/v1/host/* endpoints (RequireHost auth):
- GET  /host/event                    → event name + lock/release state
- POST /host/event/close|open         → lock or unlock uploads; SSE broadcast
- POST /host/gallery/release          → set release timestamp, enqueue export jobs
- GET  /host/users                    → all guests with upload count & bytes
- POST /host/users/{id}/ban           → ban with optional upload-hide choice
- POST /host/users/{id}/unban         → lift ban
- PATCH /host/users/{id}/role         → promote guest→host or demote host→guest
- DELETE /host/upload/{id}            → host-level soft-delete + SSE
- DELETE /host/comment/{id}           → host-level soft-delete

Frontend — /host page:
- Event controls: lock/unlock toggle and release-gallery button with status badges
- Guest table: display name, role badge, upload count, storage used
- Ban flow: modal asking whether to keep or hide the user's uploads
- Promote/demote buttons respecting caller role (host can promote guests; admin can demote hosts)
- auth.ts: getRole() decodes JWT payload client-side to gate the route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:43:09 +02:00

89 lines
3.3 KiB
Rust

use anyhow::Result;
use axum::routing::{delete, get, patch, post};
use axum::Router;
use tower_http::services::ServeDir;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod auth;
mod config;
mod db;
mod error;
mod handlers;
mod models;
mod services;
mod state;
use config::AppConfig;
use state::AppState;
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
"eventsnap_backend=debug,tower_http=debug".into()
}))
.with(tracing_subscriber::fmt::layer())
.init();
let config = AppConfig::from_env()?;
let pool = db::create_pool(&config.database_url).await?;
let state = AppState::new(pool, config.clone());
// Ensure media directories exist
tokio::fs::create_dir_all(&config.media_path).await.ok();
let api = Router::new()
// Auth
.route("/api/v1/join", post(auth::handlers::join))
.route("/api/v1/recover", post(auth::handlers::recover))
.route("/api/v1/admin/login", post(auth::handlers::admin_login))
.route("/api/v1/session", delete(auth::handlers::logout))
// Upload
.route("/api/v1/upload", post(handlers::upload::upload))
.route(
"/api/v1/upload/{id}",
patch(handlers::upload::edit_upload).delete(handlers::upload::delete_upload),
)
// Feed
.route("/api/v1/feed", get(handlers::feed::feed))
.route("/api/v1/feed/delta", get(handlers::feed::feed_delta))
.route("/api/v1/hashtags", get(handlers::feed::hashtags))
// Social
.route("/api/v1/upload/{id}/like", post(handlers::social::toggle_like))
.route(
"/api/v1/upload/{id}/comments",
get(handlers::social::list_comments).post(handlers::social::add_comment),
)
.route("/api/v1/comment/{id}", delete(handlers::social::delete_comment))
// SSE
.route("/api/v1/stream", get(handlers::sse::stream))
// Host Dashboard
.route("/api/v1/host/event", get(handlers::host::get_event_status))
.route("/api/v1/host/event/close", post(handlers::host::close_event))
.route("/api/v1/host/event/open", post(handlers::host::open_event))
.route("/api/v1/host/gallery/release", post(handlers::host::release_gallery))
.route("/api/v1/host/users", get(handlers::host::list_users))
.route("/api/v1/host/users/{id}/ban", post(handlers::host::ban_user))
.route("/api/v1/host/users/{id}/unban", post(handlers::host::unban_user))
.route("/api/v1/host/users/{id}/role", patch(handlers::host::set_role))
.route("/api/v1/host/upload/{id}", delete(handlers::host::host_delete_upload))
.route("/api/v1/host/comment/{id}", delete(handlers::host::host_delete_comment));
// Serve media files from disk
let media_service = ServeDir::new(&config.media_path);
let router = Router::new()
.route("/health", get(|| async { "ok" }))
.merge(api)
.nest_service("/media", media_service)
.with_state(state);
let listener = tokio::net::TcpListener::bind(("0.0.0.0", config.app_port)).await?;
tracing::info!("listening on {}", listener.local_addr()?);
axum::serve(listener, router).await?;
Ok(())
}