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..bf09bad --- /dev/null +++ b/koucha/src/bin/webapi/main.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; +use koucha::{ + 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, 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/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/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..24dc59c --- /dev/null +++ b/koucha/src/bin/webapi/routes/mod.rs @@ -0,0 +1,49 @@ +use serde::Serialize; +use serde_json::json; +use axum::{ + Json, + Router, + response::IntoResponse, + routing::{ + get, + post, + }, +}; +use reqwest::StatusCode; + +mod get_feeds; +mod create_session; +mod get_users; +mod new_user; + +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("/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)) +} + +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/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 })) +} diff --git a/koucha/src/bin/webapi/types.rs b/koucha/src/bin/webapi/types.rs new file mode 100644 index 0000000..5a2846c --- /dev/null +++ b/koucha/src/bin/webapi/types.rs @@ -0,0 +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!( 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,