From 1400b2fc956b92267d7342488cdef692912174f7 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Tue, 3 Mar 2026 10:29:24 -0800 Subject: [PATCH 1/5] lib, make adapter clonable & make result public --- koucha/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/koucha/src/lib.rs b/koucha/src/lib.rs index a20a9dc..3158657 100644 --- a/koucha/src/lib.rs +++ b/koucha/src/lib.rs @@ -1,6 +1,6 @@ use std::error::Error; -type Result = std::result::Result>; +pub type Result = std::result::Result>; pub mod db; pub mod fetch; @@ -9,7 +9,9 @@ pub mod score; #[cfg(test)] pub mod test_utils; +#[derive(Clone)] pub struct AdapterPool(sqlx::SqlitePool); +#[derive(Clone)] pub struct AdapterClient(reqwest::Client); pub struct AdapterBuilder { @@ -38,6 +40,7 @@ impl AdapterBuilder { } } +#[derive(Clone)] pub struct Adapter { db: AdapterPool, client: AdapterClient, From 92763fd7dc3f84fd49edf6734a285c39c2a119ff Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Tue, 3 Mar 2026 10:45:36 -0800 Subject: [PATCH 2/5] webapi, inital schema and routes (get_users) provides the infrastructure for the webapi including setting up the server and the routes. Implements get_users as a test route --- koucha/Cargo.lock | 92 +++++++++++++++++++++++ koucha/Cargo.toml | 3 + koucha/src/bin/server.rs | 0 koucha/src/bin/webapi/main.rs | 25 ++++++ koucha/src/bin/webapi/routes/get_users.rs | 36 +++++++++ koucha/src/bin/webapi/routes/mod.rs | 35 +++++++++ koucha/src/bin/webapi/types.rs | 6 ++ 7 files changed, 197 insertions(+) delete mode 100644 koucha/src/bin/server.rs create mode 100644 koucha/src/bin/webapi/main.rs create mode 100644 koucha/src/bin/webapi/routes/get_users.rs create mode 100644 koucha/src/bin/webapi/routes/mod.rs create mode 100644 koucha/src/bin/webapi/types.rs diff --git a/koucha/Cargo.lock b/koucha/Cargo.lock index c9fd8ab..d78118a 100644 --- a/koucha/Cargo.lock +++ b/koucha/Cargo.lock @@ -73,6 +73,70 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "base64" version = "0.22.1" @@ -726,6 +790,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -740,6 +810,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -999,9 +1070,12 @@ dependencies = [ name = "koucha" version = "0.1.0" dependencies = [ + "axum", "chrono", "reqwest", "rss", + "serde", + "serde_json", "sqlx", "tokio", ] @@ -1076,6 +1150,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1708,6 +1788,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2221,6 +2312,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/koucha/Cargo.toml b/koucha/Cargo.toml index 1106901..2de5afc 100644 --- a/koucha/Cargo.toml +++ b/koucha/Cargo.toml @@ -9,3 +9,6 @@ rss = "2.0.12" tokio = { version = "1.49.0", features = ["full"] } sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite" ] } chrono = "0.4.43" +axum = { version= "0.8.8", features = [ "macros" ] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.149" diff --git a/koucha/src/bin/server.rs b/koucha/src/bin/server.rs deleted file mode 100644 index e69de29..0000000 diff --git a/koucha/src/bin/webapi/main.rs b/koucha/src/bin/webapi/main.rs new file mode 100644 index 0000000..21cb1eb --- /dev/null +++ b/koucha/src/bin/webapi/main.rs @@ -0,0 +1,25 @@ +use koucha::{ + AdapterBuilder, + Adapter, +}; + +mod routes; +mod types; + +#[derive(Clone)] +struct AppState { + pub adapter: Adapter, +} + +#[tokio::main] +async fn main() { + let db_url = std::env::var("DATABASE_URL").unwrap(); + let adapter = AdapterBuilder::new() + .database_url(&db_url) + .create().await.unwrap(); + + let app = routes::router().with_state(AppState { adapter }); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:4142").await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/koucha/src/bin/webapi/routes/get_users.rs b/koucha/src/bin/webapi/routes/get_users.rs new file mode 100644 index 0000000..14ecf38 --- /dev/null +++ b/koucha/src/bin/webapi/routes/get_users.rs @@ -0,0 +1,36 @@ +use axum::{Json, extract::State}; +use koucha::db::User as DbUser; +use reqwest::StatusCode; +use serde::{Serialize, Deserialize}; + +use crate::{ + AppState, routes::{ApiError, ApiResult, ApiResponse}, types::User +}; + +#[derive(Deserialize)] +pub struct Input { } +#[derive(Serialize, Deserialize)] +pub struct Output { + users: Vec, +} + +pub async fn handler( + State(state): State, + Json(_body): Json, +) -> ApiResult { + let dbusers = DbUser::get_all(state.adapter.get_pool()) + .await.map_err(|_e| { + // TODO: Logging + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + error: "InternalError", + message: "Error getting all users from DB.".to_string(), + } + })?; + + let users: Vec = dbusers.iter().map(|u| User { + name: u.name().to_string(), + }).collect(); + + Ok(ApiResponse(StatusCode::OK, Output { users })) +} diff --git a/koucha/src/bin/webapi/routes/mod.rs b/koucha/src/bin/webapi/routes/mod.rs new file mode 100644 index 0000000..acc21cf --- /dev/null +++ b/koucha/src/bin/webapi/routes/mod.rs @@ -0,0 +1,35 @@ +use serde::Serialize; +use serde_json::json; +use axum::{Json, Router, response::IntoResponse, routing::get}; +use reqwest::StatusCode; + +mod get_users; + +use crate::AppState; + +pub type ApiResult = Result, ApiError>; + +pub struct ApiResponse(pub StatusCode, pub T); + +pub struct ApiError { + pub status: StatusCode, + pub error: &'static str, + pub message: String, +} + +pub fn router() -> Router { + Router::new() + .route("/get_users", get(get_users::handler)) +} + +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + let body = json!({ "error": self.error, "message": self.message }); + (self.status, Json(body)).into_response() + } +} +impl IntoResponse for ApiResponse { + fn into_response(self) -> axum::response::Response { + (self.0, Json(self.1)).into_response() + } +} diff --git a/koucha/src/bin/webapi/types.rs b/koucha/src/bin/webapi/types.rs new file mode 100644 index 0000000..2216352 --- /dev/null +++ b/koucha/src/bin/webapi/types.rs @@ -0,0 +1,6 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct User { + pub name: String, +} From 949a984d0c5e87ff62e4fc12a82a23e202ac691e Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Tue, 3 Mar 2026 11:33:46 -0800 Subject: [PATCH 3/5] routes, new_user route --- koucha/src/bin/webapi/routes/mod.rs | 12 ++++++- koucha/src/bin/webapi/routes/new_user.rs | 40 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 koucha/src/bin/webapi/routes/new_user.rs diff --git a/koucha/src/bin/webapi/routes/mod.rs b/koucha/src/bin/webapi/routes/mod.rs index acc21cf..ce4477a 100644 --- a/koucha/src/bin/webapi/routes/mod.rs +++ b/koucha/src/bin/webapi/routes/mod.rs @@ -1,9 +1,18 @@ use serde::Serialize; use serde_json::json; -use axum::{Json, Router, response::IntoResponse, routing::get}; +use axum::{ + Json, + Router, + response::IntoResponse, + routing::{ + get, + post, + }, +}; use reqwest::StatusCode; mod get_users; +mod new_user; use crate::AppState; @@ -20,6 +29,7 @@ pub struct ApiError { pub fn router() -> Router { Router::new() .route("/get_users", get(get_users::handler)) + .route("/new_user", post(new_user::handler)) } impl IntoResponse for ApiError { diff --git a/koucha/src/bin/webapi/routes/new_user.rs b/koucha/src/bin/webapi/routes/new_user.rs new file mode 100644 index 0000000..4c0fab9 --- /dev/null +++ b/koucha/src/bin/webapi/routes/new_user.rs @@ -0,0 +1,40 @@ +use axum::{Json, extract::State}; +use koucha::db::User as DbUser; +use reqwest::StatusCode; +use serde::{Serialize, Deserialize}; + +use crate::{ + AppState, routes::{ApiError, ApiResult, ApiResponse}, types::User +}; + +#[derive(Deserialize)] +pub struct Input { + user_name: String, +} +#[derive(Serialize, Deserialize)] +pub struct Output { + user: User, +} + +pub async fn handler( + State(state): State, + Json(body): Json, +) -> ApiResult { + let dbuser = DbUser::create( + state.adapter.get_pool(), + &body.user_name + ).await.map_err(|_e| { + // TODO: Logging + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + error: "InternalError", + message: String::from( + "Error creating user ".to_owned() + &body.user_name + ), + } + })?; + + let user = User { name: dbuser.name().to_string() }; + + Ok(ApiResponse(StatusCode::OK, Output { user })) +} From b089f62bcd645b2597da8bc77b2b373857cdcfa3 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Thu, 5 Mar 2026 10:15:16 -0800 Subject: [PATCH 4/5] db&webapi, primitive session tokens adds a temporary auth method to users which does not require a password or similar. This is just for testing right now and assumes a self-hosted no-threats environment. Also adds a user_key state to keep track of authed users. These currently *DO NOT EXPIRE* which is pretty bad haha. The entire auth system will be redone. --- koucha/src/bin/webapi/main.rs | 24 +++++++++-- .../src/bin/webapi/routes/create_session.rs | 41 +++++++++++++++++++ koucha/src/bin/webapi/routes/mod.rs | 2 + koucha/src/bin/webapi/types.rs | 11 +++++ koucha/src/db/user.rs | 11 +++++ 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 koucha/src/bin/webapi/routes/create_session.rs diff --git a/koucha/src/bin/webapi/main.rs b/koucha/src/bin/webapi/main.rs index 21cb1eb..bf09bad 100644 --- a/koucha/src/bin/webapi/main.rs +++ b/koucha/src/bin/webapi/main.rs @@ -1,24 +1,42 @@ +use std::collections::HashMap; use koucha::{ - AdapterBuilder, - Adapter, + Adapter, AdapterBuilder, db::{User, UserKey as DbUserKey} }; +use crate::types::UserKey; + mod routes; mod types; #[derive(Clone)] struct AppState { pub adapter: Adapter, + // TODO: Set up UserKey expirations + auth_map: HashMap, } +impl AppState { + fn create_user_key(&mut self, db_user_key: DbUserKey) -> UserKey { + let key = UserKey::new(); + self.auth_map.insert(key.clone(), db_user_key); + key + } + #[allow(dead_code)] + fn user_from_key(&self, key: &UserKey) -> Option { + self.auth_map.get(key).map(DbUserKey::clone) + } +} + + #[tokio::main] async fn main() { let db_url = std::env::var("DATABASE_URL").unwrap(); let adapter = AdapterBuilder::new() .database_url(&db_url) .create().await.unwrap(); + let auth_map = HashMap::new(); - let app = routes::router().with_state(AppState { adapter }); + let app = routes::router().with_state(AppState { adapter, auth_map }); let listener = tokio::net::TcpListener::bind("0.0.0.0:4142").await.unwrap(); axum::serve(listener, app).await.unwrap(); diff --git a/koucha/src/bin/webapi/routes/create_session.rs b/koucha/src/bin/webapi/routes/create_session.rs new file mode 100644 index 0000000..fd74294 --- /dev/null +++ b/koucha/src/bin/webapi/routes/create_session.rs @@ -0,0 +1,41 @@ +use axum::{Json, extract::State}; +use koucha::db::User as DbUser; +use reqwest::StatusCode; +use serde::{Serialize, Deserialize}; + +use crate::{ + AppState, routes::{ApiError, ApiResult, ApiResponse}, types::UserKey +}; + +#[derive(Deserialize)] +pub struct Input { + pub user_name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Output { + user_key: UserKey, +} + +pub async fn handler( + State(mut state): State, + Json(body): Json, +) -> ApiResult { + let dbuser = DbUser::temporary_auth( + state.adapter.get_pool(), + &body.user_name + ).await.map_err(|_e| { + // TODO: Logging + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + error: "InternalError", + message: String::from( + "Error authentiating user ".to_owned() + &body.user_name + ), + } + })?; + + let key = state.create_user_key(dbuser.key()); + + Ok(ApiResponse(StatusCode::OK, Output { user_key: key })) +} diff --git a/koucha/src/bin/webapi/routes/mod.rs b/koucha/src/bin/webapi/routes/mod.rs index ce4477a..58afe73 100644 --- a/koucha/src/bin/webapi/routes/mod.rs +++ b/koucha/src/bin/webapi/routes/mod.rs @@ -11,6 +11,7 @@ use axum::{ }; use reqwest::StatusCode; +mod create_session; mod get_users; mod new_user; @@ -28,6 +29,7 @@ pub struct ApiError { pub fn router() -> Router { Router::new() + .route("/create_session", post(create_session::handler)) .route("/get_users", get(get_users::handler)) .route("/new_user", post(new_user::handler)) } diff --git a/koucha/src/bin/webapi/types.rs b/koucha/src/bin/webapi/types.rs index 2216352..5a2846c 100644 --- a/koucha/src/bin/webapi/types.rs +++ b/koucha/src/bin/webapi/types.rs @@ -1,6 +1,17 @@ +use chrono::Utc; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct User { pub name: String, } + +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct UserKey(String); +impl UserKey { + pub fn new() -> Self { + UserKey( + Utc::now().to_rfc3339() + ) + } +} diff --git a/koucha/src/db/user.rs b/koucha/src/db/user.rs index 038b82a..ff58605 100644 --- a/koucha/src/db/user.rs +++ b/koucha/src/db/user.rs @@ -29,6 +29,17 @@ pub struct User { impl User { pub fn key(&self) -> UserKey { self.key } pub fn name(&self) -> &str { &self.name } + + pub async fn temporary_auth( + pool: &AdapterPool, + name: &str, + ) -> Result { + sqlx::query_as!( + UnparsedUser, + "SELECT id as `id!`, name FROM users WHERE name = ?", + name + ).fetch_one(&pool.0).await?.parse() + } pub async fn get(pool: &AdapterPool, key: UserKey) -> Result { sqlx::query_as!( From 8a627243345e6d69277a2ebb33402ed5b4c56cb1 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Tue, 12 May 2026 21:07:10 -0700 Subject: [PATCH 5/5] WIP: Get Feeds --- koucha/src/bin/webapi/routes/get_feeds.rs | 37 +++++++++++++++++++++++ koucha/src/bin/webapi/routes/mod.rs | 2 ++ 2 files changed, 39 insertions(+) create mode 100644 koucha/src/bin/webapi/routes/get_feeds.rs diff --git a/koucha/src/bin/webapi/routes/get_feeds.rs b/koucha/src/bin/webapi/routes/get_feeds.rs new file mode 100644 index 0000000..0fb7488 --- /dev/null +++ b/koucha/src/bin/webapi/routes/get_feeds.rs @@ -0,0 +1,37 @@ +use axum::{Json, extract::State, http::HeaderMap}; +use koucha::db::User as DbUser; +use reqwest::StatusCode; +use serde::{Serialize, Deserialize}; + +use crate::{ + AppState, routes::{ApiError, ApiResult, ApiResponse}, types::User +}; + +#[derive(Deserialize)] +pub struct Input { } +#[derive(Serialize, Deserialize)] +pub struct Output { + users: Vec, +} + +pub async fn handler( + headers: HeaderMap, + State(state): State, + Json(_body): Json, +) -> ApiResult { + let dbusers = DbUser::get_all(state.adapter.get_pool()) + .await.map_err(|_e| { + // TODO: Logging + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + error: "InternalError", + message: "Error getting all users from DB.".to_string(), + } + })?; + + let users: Vec = dbusers.iter().map(|u| User { + name: u.name().to_string(), + }).collect(); + + Ok(ApiResponse(StatusCode::OK, Output { users })) +} diff --git a/koucha/src/bin/webapi/routes/mod.rs b/koucha/src/bin/webapi/routes/mod.rs index 58afe73..24dc59c 100644 --- a/koucha/src/bin/webapi/routes/mod.rs +++ b/koucha/src/bin/webapi/routes/mod.rs @@ -11,6 +11,7 @@ use axum::{ }; use reqwest::StatusCode; +mod get_feeds; mod create_session; mod get_users; mod new_user; @@ -30,6 +31,7 @@ pub struct ApiError { pub fn router() -> Router { Router::new() .route("/create_session", post(create_session::handler)) + .route("/get_feeds", get(get_feeds::handler)) .route("/get_users", get(get_users::handler)) .route("/new_user", post(new_user::handler)) }