Compare commits
5 commits
bab52687c7
...
8a62724334
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a62724334 | |||
| b089f62bcd | |||
| 949a984d0c | |||
| 92763fd7dc | |||
| 1400b2fc95 |
12 changed files with 373 additions and 1 deletions
92
koucha/Cargo.lock
generated
92
koucha/Cargo.lock
generated
|
|
@ -73,6 +73,70 @@ dependencies = [
|
||||||
"fs_extra",
|
"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]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -726,6 +790,12 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
@ -740,6 +810,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
|
|
@ -999,9 +1070,12 @@ dependencies = [
|
||||||
name = "koucha"
|
name = "koucha"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rss",
|
"rss",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
@ -1076,6 +1150,12 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
|
@ -1708,6 +1788,17 @@ dependencies = [
|
||||||
"zmij",
|
"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]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
@ -2221,6 +2312,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,6 @@ rss = "2.0.12"
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite" ] }
|
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
chrono = "0.4.43"
|
chrono = "0.4.43"
|
||||||
|
axum = { version= "0.8.8", features = [ "macros" ] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
|
|
||||||
43
koucha/src/bin/webapi/main.rs
Normal file
43
koucha/src/bin/webapi/main.rs
Normal file
|
|
@ -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<UserKey, DbUserKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<DbUserKey> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
41
koucha/src/bin/webapi/routes/create_session.rs
Normal file
41
koucha/src/bin/webapi/routes/create_session.rs
Normal file
|
|
@ -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<AppState>,
|
||||||
|
Json(body): Json<Input>,
|
||||||
|
) -> ApiResult<Output> {
|
||||||
|
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 }))
|
||||||
|
}
|
||||||
37
koucha/src/bin/webapi/routes/get_feeds.rs
Normal file
37
koucha/src/bin/webapi/routes/get_feeds.rs
Normal file
|
|
@ -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<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(_body): Json<Input>,
|
||||||
|
) -> ApiResult<Output> {
|
||||||
|
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<User> = dbusers.iter().map(|u| User {
|
||||||
|
name: u.name().to_string(),
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(ApiResponse(StatusCode::OK, Output { users }))
|
||||||
|
}
|
||||||
36
koucha/src/bin/webapi/routes/get_users.rs
Normal file
36
koucha/src/bin/webapi/routes/get_users.rs
Normal file
|
|
@ -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<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(_body): Json<Input>,
|
||||||
|
) -> ApiResult<Output> {
|
||||||
|
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<User> = dbusers.iter().map(|u| User {
|
||||||
|
name: u.name().to_string(),
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(ApiResponse(StatusCode::OK, Output { users }))
|
||||||
|
}
|
||||||
49
koucha/src/bin/webapi/routes/mod.rs
Normal file
49
koucha/src/bin/webapi/routes/mod.rs
Normal file
|
|
@ -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<T> = Result<ApiResponse<T>, ApiError>;
|
||||||
|
|
||||||
|
pub struct ApiResponse<T>(pub StatusCode, pub T);
|
||||||
|
|
||||||
|
pub struct ApiError {
|
||||||
|
pub status: StatusCode,
|
||||||
|
pub error: &'static str,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
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<T: Serialize> IntoResponse for ApiResponse<T> {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(self.0, Json(self.1)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
40
koucha/src/bin/webapi/routes/new_user.rs
Normal file
40
koucha/src/bin/webapi/routes/new_user.rs
Normal file
|
|
@ -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<AppState>,
|
||||||
|
Json(body): Json<Input>,
|
||||||
|
) -> ApiResult<Output> {
|
||||||
|
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 }))
|
||||||
|
}
|
||||||
17
koucha/src/bin/webapi/types.rs
Normal file
17
koucha/src/bin/webapi/types.rs
Normal file
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,17 @@ impl User {
|
||||||
pub fn key(&self) -> UserKey { self.key }
|
pub fn key(&self) -> UserKey { self.key }
|
||||||
pub fn name(&self) -> &str { &self.name }
|
pub fn name(&self) -> &str { &self.name }
|
||||||
|
|
||||||
|
pub async fn temporary_auth(
|
||||||
|
pool: &AdapterPool,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Self> {
|
||||||
|
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<Self> {
|
pub async fn get(pool: &AdapterPool, key: UserKey) -> Result<Self> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
UnparsedUser,
|
UnparsedUser,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod fetch;
|
pub mod fetch;
|
||||||
|
|
@ -9,7 +9,9 @@ pub mod score;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_utils;
|
pub mod test_utils;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct AdapterPool(sqlx::SqlitePool);
|
pub struct AdapterPool(sqlx::SqlitePool);
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct AdapterClient(reqwest::Client);
|
pub struct AdapterClient(reqwest::Client);
|
||||||
|
|
||||||
pub struct AdapterBuilder {
|
pub struct AdapterBuilder {
|
||||||
|
|
@ -38,6 +40,7 @@ impl AdapterBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Adapter {
|
pub struct Adapter {
|
||||||
db: AdapterPool,
|
db: AdapterPool,
|
||||||
client: AdapterClient,
|
client: AdapterClient,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue