From 4bd3425b1baa68b16d7af36230f5feff9b7f37ab Mon Sep 17 00:00:00 2001 From: MCorange99 Date: Sat, 30 Mar 2024 02:52:46 +0200 Subject: [PATCH] added more db stuff, NOT WORKING --- Cargo.lock | 146 +++++++++++++++++- Cargo.toml | 8 +- migrations/posts/down.sql | 0 migrations/posts/up.sql | 11 ++ migrations/tokens/down.sql | 0 migrations/tokens/up.sql | 7 + .../down.sql | 0 .../{2024-03-25-183335_users => users}/up.sql | 2 +- src/database/models/mod.rs | 11 ++ src/database/models/posts.rs | 135 ++++++++++++++++ src/database/models/tokens.rs | 118 ++++++++++++++ src/database/models/users.rs | 4 +- src/web/mod.rs | 3 +- src/web/routes/api/mod.rs | 14 ++ src/web/routes/api/webhooks/github.rs | 83 ++++++++++ src/web/routes/api/webhooks/mod.rs | 11 ++ src/web/routes/mod.rs | 2 + 17 files changed, 547 insertions(+), 8 deletions(-) create mode 100644 migrations/posts/down.sql create mode 100644 migrations/posts/up.sql create mode 100644 migrations/tokens/down.sql create mode 100644 migrations/tokens/up.sql rename migrations/{2024-03-25-183335_users => users}/down.sql (100%) rename migrations/{2024-03-25-183335_users => users}/up.sql (89%) create mode 100644 src/database/models/posts.rs create mode 100644 src/database/models/tokens.rs create mode 100644 src/web/routes/api/mod.rs create mode 100644 src/web/routes/api/webhooks/github.rs create mode 100644 src/web/routes/api/webhooks/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 00f09f7..4bf5607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -551,6 +566,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + [[package]] name = "byteorder" version = "1.5.0" @@ -594,6 +615,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + [[package]] name = "cipher" version = "0.4.4" @@ -673,6 +709,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1193,6 +1235,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1252,6 +1317,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1820,9 +1894,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -2512,6 +2586,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.53", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "webpki-roots" version = "0.25.4" @@ -2528,15 +2656,18 @@ dependencies = [ "anyhow", "askama", "bcrypt", + "bitflags 2.5.0", "camino", + "chrono", "clap", "crypto", "env_logger", "futures", "log", + "rand", "serde", + "serde_json", "sqlx", - "time", "toml", "uuid", ] @@ -2551,6 +2682,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index d34db43..ee24d10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,13 +24,19 @@ actix-web = "4.5.1" actix-web-lab = "0.20.2" askama = "0.12.1" +#? web::api +chrono = { version = "0.4.37", features = ["serde"] } + #? database sqlx = { version = "0.7.4", features = ["tls-rustls", "runtime-tokio", "postgres", "uuid"] } uuid = { version = "1.8.0", features = ["v4"] } -time = { version = "0.3.34", features = ["macros"] } +# time = { version = "0.3.34", features = ["macros"] } crypto = { version = "0.5.1", features = ["digest", "password-hash"] } bcrypt = "0.15.1" futures = "0.3.30" +bitflags = "2.5.0" +rand = "0.8.5" +serde_json = "1.0.115" [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/migrations/posts/down.sql b/migrations/posts/down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/posts/up.sql b/migrations/posts/up.sql new file mode 100644 index 0000000..fd739b4 --- /dev/null +++ b/migrations/posts/up.sql @@ -0,0 +1,11 @@ + +CREATE TABLE IF NOT EXISTS Posts ( + id UUID NOT NULL UNIQUE, + title TEXT NOT NULL, + descr TEXT NOT NULL, + img_url TEXT NOT NULL, + origin_url TEXT NOT NULL, + original_request JSON NOT NULL, + posted_on TIMESTAMP NOT NULL + PRIMARY KEY (id) +) diff --git a/migrations/tokens/down.sql b/migrations/tokens/down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/tokens/up.sql b/migrations/tokens/up.sql new file mode 100644 index 0000000..2ff88b5 --- /dev/null +++ b/migrations/tokens/up.sql @@ -0,0 +1,7 @@ + +CREATE TABLE IF NOT EXISTS Tokens ( + token TEXT NOT NULL UNIQUE, + owner_id UUID NOT NULL, + permissions BIGINT NOT NULL, + PRIMARY KEY (token) +) diff --git a/migrations/2024-03-25-183335_users/down.sql b/migrations/users/down.sql similarity index 100% rename from migrations/2024-03-25-183335_users/down.sql rename to migrations/users/down.sql diff --git a/migrations/2024-03-25-183335_users/up.sql b/migrations/users/up.sql similarity index 89% rename from migrations/2024-03-25-183335_users/up.sql rename to migrations/users/up.sql index 51b3179..1d69caa 100644 --- a/migrations/2024-03-25-183335_users/up.sql +++ b/migrations/users/up.sql @@ -1,4 +1,4 @@ --- Your SQL goes here + CREATE TABLE IF NOT EXISTS Users ( id UUID NOT NULL UNIQUE, email TEXT NOT NULL, diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 913bd46..9ce89f3 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -1 +1,12 @@ +use bitflags::bitflags; + pub mod users; +pub mod tokens; +pub mod posts; + + +bitflags! { + struct Permissions: i64 { + const MAKE_POST = 1 << 0; + } +} \ No newline at end of file diff --git a/src/database/models/posts.rs b/src/database/models/posts.rs new file mode 100644 index 0000000..039dfa8 --- /dev/null +++ b/src/database/models/posts.rs @@ -0,0 +1,135 @@ +use serde_json::Value; +use uuid::Uuid; +use sqlx::Row; +use crate::database::Database; +use futures::TryStreamExt; + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct Post { + pub id: uuid::Uuid, + pub title: String, + pub descr: String, + pub img_url: String, + pub origin_url: String, + pub original_request: Value, + pub posted_on: i64, +} + +#[allow(dead_code)] +impl Post { + pub async fn create_new(db: &mut Database, title: String, descr: String, img_url: String, origin_url: String, orignal_request: Value) -> anyhow::Result { + let id = Uuid::new_v4(); + let posted_on = chrono::Utc::now().timestamp_millis(); + + + sqlx::query(r#" + INSERT INTO posts ( id, title, descr, img_url, origin_url, original_request, posted_on ) + VALUES ( $1, $2, $3, $4, 0 ) + RETURNING id + "#) + .bind(id) + .bind(title) + .bind(descr) + .bind(img_url) + .bind(origin_url) + .bind(orignal_request) + .bind(posted_on) + .execute(db.connection()) + .await?; + + log::debug!("Created post with id '{id}'"); + + Ok(id) + } + + pub async fn get_by_id(db: &mut Database, id: Uuid) -> anyhow::Result>{ + let mut rows = sqlx::query("SELECT * FROM posts WHERE id = $1") + .bind(id) + .fetch(db.connection()); + + while let Some(row) = rows.try_next().await? { + return Ok(Some(Self { + id: row.try_get("id")?, + title: row.try_get("title")?, + descr: row.try_get("descr")?, + img_url: row.try_get("img_url")?, + origin_url: row.try_get("origin_url")?, + original_request: row.try_get("original_request")?, + posted_on: row.try_get("posted_on")?, + })); + } + + Ok(None) + } + + pub async fn get_last_n(db: &mut Database, n: usize) -> anyhow::Result>{ + let mut rows = sqlx::query("SELECT * FROM posts") + .fetch(db.connection()); + + let mut posts = Vec::new(); + + while let Some(row) = rows.try_next().await? { + let post = Self { + id: row.try_get("id")?, + title: row.try_get("title")?, + descr: row.try_get("descr")?, + img_url: row.try_get("img_url")?, + origin_url: row.try_get("origin_url")?, + original_request: row.try_get("original_request")?, + posted_on: row.try_get("posted_on")?, + }; + posts.push(post); + } + + posts.sort_unstable_by(|a, b| { + b.posted_on.cmp(&a.posted_on) + }); + + let posts = if posts.len() < n { + posts.to_vec() + } else { + posts[..n].to_vec() + }; + + + Ok(posts) + } + + pub async fn save(&self, db: &mut Database) -> anyhow::Result<()> { + let _ = sqlx::query(r#" + UPDATE posts + SET id = $1 + SET title = $2 + SET descr = $3 + SET img_url = $4 + SET origin_url = $5 + SET original_request = $6 + SET posted_on = $7 + + "#) + .bind(&self.id) + .bind(&self.title) + .bind(&self.descr) + .bind(&self.img_url) + .bind(&self.origin_url) + .bind(&self.original_request) + .bind(&self.posted_on) + .execute(db.connection()).await?; + + Ok(()) + } + + pub async fn remove(&self, db: &mut Database) -> anyhow::Result<()> { + sqlx::query(r#" + DELETE FROM posts + WHERE id = $1 + "#) + .bind(self.id) + .execute(db.connection()) + .await?; + + log::debug!("Deleted post with id '{}'", self.id); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/database/models/tokens.rs b/src/database/models/tokens.rs new file mode 100644 index 0000000..0246576 --- /dev/null +++ b/src/database/models/tokens.rs @@ -0,0 +1,118 @@ +use rand::Rng; +use uuid::Uuid; +use sqlx::Row; +use crate::database::Database; +use futures::TryStreamExt; + +use super::Permissions; + +#[derive(sqlx::FromRow)] +pub struct Token { + pub token: String, + pub owner_id: Uuid, + pub permissions: Permissions, +} + +#[allow(dead_code)] +impl Token { + pub async fn create_new(db: &mut Database, owner_id: Uuid, permissions: Permissions) -> anyhow::Result { + + let token = generate_token(32); + + sqlx::query(r#" + INSERT INTO tokens ( token, owner_id, permissions ) + VALUES ( $1, $2, $3 ) + "#) + .bind(&token) + .bind(owner_id) + .bind(permissions.bits()) + .execute(db.connection()) + .await?; + + log::debug!("Created token '{token}'"); + + Ok(Self { + token, + owner_id, + permissions, + }) + } + + pub async fn get_by_token(db: &mut Database, token: String) -> anyhow::Result>{ + let mut rows = sqlx::query("SELECT * FROM tokens WHERE token = $1") + .bind(token) + .fetch(db.connection()); + + while let Some(row) = rows.try_next().await? { + return Ok(Some(Self { + token: row.try_get("token")?, + owner_id: row.try_get("owner_id")?, + permissions: Permissions::from_bits(row.try_get("permissions")?).unwrap(), + })); + } + + Ok(None) + } + + pub async fn get_by_owner_id(db: &mut Database, owner_id: String) -> anyhow::Result>{ + let mut rows = sqlx::query("SELECT * FROM tokens WHERE owner_id = $1") + .bind(owner_id) + .fetch(db.connection()); + + while let Some(row) = rows.try_next().await? { + return Ok(Some(Self { + token: row.try_get("token")?, + owner_id: row.try_get("owner_id")?, + permissions: Permissions::from_bits(row.try_get("permissions")?).unwrap(), + })); + } + + Ok(None) + } + + + pub async fn save(&self, db: &mut Database) -> anyhow::Result<()> { + let _ = sqlx::query(r#" + UPDATE users + SET token = $1 + SET owner_id = $2 + SET permissions = $5 + "#) + .bind(&self.token) + .bind(&self.owner_id) + .bind(&self.permissions.bits()) + .execute(db.connection()).await?; + + Ok(()) + } + + pub async fn remove(&self, db: &mut Database) -> anyhow::Result<()> { + sqlx::query(r#" + DELETE FROM tokens + WHERE token = $1 + "#) + .bind(&self.token) + .execute(db.connection()) + .await?; + + log::debug!("Deleted token '{}'", self.token); + + Ok(()) + } +} + + + + +const TOKEN_CHARSET: &'static [char] = &['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + +fn generate_token(len: u8) -> String { + let mut token = String::new(); + + for _ in 0..len { + let rand = rand::thread_rng().gen_range(0..TOKEN_CHARSET.len()); + token.push(TOKEN_CHARSET[rand]) + } + + token +} \ No newline at end of file diff --git a/src/database/models/users.rs b/src/database/models/users.rs index 1744111..f68b9de 100644 --- a/src/database/models/users.rs +++ b/src/database/models/users.rs @@ -4,7 +4,7 @@ use crate::database::Database; use futures::TryStreamExt; #[derive(sqlx::FromRow)] -pub struct Users { +pub struct User { pub id: uuid::Uuid, pub email: String, pub username: String, @@ -13,7 +13,7 @@ pub struct Users { } #[allow(dead_code)] -impl Users { +impl User { pub async fn create_new(db: &mut Database, email: String, username: String, password: String) -> anyhow::Result { let id = Uuid::new_v4(); let hash = bcrypt::hash(password, 15)?; diff --git a/src/web/mod.rs b/src/web/mod.rs index dbf6573..f46b85a 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -4,7 +4,7 @@ mod templates; use std::sync::Mutex; -use actix_web::{web, App, HttpServer}; +use actix_web::{web, App, HttpServer, Route}; use actix_files as actix_fs; use crate::{config::definition::Config, database::Database}; @@ -17,6 +17,7 @@ pub(crate) async fn start_actix(config: &Config, database: Database) -> anyhow:: App::new() .app_data(actix_web::web::Data::new(Mutex::new(database.clone()))) .route("/", web::get().to(routes::index)) // index.html + .service(routes::api::get_scope()) .service(actix_fs::Files::new("/static", "./static").index_file("index.html")) // static directory .service(web::redirect("/favicon.ico", "/static/favicon.ico")) //? special redirect for favicon }) diff --git a/src/web/routes/api/mod.rs b/src/web/routes/api/mod.rs new file mode 100644 index 0000000..a52b6d9 --- /dev/null +++ b/src/web/routes/api/mod.rs @@ -0,0 +1,14 @@ +mod webhooks; + +use actix_web::{web, Route, Scope}; + + + + +pub fn get_scope() -> Scope { + Scope::new("/api") + .service( + webhooks::get_scope() + ) + +} \ No newline at end of file diff --git a/src/web/routes/api/webhooks/github.rs b/src/web/routes/api/webhooks/github.rs new file mode 100644 index 0000000..af8d126 --- /dev/null +++ b/src/web/routes/api/webhooks/github.rs @@ -0,0 +1,83 @@ +use std::{borrow::BorrowMut, sync::Mutex}; + +use actix_web::{http::header, web::{self, Data}, HttpRequest, HttpResponse, HttpResponseBuilder, Responder, Result, Scope}; +use serde_json::Value; + +use crate::database::{models::{self, tokens::Token}, Database}; + +pub fn get_scope() -> Scope { + Scope::new("/github") + .service( + web::resource("/push") + .to(handler) + ) +} + +pub async fn handler(req: HttpRequest, body: web::Json, db: Data>) -> Result { + let Some(auth) = req.headers().get(header::AUTHORIZATION) else { + return Ok(HttpResponse::Unauthorized()); + }; + + let Ok(token) = auth.to_str() else { + return Ok(HttpResponse::Unauthorized()); + }; + + let token = models::tokens::Token::get_by_token( + db.lock().unwrap().borrow_mut(), + token.to_string() + ).await; + + let Ok(token) = token else { + return Ok(HttpResponse::Unauthorized()); + }; + + let Some(token) = token else { + return Ok(HttpResponse::Unauthorized()); + }; + + let Some(event_type) = req.headers().get("X-GitHub-Event") else { + return Ok(HttpResponse::BadRequest()); + }; + + let Ok(event_type) = event_type.to_str() else { + return Ok(HttpResponse::BadRequest()); + }; + + match event_type { + "release" => { + release_handler(db, token, body).await + } + + _ => Ok(HttpResponse::Ok()) + } + + + +} + + + +pub async fn release_handler(db: Data>, token: Token, body: web::Json) -> Result { + let Some(release) = body.get("release") else { + return Ok(HttpResponse::BadRequest()); + }; + + let Some(origin_url) = release.get("repository") else { + return Ok(HttpResponse::BadRequest()); + }; + + + models::posts::Post::create_new( + db.lock().unwrap().borrow_mut(), + title, + descr, + img_url, + origin_url, + orignal_request + ); + + + + + Ok(HttpResponse::Ok()) +} \ No newline at end of file diff --git a/src/web/routes/api/webhooks/mod.rs b/src/web/routes/api/webhooks/mod.rs new file mode 100644 index 0000000..d71254b --- /dev/null +++ b/src/web/routes/api/webhooks/mod.rs @@ -0,0 +1,11 @@ +use actix_web::{web, Scope}; + +mod github; + + +pub fn get_scope() -> Scope { + Scope::new("/wh") + .service( + github::get_scope() + ) +} \ No newline at end of file diff --git a/src/web/routes/mod.rs b/src/web/routes/mod.rs index ff69700..5e338ea 100644 --- a/src/web/routes/mod.rs +++ b/src/web/routes/mod.rs @@ -1,3 +1,5 @@ +pub mod api; + use std::sync::Mutex; use actix_web_lab::respond::Html;