diff --git a/migrations/20221220010013_librepages_gitea_webhooks.sql b/migrations/20221220010013_librepages_gitea_webhooks.sql new file mode 100644 index 0000000..655f505 --- /dev/null +++ b/migrations/20221220010013_librepages_gitea_webhooks.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS librepages_gitea_webhooks ( + gitea_webhook_secret VARCHAR(40) NOT NULL UNIQUE, + gitea_url VARCHAR(3000) NOT NULL, + auth_token VARCHAR(40) NOT NULL UNIQUE, + ID SERIAL PRIMARY KEY NOT NULL, + owned_by INTEGER NOT NULL references librepages_users(ID) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX librepages_gitea_webhook_auth_token_index ON librepages_gitea_webhooks(auth_token); + +CREATE TABLE IF NOT EXISTS librepages_gitea_webhook_site_mapping ( + site_id INTEGER NOT NULL references librepages_sites(ID) ON DELETE CASCADE, + gitea_webhook_id INTEGER NOT NULL references librepages_gitea_webhooks(ID) ON DELETE CASCADE, + UNIQUE(site_id, gitea_webhook_id) +); diff --git a/sqlx-data.json b/sqlx-data.json index f3ff096..c4e2b66 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -370,6 +370,38 @@ }, "query": "SELECT\n time,\n pub_id\n FROM\n librepages_site_deploy_events\n WHERE\n site = (SELECT ID FROM librepages_sites WHERE hostname = $1)\n AND\n event_type = (SELECT ID FROM librepages_deploy_event_type WHERE name = $2)\n AND\n time = (\n SELECT MAX(time) \n FROM\n librepages_site_deploy_events\n WHERE\n site = (SELECT ID FROM librepages_sites WHERE hostname = $1)\n )\n " }, + "7d2b7a4a57b9b031d15db57116807355e9e03b7bf9b0cff0958bfebe4bc1d1be": { + "describe": { + "columns": [ + { + "name": "gitea_url", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "auth_token", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "gitea_webhook_secret", + "ordinal": 2, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT gitea_url, auth_token, gitea_webhook_secret\n FROM librepages_gitea_webhooks\n WHERE auth_token = $1\n " + }, "8735b654bc261571e6a5908d55a8217474c76bdff7f3cbcc71500a0fe13249db": { "describe": { "columns": [ @@ -466,6 +498,21 @@ }, "query": "SELECT EXISTS (SELECT 1 from librepages_users WHERE name = $1)" }, + "cd2347774ea740deae59e031a398adfc0b5c2942e556faa676fbd161297a81a6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Text" + ] + } + }, + "query": "INSERT INTO librepages_gitea_webhooks\n (gitea_url , auth_token, gitea_webhook_secret, owned_by) VALUES ($1, $2, $3, \n (SELECT ID FROM librepages_users WHERE name = $4)\n )" + }, "ced69a08729ffb906e8971dbdce6a8d4197bc9bb8ccd7c58b3a88eb7be73fc2e": { "describe": { "columns": [ @@ -563,5 +610,18 @@ } }, "query": "INSERT INTO librepages_users\n (name , password, email) VALUES ($1, $2, $3)" + }, + "fdcad0cd77ae37ed74cc7c92f20f9d95d851af5f9c9e6e4c34c39abf4a1d0f14": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "INSERT INTO librepages_gitea_webhook_site_mapping\n (site_id, gitea_webhook_id) VALUES (\n (SELECT ID FROM librepages_sites WHERE repo_url = $1),\n (SELECT ID FROM librepages_gitea_webhooks WHERE auth_token = $2)\n ) ON CONFLICT (site_id, gitea_webhook_id) DO NOTHING;" } } \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index d09d34e..7bf8dac 100644 --- a/src/db.rs +++ b/src/db.rs @@ -23,9 +23,11 @@ use sqlx::types::time::OffsetDateTime; use sqlx::ConnectOptions; use sqlx::PgPool; use tracing::error; +use url::Url; use uuid::Uuid; use crate::errors::*; +use crate::utils; /// Connect to databse pub enum ConnectionOptions { @@ -622,7 +624,73 @@ impl Database { } Ok(events) } + + /// register a new webhook + pub async fn new_webhook(&self, gitea_url: Url, owner: &str) -> ServiceResult { + let hook = GiteaWebhook::new(gitea_url); + sqlx::query!( + "INSERT INTO librepages_gitea_webhooks + (gitea_url , auth_token, gitea_webhook_secret, owned_by) VALUES ($1, $2, $3, + (SELECT ID FROM librepages_users WHERE name = $4) + )", + hook.gitea_url.as_str(), + &hook.auth_token, + &hook.gitea_webhook_secret, + owner, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + Ok(hook) + } + + pub async fn get_wenhook(&self, auth_token: &str) -> ServiceResult { + struct H { + gitea_url: String, + auth_token: String, + gitea_webhook_secret: String, + } + + let h = sqlx::query_as!( + H, + "SELECT gitea_url, auth_token, gitea_webhook_secret + FROM librepages_gitea_webhooks + WHERE auth_token = $1 + ", + auth_token, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::WebhookNotFound))?; + + let h = GiteaWebhook { + gitea_url: Url::parse(&h.gitea_url).unwrap(), + auth_token: h.auth_token, + gitea_webhook_secret: h.gitea_webhook_secret, + }; + + Ok(h) + } + + + /// register a webhook against a site + pub async fn webhoo_link_site(&self, auth_token: &str, repo_url: &Url) -> ServiceResult<()> { + sqlx::query!( + "INSERT INTO librepages_gitea_webhook_site_mapping + (site_id, gitea_webhook_id) VALUES ( + (SELECT ID FROM librepages_sites WHERE repo_url = $1), + (SELECT ID FROM librepages_gitea_webhooks WHERE auth_token = $2) + ) ON CONFLICT (site_id, gitea_webhook_id) DO NOTHING;", + repo_url.as_str(), + auth_token + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::WebhookNotFound))?; + Ok(()) + } } + struct InnerSite { site_secret: String, repo_url: String, @@ -732,6 +800,23 @@ pub struct LibrePagesEvent { pub id: Uuid, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GiteaWebhook { + pub gitea_url: Url, + pub gitea_webhook_secret: String, + pub auth_token: String, +} + +impl GiteaWebhook { + fn new(gitea_url: Url) -> Self { + Self { + gitea_url, + gitea_webhook_secret: utils::get_random(40), + auth_token: utils::get_random(40), + } + } +} + fn now_unix_time_stamp() -> OffsetDateTime { OffsetDateTime::now_utc() } @@ -1023,6 +1108,18 @@ mod tests { ); assert_eq!(latest_update_event_id_from_db.id, latest_update_event_id); + // add webhook + let gitea_url = Url::parse("https://example.org").unwrap(); + let hook = db.new_webhook(gitea_url, NAME).await.unwrap(); + assert_eq!(hook, db.get_wenhook(&hook.auth_token).await.unwrap()); + assert_eq!( + db.get_wenhook(&hook.gitea_webhook_secret).await.err(), + Some(ServiceError::WebhookNotFound) + ); + + db.webhoo_link_site(&hook.auth_token, &Url::parse(&site.repo_url).unwrap()).await.unwrap(); + db.webhoo_link_site(&hook.auth_token, &Url::parse(&site.repo_url).unwrap()).await.unwrap(); + // delete site db.delete_site(p.username, &site.hostname).await.unwrap(); diff --git a/src/errors.rs b/src/errors.rs index 450fefa..fbebab6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -178,6 +178,10 @@ pub enum ServiceError { #[display(fmt = "Passwords don't match")] /// passwords don't match PasswordsDontMatch, + + /// Webhook not found + #[display(fmt = "Webhook not found")] + WebhookNotFound, } impl From for ServiceError { @@ -252,6 +256,7 @@ impl ResponseError for ServiceError { ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN, //FORBIDDEN, ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, //BADREQUEST, ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, //UNAUTHORIZED, + ServiceError::WebhookNotFound => StatusCode::NOT_FOUND, //NOT FOUND, } } }