Compare commits
5 commits
main
...
integratio
| 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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
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 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> {
|
||||
sqlx::query_as!(
|
||||
UnparsedUser,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue