/* * Copyright (C) 2022 Aravinth Manivannan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::str::FromStr; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::types::time::OffsetDateTime; use sqlx::ConnectOptions; use sqlx::PgPool; use tracing::error; use uuid::Uuid; use crate::errors::*; /// Connect to databse pub enum ConnectionOptions { /// fresh connection Fresh(Fresh), /// existing connection Existing(Conn), } /// Use an existing database pool pub struct Conn(pub PgPool); pub struct Fresh { pub pool_options: PgPoolOptions, pub disable_logging: bool, pub url: String, } impl ConnectionOptions { async fn connect(self) -> ServiceResult { let pool = match self { Self::Fresh(fresh) => { let mut connect_options = sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap(); if fresh.disable_logging { connect_options.disable_statement_logging(); } sqlx::postgres::PgConnectOptions::from_str(&fresh.url) .unwrap() .disable_statement_logging(); fresh .pool_options .connect_with(connect_options) .await .unwrap() //.map_err(|e| ServiceError::ServiceError(Box::new(e)))? } Self::Existing(conn) => conn.0, }; Ok(Database { pool }) } } #[derive(Clone)] pub struct Database { pub pool: PgPool, } impl Database { pub async fn migrate(&self) -> ServiceResult<()> { sqlx::migrate!("./migrations/") .run(&self.pool) .await .unwrap(); //.map_err(|e| ServiceError::ServiceError(Box::new(e)))?; self.create_event_type().await?; Ok(()) } pub async fn ping(&self) -> bool { use sqlx::Connection; if let Ok(mut con) = self.pool.acquire().await { con.ping().await.is_ok() } else { false } } /// register a new user pub async fn register(&self, p: &Register<'_>) -> ServiceResult<()> { sqlx::query!( "INSERT INTO librepages_users (name , password, email) VALUES ($1, $2, $3)", &p.username, &p.hash, &p.email, ) .execute(&self.pool) .await .map_err(map_register_err)?; Ok(()) } /// delete a user pub async fn delete_user(&self, username: &str) -> ServiceResult<()> { sqlx::query!("DELETE FROM librepages_users WHERE name = ($1)", username) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(()) } /// check if username exists pub async fn username_exists(&self, username: &str) -> ServiceResult { let res = sqlx::query!( "SELECT EXISTS (SELECT 1 from librepages_users WHERE name = $1)", username, ) .fetch_one(&self.pool) .await .map_err(map_register_err)?; let mut resp = false; if let Some(x) = res.exists { resp = x; } Ok(resp) } /// get user email pub async fn get_email(&self, username: &str) -> ServiceResult { struct Email { email: String, } let res = sqlx::query_as!( Email, "SELECT email FROM librepages_users WHERE name = $1", username ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(res.email) } /// check if email exists pub async fn email_exists(&self, email: &str) -> ServiceResult { let res = sqlx::query!( "SELECT EXISTS (SELECT 1 from librepages_users WHERE email = $1)", email ) .fetch_one(&self.pool) .await .map_err(map_register_err)?; let mut resp = false; if let Some(x) = res.exists { resp = x; } Ok(resp) } /// update a user's email pub async fn update_email(&self, p: &UpdateEmail<'_>) -> ServiceResult<()> { sqlx::query!( "UPDATE librepages_users set email = $1 WHERE name = $2", &p.new_email, &p.username, ) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(()) } /// get a user's password pub async fn get_password(&self, l: &Login<'_>) -> ServiceResult { struct Password { name: String, password: String, } let rec = match l { Login::Username(u) => sqlx::query_as!( Password, r#"SELECT name, password FROM librepages_users WHERE name = ($1)"#, u, ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, Login::Email(e) => sqlx::query_as!( Password, r#"SELECT name, password FROM librepages_users WHERE email = ($1)"#, e, ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, }; let res = NameHash { hash: rec.password, username: rec.name, }; Ok(res) } /// update user's password pub async fn update_password(&self, p: &NameHash) -> ServiceResult<()> { sqlx::query!( "UPDATE librepages_users set password = $1 WHERE name = $2", &p.hash, &p.username, ) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(()) } /// update username pub async fn update_username(&self, current: &str, new: &str) -> ServiceResult<()> { sqlx::query!( "UPDATE librepages_users set name = $1 WHERE name = $2", new, current, ) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(()) } pub async fn add_site(&self, msg: &Site) -> ServiceResult<()> { sqlx::query!( " INSERT INTO librepages_sites (site_secret, repo_url, branch, hostname, pub_id, owned_by) VALUES ($1, $2, $3, $4, $5, ( SELECT ID FROM librepages_users WHERE name = $6 )); ", msg.site_secret, msg.repo_url, msg.branch, msg.hostname, msg.pub_id, msg.owner, ) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; Ok(()) } pub async fn get_site_from_pub_id(&self, pub_id: Uuid, owner: String) -> ServiceResult { struct S { repo_url: String, branch: String, hostname: String, owned_by: i32, site_secret: String, } let site = sqlx::query_as!( S, "SELECT repo_url, branch, hostname, owned_by, site_secret FROM librepages_sites WHERE pub_id = $1 AND owned_by = (SELECT ID from librepages_users WHERE name = $2) AND deleted = false; ", &pub_id, &owner, ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; let site = Site { site_secret: site.site_secret, branch: site.branch, hostname: site.hostname, owner, repo_url: site.repo_url, pub_id, }; Ok(site) } pub async fn get_site_from_secret(&self, site_secret: &str) -> ServiceResult { struct S { repo_url: String, branch: String, hostname: String, owned_by: i32, pub_id: Uuid, } let site = sqlx::query_as!( S, "SELECT repo_url, branch, hostname, owned_by, pub_id FROM librepages_sites WHERE site_secret = $1 AND deleted = false; ", site_secret, ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; struct Owner { name: String, } let owner = sqlx::query_as!( Owner, "SELECT name FROM librepages_users WHERE ID = $1", site.owned_by ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; let site = Site { site_secret: site_secret.to_owned(), branch: site.branch, hostname: site.hostname, owner: owner.name, repo_url: site.repo_url, pub_id: site.pub_id, }; Ok(site) } pub async fn get_site(&self, owner: &str, hostname: &str) -> ServiceResult { let site = sqlx::query_as!( InnerSite, "SELECT site_secret, repo_url, branch, hostname, pub_id FROM librepages_sites WHERE deleted = false AND owned_by = (SELECT ID FROM librepages_users WHERE name = $1 ) AND hostname = $2; ", owner, hostname ) .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; let res = site.to_site(owner.into()); Ok(res) } pub async fn list_all_sites(&self, owner: &str) -> ServiceResult> { let mut sites = sqlx::query_as!( InnerSite, "SELECT site_secret, repo_url, branch, hostname, pub_id FROM librepages_sites WHERE deleted = false AND owned_by = (SELECT ID FROM librepages_users WHERE name = $1 ); ", owner, ) .fetch_all(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; let res = sites.drain(0..).map(|s| s.to_site(owner.into())).collect(); Ok(res) } pub async fn delete_site(&self, owner: &str, hostname: &str) -> ServiceResult<()> { sqlx::query!( "UPDATE librepages_sites SET deleted = true WHERE hostname = ($1) AND owned_by = ( SELECT ID FROM librepages_users WHERE name = $2); ", hostname, owner ) .execute(&self.pool) .await .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; Ok(()) } /// check if hostname exists pub async fn hostname_exists(&self, hostname: &str) -> ServiceResult { let res = sqlx::query!( "SELECT EXISTS (SELECT 1 from librepages_sites WHERE hostname = $1 AND deleted = false)", hostname, ) .fetch_one(&self.pool) .await .map_err(map_register_err)?; let mut resp = false; if let Some(x) = res.exists { resp = x; } 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) ON CONFLICT (name) DO NOTHING;", 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 get_latest_update_event( &self, hostname: &str, ) -> ServiceResult> { self.get_latest_event_of_type(hostname, &EVENT_TYPE_UPDATE) .await } async fn get_latest_event_of_type( &self, hostname: &str, event_type: &Event, ) -> ServiceResult> { struct InnerLibrepagesEventNameless { time: OffsetDateTime, pub_id: Uuid, } let event = sqlx::query_as!( InnerLibrepagesEventNameless, "SELECT time, pub_id FROM librepages_site_deploy_events WHERE site = (SELECT ID FROM librepages_sites WHERE hostname = $1) AND event_type = (SELECT ID FROM librepages_deploy_event_type WHERE name = $2) AND time = ( SELECT MAX(time) FROM librepages_site_deploy_events WHERE site = (SELECT ID FROM librepages_sites WHERE hostname = $1) ) ", hostname, event_type.name ) .fetch_one(&self.pool) .await; match event { Ok(event) => Ok(Some(LibrePagesEvent { id: event.pub_id, time: event.time, event_type: event_type.clone(), site: hostname.to_owned(), })), Err(sqlx::Error::RowNotFound) => Ok(None), Err(e) => Err(map_register_err(e)), } // map_row_not_found_err(e, ServiceError::AccountNotFound))?; } 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, repo_url: String, branch: String, hostname: String, pub_id: Uuid, } impl InnerSite { fn to_site(self, owner: String) -> Site { Site { site_secret: self.site_secret, repo_url: self.repo_url, branch: self.branch, hostname: self.hostname, pub_id: self.pub_id, owner, } } } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// Data required to add a new site pub struct Site { pub site_secret: String, pub repo_url: String, pub pub_id: Uuid, pub branch: String, pub hostname: String, pub owner: String, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// Data required to register a new user pub struct Register<'a> { /// username of new user pub username: &'a str, /// hashed password of new use pub hash: &'a str, /// Optionally, email of new use pub email: &'a str, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// data required to update them email of a user pub struct UpdateEmail<'a> { /// username of the user pub username: &'a str, /// new email address of the user pub new_email: &'a str, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// types of credentials used as identifiers during login pub enum Login<'a> { /// username as login Username(&'a str), /// email as login Email(&'a str), } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] /// type encapsulating username and hashed password of a user pub struct NameHash { /// username pub username: String, /// hashed password pub hash: String, } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] pub struct Event { pub name: String, } impl Event { fn new(name: String) -> Self { Self { name } } pub fn from_str(name: &str) -> Option { (*EVENTS).into_iter().find(|e| e.name == name).cloned() } } lazy_static! { pub static ref EVENT_TYPE_CREATE: Event = Event::new("site.event.create".into()); pub static ref EVENT_TYPE_UPDATE: Event = Event::new("site.event.update".into()); pub static ref EVENT_TYPE_DELETE: Event = Event::new("site.event.delete".into()); pub static ref EVENTS: [&'static Event; 3] = [ &*EVENT_TYPE_CREATE, &*EVENT_TYPE_DELETE, &*EVENT_TYPE_UPDATE ]; } 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() } pub async fn get_db(settings: &crate::settings::Settings) -> Database { let pool_options = PgPoolOptions::new().max_connections(settings.database.pool); ConnectionOptions::Fresh(Fresh { pool_options, url: settings.database.url.clone(), disable_logging: !settings.debug, }) .connect() .await .unwrap() } /// map custom row not found error to DB error pub fn map_row_not_found_err(e: sqlx::Error, row_not_found: ServiceError) -> ServiceError { if let sqlx::Error::RowNotFound = e { row_not_found } else { map_register_err(e) } } /// map postgres errors to [ServiceError](ServiceError) types fn map_register_err(e: sqlx::Error) -> ServiceError { use sqlx::Error; use std::borrow::Cow; if let Error::Database(err) = e { if err.code() == Some(Cow::from("23505")) { let msg = err.message(); println!("{}", msg); if msg.contains("librepages_users_name_key") { ServiceError::UsernameTaken } else if msg.contains("librepages_users_email_key") { ServiceError::EmailTaken } else { error!("{}", msg); ServiceError::InternalServerError } } else { ServiceError::InternalServerError } } else { ServiceError::InternalServerError } } #[cfg(test)] mod tests { use std::collections::HashSet; use super::*; use crate::settings::Settings; #[test] fn event_names_are_unique() { let mut uniq = HashSet::new(); assert!(EVENTS.into_iter().all(move |x| uniq.insert(x.name.clone()))); } #[actix_rt::test] async fn db_works() { let settings = Settings::new().unwrap(); let pool_options = PgPoolOptions::new().max_connections(1); let db = ConnectionOptions::Fresh(Fresh { pool_options, url: settings.database.url.clone(), disable_logging: !settings.debug, }) .connect() .await .unwrap(); assert!(db.ping().await); const EMAIL: &str = "postgresuser@foo.com"; const EMAIL2: &str = "postgresuser2@foo.com"; const NAME: &str = "postgresuser"; const PASSWORD: &str = "pasdfasdfasdfadf"; db.migrate().await.unwrap(); let p = super::Register { username: NAME, email: EMAIL, hash: PASSWORD, }; if db.username_exists(p.username).await.unwrap() { db.delete_user(p.username).await.unwrap(); assert!( !db.username_exists(p.username).await.unwrap(), "user is deleted so username shouldn't exist" ); } db.register(&p).await.unwrap(); assert!(matches!( db.register(&p).await, Err(ServiceError::UsernameTaken) )); // testing get_password // with username let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); assert_eq!(name_hash.hash, p.hash, "user password matches"); assert_eq!(name_hash.username, p.username, "username matches"); // with email let mut name_hash = db.get_password(&Login::Email(p.email)).await.unwrap(); assert_eq!(name_hash.hash, p.hash, "user password matches"); assert_eq!(name_hash.username, p.username, "username matches"); // testing get_email assert_eq!(db.get_email(p.username).await.unwrap(), p.email); // testing email exists assert!( db.email_exists(p.email).await.unwrap(), "user is registered so email should exist" ); assert!( db.username_exists(p.username).await.unwrap(), "user is registered so username should exist" ); // update password test. setting password = username name_hash.hash = name_hash.username.clone(); db.update_password(&name_hash).await.unwrap(); let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); assert_eq!( name_hash.hash, p.username, "user password matches with changed value" ); assert_eq!(name_hash.username, p.username, "username matches"); // update username to p.email assert!( !db.username_exists(p.email).await.unwrap(), "user with p.email doesn't exist. pre-check to update username to p.email" ); db.update_username(p.username, p.email).await.unwrap(); assert!( db.username_exists(p.email).await.unwrap(), "user with p.email exist post-update" ); // testing update email let update_email = UpdateEmail { username: p.username, new_email: EMAIL2, }; db.update_email(&update_email).await.unwrap(); println!( "null user email: {}", db.email_exists(p.email).await.unwrap() ); assert!( db.email_exists(p.email).await.unwrap(), "user was with empty email but email is set; so email should exist" ); // deleting user db.delete_user(p.email).await.unwrap(); assert!( !db.username_exists(p.email).await.unwrap(), "user is deleted so username shouldn't exist" ); } #[actix_rt::test] pub async fn test_db_sites() { let settings = Settings::new().unwrap(); let pool_options = PgPoolOptions::new().max_connections(1); let db = ConnectionOptions::Fresh(Fresh { pool_options, url: settings.database.url.clone(), disable_logging: !settings.debug, }) .connect() .await .unwrap(); assert!(db.ping().await); const EMAIL: &str = "postgresdbsiteuser@foo.com"; const NAME: &str = "postgresdbsiteuser"; 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, hash: PASSWORD, }; if db.username_exists(p.username).await.unwrap() { db.delete_user(p.username).await.unwrap(); assert!( !db.username_exists(p.username).await.unwrap(), "user is deleted so username shouldn't exist" ); } db.register(&p).await.unwrap(); let site = Site { site_secret: "foobar".into(), repo_url: "https://git.batsense.net/LibrePages/librepages.git".into(), branch: "librepages".into(), hostname: "db_works.tests.librepages.librepages.org".into(), pub_id: Uuid::new_v4(), owner: p.username.into(), }; // test if hostname exists. Should be false assert!(!db.hostname_exists(&site.hostname).await.unwrap()); // testing adding site db.add_site(&site).await.unwrap(); // test if hostname exists. Should be true assert!(db.hostname_exists(&site.hostname).await.unwrap()); // get site let db_site = db.get_site(p.username, &site.hostname).await.unwrap(); assert_eq!(db_site, site); // get site by secret assert_eq!( db_site, db.get_site_from_secret(&site.site_secret).await.unwrap() ); // list all sites owned by user let db_sites = db.list_all_sites(p.username).await.unwrap(); 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] ); // when no update event exist, None is returned assert!(db .get_latest_update_event(&site.hostname) .await .unwrap() .is_none()); // add multiple update events, see if latest is returned db.log_event(&site.hostname, &EVENT_TYPE_UPDATE) .await .unwrap(); let latest_update_event_id = db .log_event(&site.hostname, &EVENT_TYPE_UPDATE) .await .unwrap(); let latest_update_event_id_from_db = db .get_latest_update_event(&site.hostname) .await .unwrap() .unwrap(); assert_eq!( latest_update_event_id_from_db.event_type, *EVENT_TYPE_UPDATE ); assert_eq!(latest_update_event_id_from_db.id, latest_update_event_id); // delete site db.delete_site(p.username, &site.hostname).await.unwrap(); // test if hostname exists. Should be false assert!(!db.hostname_exists(&site.hostname).await.unwrap()); } }