diff --git a/Cargo.lock b/Cargo.lock index b321ce0..3a517f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,6 +1620,7 @@ dependencies = [ "tracing-actix-web", "url", "urlencoding", + "uuid 1.2.2", ] [[package]] @@ -2323,6 +2324,7 @@ dependencies = [ "time", "tokio-stream", "url", + "uuid 1.2.2", "webpki-roots", "whoami", ] @@ -2610,7 +2612,7 @@ dependencies = [ "actix-web", "pin-project", "tracing", - "uuid 1.2.1", + "uuid 1.2.2", ] [[package]] @@ -2794,11 +2796,12 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f8e1e97..f181f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ actix-identity = "0.4.0" actix-rt = "2" actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" } argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"} -sqlx = { version = "0.6.1", features = [ "runtime-actix-rustls", "postgres", "time", "offline", "json"] } +sqlx = { version = "0.6.1", features = ["runtime-actix-rustls", "postgres", "time", "offline", "json", "uuid"] } clap = { version = "3.2.20", features = ["derive"]} config = "0.13" @@ -50,6 +50,7 @@ tracing = { version = "0.1.37", features = ["log"]} tracing-actix-web = "0.6.2" toml = "0.5.9" serde_yaml = "0.9.14" +uuid = { version = "1.2.2", features = ["serde"] } [dependencies.cache-buster] git = "https://github.com/realaravinth/cache-buster" diff --git a/migrations/20221115124105_librepages_site_deploy_events.sql b/migrations/20221115124105_librepages_site_deploy_events.sql new file mode 100644 index 0000000..c33acf6 --- /dev/null +++ b/migrations/20221115124105_librepages_site_deploy_events.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS librepages_deploy_event_type ( + name VARCHAR(30) NOT NULL UNIQUE, + ID SERIAL PRIMARY KEY NOT NULL +); + +CREATE UNIQUE INDEX librepages_deploy_event_name_index ON librepages_deploy_event_type(name); + +CREATE TABLE IF NOT EXISTS librepages_site_deploy_events ( + site INTEGER NOT NULL references librepages_sites(ID) ON DELETE CASCADE, + event_type INTEGER NOT NULL references librepages_deploy_event_type(ID), + time timestamptz NOT NULL, + pub_id uuid NOT NULL UNIQUE, + ID SERIAL PRIMARY KEY NOT NULL +); diff --git a/sqlx-data.json b/sqlx-data.json index cd3f69f..d284c65 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -347,6 +347,38 @@ }, "query": "SELECT email FROM librepages_users WHERE name = $1" }, + "e4adf1bc9175eeb9d61b495653bb452039cc38818c8792acdc6a1c732b6f4554": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT EXISTS (SELECT 1 from librepages_deploy_event_type WHERE name = $1)" + }, + "ed935cd75e805ddd7223ea8ba298ff94018cf305c519120279a5d1f7bb99e23e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar" + ] + } + }, + "query": "INSERT INTO librepages_deploy_event_type\n (name) VALUES ($1);" + }, "faa4170a309f19a4abf1ca3f8dd3c0526945aa00f028ebf8bd7063825d448f5b": { "describe": { "columns": [], diff --git a/src/db.rs b/src/db.rs index 41bb8d4..a4ee94e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -19,10 +19,10 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::types::time::OffsetDateTime; -//use sqlx::types::Json; use sqlx::ConnectOptions; use sqlx::PgPool; use tracing::error; +use uuid::Uuid; use crate::errors::*; @@ -81,6 +81,7 @@ impl Database { .await .unwrap(); //.map_err(|e| ServiceError::ServiceError(Box::new(e)))?; + self.create_event_type().await?; Ok(()) } @@ -383,6 +384,139 @@ impl Database { Ok(resp) } + + /// check if event type exists + async fn event_type_exists(&self, event: &Event) -> ServiceResult { + let res = sqlx::query!( + "SELECT EXISTS (SELECT 1 from librepages_deploy_event_type WHERE name = $1)", + event.name, + ) + .fetch_one(&self.pool) + .await + .map_err(map_register_err)?; + + let mut resp = false; + if let Some(x) = res.exists { + resp = x; + } + + Ok(resp) + } + + async fn create_event_type(&self) -> ServiceResult<()> { + for e in EVENTS { + if !self.event_type_exists(&e).await? { + sqlx::query!( + "INSERT INTO librepages_deploy_event_type + (name) VALUES ($1);", + e.name + ) + .execute(&self.pool) + .await + .map_err(map_register_err)?; + } + } + Ok(()) + } + + pub async fn log_event(&self, hostname: &str, event: &Event) -> ServiceResult { + let now = now_unix_time_stamp(); + let uuid = Uuid::new_v4(); + + sqlx::query!( + "INSERT INTO librepages_site_deploy_events + (event_type, time, site, pub_id) VALUES ( + (SELECT iD from librepages_deploy_event_type WHERE name = $1), + $2, + (SELECT ID from librepages_sites WHERE hostname = $3), + $4 + ); + ", + event.name, + &now, + hostname, + uuid, + ) + .execute(&self.pool) + .await + .map_err(map_register_err)?; + Ok(uuid) + } + + pub async fn get_event( + &self, + hostname: &str, + event_id: &Uuid, + ) -> ServiceResult { + let event = sqlx::query_as!( + InnerLibrepagesEvent, + "SELECT + librepages_deploy_event_type.name, + librepages_site_deploy_events.time, + librepages_site_deploy_events.pub_id + FROM + librepages_site_deploy_events + INNER JOIN librepages_deploy_event_type ON + librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type + WHERE + librepages_site_deploy_events.site = ( + SELECT ID FROM librepages_sites WHERE hostname = $1 + ) + AND + librepages_site_deploy_events.pub_id = $2 + ", + hostname, + event_id, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(LibrePagesEvent { + id: event.pub_id, + time: event.time, + event_type: Event::from_str(&event.name).unwrap(), + site: hostname.to_owned(), + }) + } + + pub async fn list_all_site_events( + &self, + hostname: &str, + ) -> ServiceResult> { + let mut inner_events = sqlx::query_as!( + InnerLibrepagesEvent, + "SELECT + librepages_deploy_event_type.name, + librepages_site_deploy_events.time, + librepages_site_deploy_events.pub_id + FROM + librepages_site_deploy_events + INNER JOIN librepages_deploy_event_type ON + librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type + WHERE + librepages_site_deploy_events.site = ( + SELECT ID FROM librepages_sites WHERE hostname = $1 + ); + ", + hostname, + ) + .fetch_all(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + let mut events = Vec::with_capacity(inner_events.len()); + + for e in inner_events.drain(0..) { + events.push(LibrePagesEvent { + id: e.pub_id, + time: e.time, + event_type: Event::from_str(&e.name).unwrap(), + site: hostname.to_owned(), + }) + } + Ok(events) + } } struct InnerSite { site_secret: String, @@ -451,6 +585,40 @@ pub struct NameHash { pub hash: String, } +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct Event { + pub name: &'static str, +} + +impl Event { + const fn new(name: &'static str) -> Self { + Self { name } + } + + pub fn from_str(name: &str) -> Option { + EVENTS.into_iter().find(|e| e.name == name) + } +} +pub const EVENT_TYPE_CREATE: Event = Event::new("site.event.create"); +pub const EVENT_TYPE_UPDATE: Event = Event::new("site.event.update"); +pub const EVENT_TYPE_DELETE: Event = Event::new("site.event.delete"); + +pub const EVENTS: [Event; 3] = [EVENT_TYPE_DELETE, EVENT_TYPE_DELETE, EVENT_TYPE_CREATE]; + +struct InnerLibrepagesEvent { + name: String, + time: OffsetDateTime, + pub_id: Uuid, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LibrePagesEvent { + pub event_type: Event, + pub time: OffsetDateTime, + pub site: String, + pub id: Uuid, +} + fn now_unix_time_stamp() -> OffsetDateTime { OffsetDateTime::now_utc() } @@ -637,6 +805,13 @@ mod tests { const PASSWORD: &str = "pasdfasdfasdfadf"; db.migrate().await.unwrap(); + + // check if events are created + for e in EVENTS { + println!("Testing event type exists {}", e.name); + assert!(db.event_type_exists(&e).await.unwrap()); + } + let p = super::Register { username: NAME, email: EMAIL, @@ -685,6 +860,21 @@ mod tests { assert_eq!(db_sites.len(), 1); assert_eq!(db_sites, vec![site.clone()]); + // add event to site + let event_id = db + .log_event(&site.hostname, &EVENT_TYPE_CREATE) + .await + .unwrap(); + let event = db.get_event(&site.hostname, &event_id).await.unwrap(); + assert_eq!(event.id, event_id); + assert_eq!(event.event_type, EVENT_TYPE_CREATE); + assert_eq!(event.site, site.hostname); + + assert_eq!( + db.list_all_site_events(&site.hostname).await.unwrap(), + vec![event] + ); + // delete site db.delete_site(p.username, &site.hostname).await.unwrap();