diff --git a/.env b/.env new file mode 100644 index 0000000..bc51ebe --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgresql://mc_test:mc_test@nya-1.mcorangehq.xyz/mc_test" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4d9636b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.showUnlinkedFileNotification": false +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4bf5607..18b9357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1569,6 +1569,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parse_int" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d695b79916a2c08bcff7be7647ab60d1402885265005a6658ffe6d763553c5a" +dependencies = [ + "num-traits", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -2664,6 +2673,7 @@ dependencies = [ "env_logger", "futures", "log", + "parse_int", "rand", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ee24d10..df15401 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ futures = "0.3.30" bitflags = "2.5.0" rand = "0.8.5" serde_json = "1.0.115" +parse_int = "0.6.0" [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/env.sh b/env.sh deleted file mode 100644 index f42304a..0000000 --- a/env.sh +++ /dev/null @@ -1 +0,0 @@ -export DATABASE_URL="postgresql://postgres@localhost/mctest" \ No newline at end of file diff --git a/migrations/20240330145459_users.down.sql b/migrations/20240330145459_users.down.sql new file mode 100644 index 0000000..f67aede --- /dev/null +++ b/migrations/20240330145459_users.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE users; \ No newline at end of file diff --git a/migrations/users/up.sql b/migrations/20240330145459_users.up.sql similarity index 70% rename from migrations/users/up.sql rename to migrations/20240330145459_users.up.sql index 1d69caa..fd1b1f4 100644 --- a/migrations/users/up.sql +++ b/migrations/20240330145459_users.up.sql @@ -1,5 +1,5 @@ - -CREATE TABLE IF NOT EXISTS Users ( +-- Add up migration script here +CREATE TABLE IF NOT EXISTS users ( id UUID NOT NULL UNIQUE, email TEXT NOT NULL, username TEXT NOT NULL, diff --git a/migrations/20240330145503_tokens.down.sql b/migrations/20240330145503_tokens.down.sql new file mode 100644 index 0000000..f19e6ca --- /dev/null +++ b/migrations/20240330145503_tokens.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE tokens; \ No newline at end of file diff --git a/migrations/tokens/up.sql b/migrations/20240330145503_tokens.up.sql similarity index 62% rename from migrations/tokens/up.sql rename to migrations/20240330145503_tokens.up.sql index 2ff88b5..5fe6b70 100644 --- a/migrations/tokens/up.sql +++ b/migrations/20240330145503_tokens.up.sql @@ -1,7 +1,7 @@ - -CREATE TABLE IF NOT EXISTS Tokens ( +-- Add up migration script here +CREATE TABLE IF NOT EXISTS tokens ( token TEXT NOT NULL UNIQUE, owner_id UUID NOT NULL, permissions BIGINT NOT NULL, PRIMARY KEY (token) -) +) \ No newline at end of file diff --git a/migrations/20240330145505_posts.down.sql b/migrations/20240330145505_posts.down.sql new file mode 100644 index 0000000..aca1b5f --- /dev/null +++ b/migrations/20240330145505_posts.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE posts; \ No newline at end of file diff --git a/migrations/posts/up.sql b/migrations/20240330145505_posts.up.sql similarity index 65% rename from migrations/posts/up.sql rename to migrations/20240330145505_posts.up.sql index fd739b4..e2f0ac3 100644 --- a/migrations/posts/up.sql +++ b/migrations/20240330145505_posts.up.sql @@ -1,11 +1,11 @@ - -CREATE TABLE IF NOT EXISTS Posts ( +-- Add up migration script here +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 + posted_on TIMESTAMP NOT NULL, PRIMARY KEY (id) ) diff --git a/migrations/tokens/down.sql b/migrations/tokens/down.sql deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/users/down.sql b/migrations/users/down.sql deleted file mode 100644 index bd91388..0000000 --- a/migrations/users/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE Users \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index ab057dc..25d5bfb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,9 @@ -use clap::Parser; +use std::str::FromStr; +use clap::{Parser, Subcommand}; +use uuid::Uuid; + +use crate::database::{models::Permissions, Database}; #[derive(Debug, Clone, Parser)] #[command(version, about, long_about = None)] @@ -18,4 +22,54 @@ pub struct CliArgs { #[arg(long, short, default_value="./config.toml")] pub config: camino::Utf8PathBuf, -} \ No newline at end of file + + #[command(subcommand)] + pub command: Option +} + +#[derive(Debug, Clone, Subcommand)] +pub enum CliArgsCommand { + #[command(arg_required_else_help = true)] + GenerateToken { + #[arg(long)] + owner_id: String, + #[arg(long)] + permissions: i64, + }, + #[command(arg_required_else_help = true)] + CreateUser { + #[arg(long)] + email: String, + #[arg(long)] + username: String, + #[arg(long)] + password: String + } +} + + +pub async fn handle_command(cli: &CliArgs, db: &mut Database) -> anyhow::Result { + let Some(command) = cli.command.clone() else { + return Ok(false); + }; + + + match command { + CliArgsCommand::GenerateToken { + owner_id, permissions + } => { + let permissions = Permissions::from_bits(permissions).unwrap(); + let owner_id = Uuid::from_str(&owner_id)?; + let token = crate::database::models::tokens::Token::create_new(db, owner_id, permissions).await?; + log::info!("Generated token: {token:#?}"); + Ok(true) + }, + CliArgsCommand::CreateUser { + email, username, password + } => { + let user = crate::database::models::users::User::create_new(db, email, username, password).await?; + log::info!("Created user: {user:?}"); + Ok(true) + }, + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 9bc8a13..d794a36 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -16,17 +16,18 @@ pub struct Database { impl Database { pub async fn new(config: &Config) -> anyhow::Result { - sqlx::migrate!("./migrations"); log::info!("Database connecting to {}", config.database.url); let conn = PgPoolOptions::new() - .max_connections(5) - .connect(&config.database.url).await; + .max_connections(5) + .connect(&config.database.url).await; match conn { Ok(c) => { log::info!("Connection successfull"); + log::info!("Running migrations"); + sqlx::migrate!("./migrations").run(&c).await?; Ok(Self { connection: c }) diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 9ce89f3..4263c01 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -4,9 +4,11 @@ pub mod users; pub mod tokens; pub mod posts; +#[derive(Debug, Clone)] +pub struct Permissions(i64); bitflags! { - struct Permissions: i64 { + impl 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 index 039dfa8..6b89d76 100644 --- a/src/database/models/posts.rs +++ b/src/database/models/posts.rs @@ -11,7 +11,7 @@ pub struct Post { pub descr: String, pub img_url: String, pub origin_url: String, - pub original_request: Value, + pub original_request: String, pub posted_on: i64, } diff --git a/src/database/models/tokens.rs b/src/database/models/tokens.rs index 0246576..4a0f637 100644 --- a/src/database/models/tokens.rs +++ b/src/database/models/tokens.rs @@ -6,7 +6,7 @@ use futures::TryStreamExt; use super::Permissions; -#[derive(sqlx::FromRow)] +#[derive(sqlx::FromRow, Debug)] pub struct Token { pub token: String, pub owner_id: Uuid, diff --git a/src/database/models/users.rs b/src/database/models/users.rs index f68b9de..7f0fda3 100644 --- a/src/database/models/users.rs +++ b/src/database/models/users.rs @@ -19,7 +19,7 @@ impl User { let hash = bcrypt::hash(password, 15)?; sqlx::query(r#" - INSERT INTO users ( id, email, username, pw_hash, permissions ) + INSERT INTO "users" ( id, email, username, pw_hash, permissions ) VALUES ( $1, $2, $3, $4, 0 ) RETURNING id "#) diff --git a/src/main.rs b/src/main.rs index d86530e..9be5c63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod config; mod database; #[actix_web::main] -async fn main() -> std::io::Result<()> { +async fn main() -> anyhow::Result<()> { let cli = cli::CliArgs::parse(); logger::init_logger(&cli); @@ -19,7 +19,12 @@ async fn main() -> std::io::Result<()> { } }; - let Ok(database) = database::Database::new(config.get_ref()).await else {return Ok(())}; + let Ok(mut database) = database::Database::new(config.get_ref()).await else {return Ok(())}; + + if cli::handle_command(&cli, &mut database).await? { + log::info!("Command exectuted, exiting"); + return Ok(()); + } if let Err(e) = web::start_actix(config.get_ref(), database).await { log::error!("Actix had an error: {e}"); diff --git a/src/web/mod.rs b/src/web/mod.rs index f46b85a..034a1cc 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,10 +1,9 @@ - -mod routes; -mod templates; +pub mod routes; +pub mod templates; use std::sync::Mutex; -use actix_web::{web, App, HttpServer, Route}; +use actix_web::{web, App, HttpServer}; use actix_files as actix_fs; use crate::{config::definition::Config, database::Database}; diff --git a/src/web/routes/api/mod.rs b/src/web/routes/api/mod.rs index a52b6d9..eb144fc 100644 --- a/src/web/routes/api/mod.rs +++ b/src/web/routes/api/mod.rs @@ -1,6 +1,6 @@ -mod webhooks; +pub mod webhooks; -use actix_web::{web, Route, Scope}; +use actix_web::Scope; diff --git a/src/web/routes/api/webhooks/github/events/mod.rs b/src/web/routes/api/webhooks/github/events/mod.rs new file mode 100644 index 0000000..4bc1b24 --- /dev/null +++ b/src/web/routes/api/webhooks/github/events/mod.rs @@ -0,0 +1,28 @@ +use std::{borrow::BorrowMut, sync::Mutex}; + +use actix_web::{web::Data, HttpResponse, HttpResponseBuilder, Result}; + +use crate::database::{models::{self, tokens::Token}, Database}; + +use super::types::ReleaseEvent; + +pub async fn release_handler(db: Data>, token: Token, body: ReleaseEvent, raw_body: String,) -> Result { + + let title = format!("{} has been released on {}!", body.release.tag_name, body.repository.full_name); + + dbg!(body); + + // 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/migrations/posts/down.sql b/src/web/routes/api/webhooks/github/events/release.rs similarity index 100% rename from migrations/posts/down.sql rename to src/web/routes/api/webhooks/github/events/release.rs diff --git a/src/web/routes/api/webhooks/github.rs b/src/web/routes/api/webhooks/github/mod.rs similarity index 55% rename from src/web/routes/api/webhooks/github.rs rename to src/web/routes/api/webhooks/github/mod.rs index af8d126..bb5dd1e 100644 --- a/src/web/routes/api/webhooks/github.rs +++ b/src/web/routes/api/webhooks/github/mod.rs @@ -1,9 +1,11 @@ +pub mod types; +pub mod events; + 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 actix_web::{http::header, web::{self, Bytes, Data}, HttpRequest, HttpResponse, Responder, Result, Scope}; -use crate::database::{models::{self, tokens::Token}, Database}; +use crate::database::{models, Database}; pub fn get_scope() -> Scope { Scope::new("/github") @@ -13,7 +15,7 @@ pub fn get_scope() -> Scope { ) } -pub async fn handler(req: HttpRequest, body: web::Json, db: Data>) -> Result { +pub async fn handler(req: HttpRequest, body: Bytes, db: Data>) -> Result { let Some(auth) = req.headers().get(header::AUTHORIZATION) else { return Ok(HttpResponse::Unauthorized()); }; @@ -43,41 +45,17 @@ pub async fn handler(req: HttpRequest, body: web::Json, db: Data { - release_handler(db, token, body).await - } + let Ok(json) = String::from_utf8(body.to_vec()) else { + return Ok(HttpResponse::BadRequest()); + }; + let Ok(event) = types::Event::from_raw_json(event_type, json.clone()) else { + return Ok(HttpResponse::BadRequest()); + }; + + match event { + types::Event::Release(body) => events::release_handler(db, token, body, json).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/github/types.rs b/src/web/routes/api/webhooks/github/types.rs new file mode 100644 index 0000000..2e4d553 --- /dev/null +++ b/src/web/routes/api/webhooks/github/types.rs @@ -0,0 +1,814 @@ +use serde::Deserialize; + +//? Taken from https://github.com/softprops/afterparty/blob/master/src/events.rs.in +//? Examples of payloads in https://github.com/softprops/afterparty/blob/master/data/ + +impl Event { + pub fn from_raw_json(event: &str, json: String) -> anyhow::Result { + let json = format!("{{\"{event}\": {json}}}"); + let json: Self = serde_json::from_str(&json)?; + + Ok(json) + } +} + + +#[derive(Debug, Deserialize, Clone)] +pub struct Value { pub json: serde_json::Value } + +#[derive(Debug, Deserialize, Clone)] +pub enum Event { + CommitComment(CommitCommentEvent), + Create(CreateEvent), + Delete(DeleteEvent), + Deployment(DeploymentEvent), + DeploymentStatus(DeploymentStatusEvent), + Fork(ForkEvent), + Gollum(GollumEvent), + IssueComment(IssueCommentEvent), + Issues(IssuesEvent), + Member(MemberEvent), + Membership(MembershipEvent), + PageBuild(PageBuildEvent), + Ping(PingEvent), + Public(PublicEvent), + PullRequest(PullRequestEvent), + PullRequestReviewComment(PullRequestReviewCommentEvent), + Push(PushEvent), + Release(ReleaseEvent), + Repository(RepositoryEvent), + Status(StatusEvent), + TeamAdd(TeamAddEvent), + Watch(WatchEvent), +} + + +#[derive(Debug, Deserialize, Clone)] +pub struct CommitCommentEvent { + pub action: String, + pub comment: Comment, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct CreateEvent { + pub description: String, + pub master_branch: String, + pub pusher_type: String, + #[serde(rename="ref")] + pub _ref: String, + #[serde(rename="ref_type")] + pub ref_type: String, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DeleteEvent { + pub pusher_type: String, + #[serde(rename="ref")] + pub _ref: String, + pub ref_type: String, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DeploymentEvent { + pub deployment: Deployment, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DeploymentStatusEvent { + pub deployment: Deployment, + pub deployment_status: DeploymentStatus, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ForkEvent { + pub forkee: Repository, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct GollumEvent { + pub pages: Vec, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct IssueCommentEvent { + pub action: String, + pub comment: IssueCommentComment, + pub issue: Issue, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct IssuesEvent { + pub action: String, + pub issue: Issue, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MemberEvent { + pub action: String, + pub member: User, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MembershipEvent { + pub action: String, + pub member: User, + pub organization: Organization, + pub scope: String, + pub sender: User, + pub team: Team, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PageBuildEvent { + pub build: PageBuild, + pub id: u64, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PingEvent { + pub hook: Hook, + pub hook_id: u64, + pub repository: Repository, + pub sender: User, + pub zen: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PublicEvent { + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PullRequestEvent { + pub action: String, + pub number: u64, + pub pull_request: PullRequestDetails, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PullRequestReviewCommentEvent { + pub action: String, + pub comment: PullRequestReviewComment, + pub pull_request: PullRequest, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct PushEvent { + pub after: String, + pub base_ref: Option, + pub before: String, + pub commits: Vec, + pub compare: String, + pub created: bool, + pub deleted: bool, + pub forced: bool, + pub head_commit: CommitStats, + pub pusher: UserRef, // note there aren't may fields here + #[serde(rename="ref")] + pub _ref: String, + pub repository: PushRepository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ReleaseEvent { + pub action: String, + pub release: Release, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RepositoryEvent { + pub action: String, + pub organization: Organization, + pub repository: Repository, + pub sender: User, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct StatusEvent { + //pub branches: Vec, + pub commit: CommitRef, + pub context: String, + pub created_at: String, + pub description: Option, + pub id: u64, + pub name: String, + pub repository: Repository, + pub sender: User, + pub sha: String, + pub state: String, + pub target_url: Option, + pub updated_at: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct TeamAddEvent { + pub organization: Organization, + pub repository: Repository, + pub sender: User, + pub team: Team, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct WatchEvent { + pub action: String, + pub repository: Repository, + pub sender: User, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct Commit { + pub author: GitUser, + pub committer: GitUser, + pub message: String, + pub tree: GitRef, + pub url: String, + pub comment_count: u64 +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct BranchRef { + pub commit: GitRef, + pub name: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct PageBuild { + pub commit: String, + pub created_at: String, + pub duration: u64, + pub error: Error, + pub pusher: User, + pub status: String, + pub updated_at: String, + pub url: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct Comment { + pub body: String, + pub commit_id: String, + pub created_at: String, + pub html_url: String, + pub id: u64, + pub line: Option, + pub path: Option, + pub position: Option, + pub updated_at: String, + pub url: String, + pub user: User, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct CommitRef { + pub author: User, + pub comments_url: String, + pub commit: Commit, + pub committer: User, + pub html_url: String, + pub parents: Vec, + pub sha: String, + pub url: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Deployment { + pub created_at: String, + pub creator: User, + pub description: Option, + pub environment: String, + pub id: u64, + pub payload: Value, + #[serde(rename="ref")] + pub _ref: String, + pub repository_url: String, + pub sha: String, + pub statuses_url: String, + pub task: String, + pub updated_at: String, + pub url: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct DeploymentStatus { + pub created_at: String, + pub creator: User, + pub deployment_url: String, + pub description: Option, + pub id: u64, + pub repository_url: String, + pub state: String, + pub target_url: Option, + pub updated_at: String, + pub url: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct CommitStats { + pub added: Vec, + pub author: GitUser, + pub committer: GitUser, + pub distinct: bool, + pub id: String, + pub message: String, + pub modified: Vec, + pub removed: Vec, + pub timestamp: String, + pub tree_id: String, + pub url: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct Hook { + pub active: bool, + pub config: Config, + pub created_at: String, + pub events: Vec, + pub id: u64, + pub last_response: LastResponse, + pub name: String, + pub ping_url: String, + pub test_url: String, + pub _type: String, + pub updated_at: String, + pub url: String, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct Issue { + pub assignee: Option, + pub body: Option, + pub closed_at: Option, + pub comments: u64, + pub comments_url: String, + pub created_at: String, + pub events_url: String, + pub html_url: String, + pub id: u64, + pub labels: Vec