S3+S4: user param auth, static file serving, Docker deploy config

- replace hardcoded benji with ?user= query param
- add GET /api/users and GET /api/me?user= endpoints
- serve frontend static files via tower-http ServeDir
- add multi-stage Dockerfile (Rust + Vite → single image)
- add docker-compose.yml + Caddyfile for apes.unslope.com
- frontend: getCurrentUsername() from URL param → localStorage
- sidebar shows current user

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 19:18:06 +02:00
parent 0b6244390e
commit f8420496b2
8 changed files with 156 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ mod routes;
use axum::{routing::get, Router};
use sqlx::sqlite::SqlitePoolOptions;
use std::env;
use tower_http::services::{ServeDir, ServeFile};
#[tokio::main]
async fn main() {
@@ -32,6 +33,8 @@ async fn main() {
let app = Router::new()
.route("/api/health", get(routes::health))
.route("/api/users", get(routes::list_users))
.route("/api/me", get(routes::get_me))
.route(
"/api/channels",
get(routes::list_channels).post(routes::create_channel),
@@ -41,6 +44,10 @@ async fn main() {
"/api/channels/{channel_id}/messages",
get(routes::list_messages).post(routes::post_message),
)
// Serve frontend static files, fallback to index.html for SPA routing
.fallback_service(
ServeDir::new("static").fallback(ServeFile::new("static/index.html")),
)
.with_state(pool);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))

View File

@@ -45,6 +45,28 @@ impl From<sqlx::Error> for AppError {
type Result<T> = std::result::Result<T, AppError>;
// ── User identity from ?user= param ──
#[derive(Debug, serde::Deserialize)]
pub struct UserParam {
pub user: Option<String>,
}
async fn resolve_user(db: &SqlitePool, param: &UserParam) -> Result<String> {
let username = param.user.as_deref().unwrap_or("benji");
let row = sqlx::query_scalar::<_, String>(
"SELECT id FROM users WHERE username = ?",
)
.bind(username)
.fetch_optional(db)
.await?;
match row {
Some(id) => Ok(id),
None => Err(AppError::BadRequest(format!("Unknown user: {username}"))),
}
}
// ── Health ──
pub async fn health() -> &'static str {
@@ -62,13 +84,35 @@ pub async fn list_channels(State(db): State<SqlitePool>) -> Result<Json<Vec<Chan
Ok(Json(channels))
}
pub async fn list_users(State(db): State<SqlitePool>) -> Result<Json<Vec<User>>> {
let rows = sqlx::query_as::<_, UserRow>("SELECT * FROM users ORDER BY created_at")
.fetch_all(&db)
.await?;
Ok(Json(rows.iter().map(|r| r.to_api()).collect()))
}
pub async fn get_me(
State(db): State<SqlitePool>,
Query(user_param): Query<UserParam>,
) -> Result<Json<User>> {
let username = user_param.user.as_deref().unwrap_or("benji");
let row = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE username = ?")
.bind(username)
.fetch_optional(&db)
.await?;
match row {
Some(r) => Ok(Json(r.to_api())),
None => Err(AppError::NotFound(format!("User {username} not found"))),
}
}
pub async fn create_channel(
State(db): State<SqlitePool>,
Query(user_param): Query<UserParam>,
Json(body): Json<CreateChannel>,
) -> Result<impl IntoResponse> {
let id = Uuid::new_v4().to_string();
// Hardcoded to benji for now (no auth yet — S4 will extract from middleware)
let created_by = "00000000-0000-0000-0000-000000000001";
let created_by = resolve_user(&db, &user_param).await?;
sqlx::query("INSERT INTO channels (id, name, description, created_by) VALUES (?, ?, ?, ?)")
.bind(&id)
@@ -150,6 +194,7 @@ pub async fn list_messages(
pub async fn post_message(
State(db): State<SqlitePool>,
Path(channel_id): Path<String>,
Query(user_param): Query<UserParam>,
Json(body): Json<PostMessage>,
) -> Result<impl IntoResponse> {
// Verify channel exists
@@ -183,8 +228,7 @@ pub async fn post_message(
}
let id = Uuid::new_v4().to_string();
// Hardcoded to benji for now (no auth yet — S4 will extract from middleware)
let user_id = "00000000-0000-0000-0000-000000000001";
let user_id = resolve_user(&db, &user_param).await?;
let msg_type = match body.r#type {
MessageType::Text => "text",