Compare commits

..

3 Commits

14 changed files with 376 additions and 38 deletions

9
Cargo.lock generated
View File

@ -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]]

View File

@ -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"

View File

@ -18,7 +18,7 @@ use std::process::Command;
fn main() {
let output = Command::new("git")
.args(&["rev-parse", "HEAD"])
.args(["rev-parse", "HEAD"])
.output()
.expect("error in git command, is git installed?");
let git_hash = String::from_utf8(output.stdout).unwrap();

View File

@ -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
);

View File

@ -1,5 +1,20 @@
{
"db": "PostgreSQL",
"14cdc724af64942e93994f97e9eafc8272d15605eff7aab9e5177d01f2bf6118": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz",
"Text",
"Uuid"
]
}
},
"query": "INSERT INTO librepages_site_deploy_events\n (event_type, time, site, pub_id) VALUES (\n (SELECT iD from librepages_deploy_event_type WHERE name = $1),\n $2,\n (SELECT ID from librepages_sites WHERE hostname = $3),\n $4\n );\n "
},
"1ac91b492001493430c686d9cd7d6be03ada4b4c431d7bc112ef2105eba0e82d": {
"describe": {
"columns": [
@ -97,6 +112,39 @@
},
"query": "SELECT EXISTS (SELECT 1 from librepages_sites WHERE hostname = $1)"
},
"39854fcbfb0247377c6c5ca70c2c0fa7804548848bf56f881eea2f8242e7a09d": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "time",
"ordinal": 1,
"type_info": "Timestamptz"
},
{
"name": "pub_id",
"ordinal": 2,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
}
},
"query": "SELECT\n librepages_deploy_event_type.name,\n librepages_site_deploy_events.time,\n librepages_site_deploy_events.pub_id\n FROM\n librepages_site_deploy_events\n INNER JOIN librepages_deploy_event_type ON\n librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type\n WHERE\n librepages_site_deploy_events.site = (\n SELECT ID FROM librepages_sites WHERE hostname = $1\n )\n AND\n librepages_site_deploy_events.pub_id = $2\n "
},
"416b9f0412f0d7ee05d4a350839c5a6d1e06c1d7f8942744f6d806ddc47084c2": {
"describe": {
"columns": [],
@ -347,6 +395,70 @@
},
"query": "SELECT email FROM librepages_users WHERE name = $1"
},
"d2327c1bcb40e18518c2112413a19a9b26eb0f54f83c53e968c9752d70c8dd4e": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "time",
"ordinal": 1,
"type_info": "Timestamptz"
},
{
"name": "pub_id",
"ordinal": 2,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT\n librepages_deploy_event_type.name,\n librepages_site_deploy_events.time,\n librepages_site_deploy_events.pub_id\n FROM\n librepages_site_deploy_events\n INNER JOIN librepages_deploy_event_type ON\n librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type\n WHERE\n librepages_site_deploy_events.site = (\n SELECT ID FROM librepages_sites WHERE hostname = $1\n );\n "
},
"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)"
},
"f651da8f411b7977cb87dd8d4bd5d167661d7ef1d865747e76219453d386d593": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar"
]
}
},
"query": "INSERT INTO librepages_deploy_event_type\n (name) VALUES ($1) ON CONFLICT (name) DO NOTHING;"
},
"faa4170a309f19a4abf1ca3f8dd3c0526945aa00f028ebf8bd7063825d448f5b": {
"describe": {
"columns": [],

View File

@ -17,8 +17,10 @@
use actix_web::web;
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use uuid::Uuid;
use crate::ctx::Ctx;
use crate::db;
use crate::db::Site;
use crate::errors::*;
use crate::page::Page;
@ -55,13 +57,16 @@ impl Ctx {
self.db.add_site(&db_site).await?;
let page = Page::from_site(&self.settings, db_site);
page.update(&page.branch)?;
if let Some(config) = page_config::Config::load(&page.path, &page.branch) {
if let Some(_config) = page_config::Config::load(&page.path, &page.branch) {
unimplemented!();
}
self.db
.log_event(&page.domain, &db::EVENT_TYPE_CREATE)
.await?;
Ok(page)
}
pub async fn update_site(&self, secret: &str, branch: Option<String>) -> ServiceResult<()> {
pub async fn update_site(&self, secret: &str, branch: Option<String>) -> ServiceResult<Uuid> {
if let Ok(db_site) = self.db.get_site_from_secret(secret).await {
let page = Page::from_site(&self.settings, db_site);
let (tx, rx) = oneshot::channel();
@ -78,10 +83,13 @@ impl Ctx {
.unwrap();
}
rx.await.unwrap()?;
if let Some(config) = page_config::Config::load(&page.path, &page.branch) {
if let Some(_config) = page_config::Config::load(&page.path, &page.branch) {
unimplemented!();
}
Ok(())
println!("{}", page.domain);
self.db
.log_event(&page.domain, &db::EVENT_TYPE_UPDATE)
.await
} else {
Err(ServiceError::WebsiteNotFound)
}

201
src/db.rs
View File

@ -19,11 +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 url::quirks::hostname;
use uuid::Uuid;
use crate::errors::*;
@ -82,6 +81,7 @@ impl Database {
.await
.unwrap();
//.map_err(|e| ServiceError::ServiceError(Box::new(e)))?;
self.create_event_type().await?;
Ok(())
}
@ -384,6 +384,139 @@ impl Database {
Ok(resp)
}
/// check if event type exists
async fn event_type_exists(&self, event: &Event) -> ServiceResult<bool> {
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<Uuid> {
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<LibrePagesEvent> {
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<Vec<LibrePagesEvent>> {
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,
@ -452,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<Event> {
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_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()
}
@ -504,9 +671,17 @@ fn map_register_err(e: sqlx::Error) -> ServiceError {
#[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)));
}
#[actix_rt::test]
async fn db_works() {
let settings = Settings::new().unwrap();
@ -638,6 +813,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,
@ -686,6 +868,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();

View File

@ -16,7 +16,7 @@
*/
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use uuid::Uuid;
use crate::errors::*;
use crate::page::Page;
@ -44,13 +44,19 @@ pub struct DeployEvent {
pub branch: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DeployEventResp {
pub id: Uuid,
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.deploy.update")]
#[tracing::instrument(name = "Update webpages", skip(payload, ctx))]
async fn update(payload: web::Json<DeployEvent>, ctx: AppCtx) -> ServiceResult<impl Responder> {
let payload = payload.into_inner();
ctx.update_site(&payload.secret, Some(payload.branch))
let id = ctx
.update_site(&payload.secret, Some(payload.branch))
.await?;
Ok(HttpResponse::Ok())
Ok(HttpResponse::Ok().json(DeployEventResp { id }))
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
@ -132,6 +138,10 @@ mod tests {
)
.await;
check_status!(resp, StatusCode::OK);
let event_id: DeployEventResp = actix_web::test::read_body_json(resp).await;
let update_event = ctx.db.get_event(&page.domain, &event_id.id).await.unwrap();
assert_eq!(&update_event.site, &page.domain);
assert_eq!(update_event.id, event_id.id);
payload.secret = page.branch.clone();

View File

@ -187,12 +187,10 @@ fn read_file_inner(
}
fn file_not_found(e: git2::Error) -> ServiceError {
if e.code() == ErrorCode::NotFound {
if e.class() == ErrorClass::Tree {
return ServiceError::FileNotFound;
}
if e.code() == ErrorCode::NotFound && e.class() == ErrorClass::Tree {
return ServiceError::FileNotFound;
}
return e.into();
e.into()
}
let entry = tree.get_path(Path::new(path)).map_err(file_not_found)?;

View File

@ -102,9 +102,9 @@ impl Config {
branch: &str,
policies: &'a [Policy<'a>],
) -> Option<&'a Policy<'a>> {
let repo = git2::Repository::open(&repo_path).unwrap();
let repo = git2::Repository::open(repo_path).unwrap();
let branch = repo.find_branch(&branch, git2::BranchType::Local).unwrap();
let branch = repo.find_branch(branch, git2::BranchType::Local).unwrap();
// let tree = head.peel_to_tree().unwrap();
let branch = branch.into_reference();
let tree = branch.peel_to_tree().unwrap();
@ -114,10 +114,7 @@ impl Config {
if let Some(name) = x.name() {
if policies.iter().any(|p| p.rel_path == name) {
let mode: GitFileMode = x.into();
match mode {
GitFileMode::Executable | GitFileMode::Regular => true,
_ => false,
}
matches!(mode, GitFileMode::Executable | GitFileMode::Regular)
} else {
false
}
@ -142,7 +139,7 @@ impl Config {
}
fn load_json(c: &str) -> Config {
serde_json::from_str(&c).unwrap()
serde_json::from_str(c).unwrap()
}
}

View File

@ -104,7 +104,7 @@ pub fn context(s: &Settings) -> Context {
ctx
}
pub fn auth_ctx(username: Option<&str>, s: &Settings) -> Context {
pub fn auth_ctx(_username: Option<&str>, s: &Settings) -> Context {
let mut ctx = Context::new();
let footer = Footer::new(s);
ctx.insert("footer", &footer);

View File

@ -38,9 +38,9 @@ impl<'a> Preview<'a> {
}
pub fn extract(&self, hostname: &'a str) -> Option<&'a str> {
if !hostname.contains(&self.delimiter)
|| !hostname.contains(&self.prefix)
|| !hostname.contains(&self.base)
if !hostname.contains(self.delimiter)
|| !hostname.contains(self.prefix)
|| !hostname.contains(self.base)
{
return None;
}

View File

@ -53,8 +53,8 @@ async fn index(req: HttpRequest, ctx: AppCtx) -> ServiceResult<impl Responder> {
if host.contains(&ctx.settings.page.base_domain) {
let extractor = crate::preview::Preview::new(&ctx);
if let Some(preview_branch) = extractor.extract(host) {
let res = if ctx.db.hostname_exists(&host).await? {
let path = crate::utils::get_website_path(&ctx.settings, &host);
let res = if ctx.db.hostname_exists(host).await? {
let path = crate::utils::get_website_path(&ctx.settings, host);
let content =
crate::git::read_preview_file(&path, preview_branch, req.uri().path())?;
let mime = if let Some(mime) = content.mime.first_raw() {
@ -75,7 +75,7 @@ async fn index(req: HttpRequest, ctx: AppCtx) -> ServiceResult<impl Responder> {
// TODO: custom domains.
if ctx.db.hostname_exists(host).await? {
let path = crate::utils::get_website_path(&ctx.settings, &host);
let path = crate::utils::get_website_path(&ctx.settings, host);
let content = crate::git::read_file(&path, req.uri().path())?;
let mime = if let Some(mime) = content.mime.first_raw() {
mime

View File

@ -16,22 +16,20 @@
*/
use std::env;
use std::path::Path;
use std::sync::Arc;
use config::{Config, ConfigError, Environment, File};
use derive_more::Display;
#[cfg(not(test))]
use tracing::{error, warn};
use tracing::warn;
#[cfg(test)]
use std::{println as warn, println as error};
use std::println as warn;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use crate::errors::*;
use crate::page::Page;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
@ -178,7 +176,7 @@ impl Settings {
}
if !path.exists() {
std::fs::create_dir_all(&path).unwrap();
std::fs::create_dir_all(path).unwrap();
}
}