feat: scaffold newsfeed — user-controlled news feed
A self-hosted feed where the user owns the ranking: per-source and signed per-interest weights, decayed by recency, via a transparent deterministic scorer. Content is sourced algorithmically (worker RSS/Atom polling) and agentically (per-user API tokens POSTing candidates to the ingest endpoint). Single-user today, multi-user by construction (every row keyed on user_id). Rust cargo workspace + Vite/React/SWC/TS SPA: - newsfeed-entities: DTOs (ts-rs bindings -> web/src/api/bindings) - newsfeed-core: ranking, auth primitives, ingest, data-access ports - newsfeed-data: SQLite adapters (sqlx, runtime queries) - newsfeed-api: axum REST/JSON daemon - newsfeed-worker: RSS polling + rescoring loop - web: responsive, mobile-first SPA (React Query, generated types) Deploy (Gitea Actions, build static musl + SPA, rsync as gitea_ci): api+worker -> slartibartfast, SPA -> oolon (nginx serves + proxies /v1). Deliberate deviations from house conventions (documented in CLAUDE.md/readme): - SQLite instead of Postgres; api+worker co-locate sharing one DB file. - Runtime sqlx queries instead of query! macros (SQLite dynamic typing; keeps CI database-free). Verified end-to-end: auth, token ingest, interest-weighted ranking, signals, pagination (curl + browser); cargo fmt/clippy -D/test and pnpm build/lint pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fKZzDpvjiJ9eYbPGgJvUP
This commit is contained in:
74
crates/newsfeed-api/src/routes/mod.rs
Normal file
74
crates/newsfeed-api/src/routes/mod.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Router assembly. Public API is versioned under `/v1` from day one; `/health` is
|
||||
//! unversioned for infrastructure probes.
|
||||
|
||||
mod auth_routes;
|
||||
mod feed;
|
||||
mod health;
|
||||
mod ingest;
|
||||
mod interests;
|
||||
mod sources;
|
||||
mod tokens;
|
||||
|
||||
use axum::Router;
|
||||
use axum::http::{Method, header};
|
||||
use axum::routing::{delete, get, post};
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Build the full application router.
|
||||
pub fn router(state: AppState) -> Router {
|
||||
let cors = build_cors(&state);
|
||||
|
||||
let v1 = Router::new()
|
||||
// auth
|
||||
.route("/auth/register", post(auth_routes::register))
|
||||
.route("/auth/login", post(auth_routes::login))
|
||||
.route("/auth/logout", post(auth_routes::logout))
|
||||
.route("/auth/me", get(auth_routes::me))
|
||||
// feed
|
||||
.route("/feed", get(feed::get_feed))
|
||||
.route("/feed/signals", post(feed::post_signal))
|
||||
// sources
|
||||
.route("/sources", get(sources::list).post(sources::create))
|
||||
.route("/sources/{id}", delete(sources::remove))
|
||||
// interests
|
||||
.route("/interests", get(interests::list).put(interests::upsert))
|
||||
.route("/interests/{id}", delete(interests::remove))
|
||||
// api tokens
|
||||
.route("/tokens", get(tokens::list).post(tokens::create))
|
||||
.route("/tokens/{id}", delete(tokens::revoke))
|
||||
// ingest (bearer-token authenticated)
|
||||
.route("/ingest/candidates", post(ingest::submit));
|
||||
|
||||
Router::new()
|
||||
.route("/health", get(health::health))
|
||||
.nest("/v1", v1)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(cors)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// CORS is only needed in development, where the Vite dev server is a different origin
|
||||
/// from the API. In production nginx serves the SPA and proxies the API under one origin,
|
||||
/// so `cors_origins` is empty and no cross-origin headers are emitted.
|
||||
fn build_cors(state: &AppState) -> CorsLayer {
|
||||
if state.config.cors_origins.is_empty() {
|
||||
return CorsLayer::new();
|
||||
}
|
||||
let origins = state
|
||||
.config
|
||||
.cors_origins
|
||||
.iter()
|
||||
.filter_map(|o| o.parse().ok())
|
||||
.collect::<Vec<_>>();
|
||||
// Credentialed CORS forbids wildcard method/header lists, so enumerate them.
|
||||
CorsLayer::new()
|
||||
.allow_origin(origins)
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
|
||||
.allow_credentials(true)
|
||||
}
|
||||
Reference in New Issue
Block a user