diff --git a/Cargo.lock b/Cargo.lock index 00924a1..f450a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,6 +565,7 @@ dependencies = [ "rocket_db_pools", "rocket_dyn_templates", "settings", + "url", "users", ] @@ -2071,6 +2072,7 @@ dependencies = [ "database", "id", "thiserror", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 871b7f8..891ebae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ [workspace.dependencies] thiserror = "1" chrono = "0.4.23" -sqlx = "0.5.13" \ No newline at end of file +sqlx = "0.5.13" +url = "2.3.1" diff --git a/crates/database/migrations/20230306214726_settings_url.down.sql b/crates/database/migrations/20230306214726_settings_url.down.sql new file mode 100644 index 0000000..ef4f150 --- /dev/null +++ b/crates/database/migrations/20230306214726_settings_url.down.sql @@ -0,0 +1,2 @@ +alter table settings + drop column url; diff --git a/crates/database/migrations/20230306214726_settings_url.up.sql b/crates/database/migrations/20230306214726_settings_url.up.sql new file mode 100644 index 0000000..c6a2b9e --- /dev/null +++ b/crates/database/migrations/20230306214726_settings_url.up.sql @@ -0,0 +1,2 @@ +alter table settings + add column url TEXT; diff --git a/crates/database/queries/settings/get.sql b/crates/database/queries/settings/get.sql index 313b33b..f274f11 100644 --- a/crates/database/queries/settings/get.sql +++ b/crates/database/queries/settings/get.sql @@ -2,7 +2,8 @@ select id, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime", business_name, - business_logo + business_logo, + url from settings diff --git a/crates/database/queries/settings/set_url.sql b/crates/database/queries/settings/set_url.sql new file mode 100644 index 0000000..b228350 --- /dev/null +++ b/crates/database/queries/settings/set_url.sql @@ -0,0 +1,5 @@ +update settings + +set url = ? + +where id is 0 diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index e47ef3f..e77720a 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -40,6 +40,64 @@ }, "query": "insert or ignore into settings(id)\nvalues (0);" }, + "64cf880633d3ee5c18f6e7c2a865470442f1ba4b1019806a580ec384329dc32e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "updated_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "business_name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "business_logo", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "url", + "ordinal": 5, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Right": 0 + } + }, + "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n business_name,\n business_logo,\n url\n\nfrom settings\n\nwhere id is 0\n" + }, + "87906834faa6f185aee0e4d893b9754908b1c173e9dce383663d723891a89cd1": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "update settings\n\nset url = ?\n\nwhere id is 0\n" + }, "aae93a39c5a9f46235b5ef871b45ba76d7efa1677bfe8291a62b8cbf9cd9e0d5": { "describe": { "columns": [], @@ -127,47 +185,5 @@ } }, "query": "select u.id,\n u.created_at as \"created_at: DateTime\",\n u.updated_at as \"updated_at: DateTime\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join settings s on u.id = s.first_admin\n\nwhere u.is_admin is 1\n and u.is_archived is 0\n and u.id is s.first_admin\n\nlimit 1" - }, - "cc69514c4d9457462e634eb58cbfc82b454197c5cb7f4a451954eb5a421afc3b": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "created_at: DateTime", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "updated_at: DateTime", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "business_name", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "business_logo", - "ordinal": 4, - "type_info": "Blob" - } - ], - "nullable": [ - false, - false, - false, - true, - true - ], - "parameters": { - "Right": 0 - } - }, - "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n business_name,\n business_logo\n\nfrom settings\n\nwhere id is 0\n" } } \ No newline at end of file diff --git a/crates/database/src/tables/settings.rs b/crates/database/src/tables/settings.rs index b9f6265..d98b647 100644 --- a/crates/database/src/tables/settings.rs +++ b/crates/database/src/tables/settings.rs @@ -10,6 +10,7 @@ pub struct Settings { pub updated_at: DateTime, pub business_name: Option, pub business_logo: Option>, + pub url: Option, } impl Settings { @@ -67,4 +68,13 @@ impl Settings { Ok((query.rows_affected() == 1).then_some(())) } + + pub async fn set_url(conn: impl SqliteExecutor<'_>, url: &str) -> Result, Error> { + let query: SqliteQueryResult = sqlx::query_file!("queries/settings/set_url.sql", url) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } } diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index 89233f1..8377059 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -9,6 +9,7 @@ rocket_db_pools = { version = "0.1.0-rc.2", features = ["sqlx_sqlite"] } rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] } infer = { version = "0.12.0", default-features = false } erased-serde = "0.3" +url = { workspace = true } # local crates database_pool = { path = "../database_pool" } diff --git a/crates/ezidam/src/page.rs b/crates/ezidam/src/page.rs index 90b7d21..c553fa4 100644 --- a/crates/ezidam/src/page.rs +++ b/crates/ezidam/src/page.rs @@ -1,10 +1,13 @@ mod content; +mod flash; mod responder; mod template; use self::content::*; use erased_serde::Serialize; +pub use flash::FlashKind; + pub enum Page { Error(Error), Setup, diff --git a/crates/ezidam/src/page/flash.rs b/crates/ezidam/src/page/flash.rs new file mode 100644 index 0000000..c138b01 --- /dev/null +++ b/crates/ezidam/src/page/flash.rs @@ -0,0 +1,29 @@ +use std::fmt::{Display, Formatter}; + +pub enum FlashKind { + Success, + Info, + Warning, + Danger, +} + +impl Display for FlashKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + FlashKind::Success => "success", + FlashKind::Info => "info", + FlashKind::Warning => "warning", + FlashKind::Danger => "danger", + } + ) + } +} + +impl Into for FlashKind { + fn into(self) -> String { + self.to_string() + } +} diff --git a/crates/ezidam/src/page/template.rs b/crates/ezidam/src/page/template.rs index 0de4441..11eea88 100644 --- a/crates/ezidam/src/page/template.rs +++ b/crates/ezidam/src/page/template.rs @@ -1,4 +1,5 @@ use super::Page; +use rocket::request::FlashMessage; use rocket::serde::Serialize; use rocket_dyn_templates::Template; @@ -7,20 +8,32 @@ use rocket_dyn_templates::Template; struct TemplateContent { title: &'static str, version: &'static str, + flash: Option<(String, String)>, #[serde(flatten)] content: S, } +fn render(p: Page, flash: Option<(String, String)>) -> Template { + Template::render( + p.template_name(), + TemplateContent { + title: p.page_title(), + version: env!("CARGO_PKG_VERSION"), + flash, + content: p.content(), + }, + ) +} + impl From for Template { fn from(p: Page) -> Self { - Self::render( - p.template_name(), - TemplateContent { - title: p.page_title(), - version: env!("CARGO_PKG_VERSION"), - content: p.content(), - }, - ) + render(p, None) + } +} + +impl Page { + pub fn with_flash(self, flash: FlashMessage) -> Template { + render(self, Some(flash.into_inner())) } } diff --git a/crates/ezidam/src/routes.rs b/crates/ezidam/src/routes.rs index 1156525..b9d9ab8 100644 --- a/crates/ezidam/src/routes.rs +++ b/crates/ezidam/src/routes.rs @@ -8,16 +8,19 @@ pub(self) mod prelude { pub use crate::error::Error; pub use crate::file_from_bytes::FileFromBytes; pub use crate::guards::*; - pub use crate::page::Page; + pub use crate::page::{FlashKind, Page}; pub use hash::Password; pub use id::UserID; pub use rocket::form::Form; + pub use rocket::request::FlashMessage; + pub use rocket::response::Flash; pub use rocket::response::Redirect; pub use rocket::tokio::task; pub use rocket::FromForm; pub use rocket::{routes, uri, Either, Route}; pub use rocket_db_pools::sqlx::Acquire; pub use rocket_db_pools::Connection; + pub use rocket_dyn_templates::Template; pub type Result = std::result::Result; } diff --git a/crates/ezidam/src/routes/setup.rs b/crates/ezidam/src/routes/setup.rs index 9a77622..4518d62 100644 --- a/crates/ezidam/src/routes/setup.rs +++ b/crates/ezidam/src/routes/setup.rs @@ -1,6 +1,7 @@ use super::prelude::*; use rocket::{get, post}; use settings::Settings; +use url::Url; use users::User; pub fn routes() -> Vec { @@ -13,14 +14,18 @@ async fn setup_completed(_setup: CompletedSetup) -> Redirect { } #[get("/", rank = 2)] -async fn setup() -> Page { - Page::Setup +async fn setup(flash: Option>) -> Template { + // TODO: show flash on html page + flash + .map(|flash| Page::with_flash(Page::Setup, flash)) + .unwrap_or_else(|| Page::Setup.into()) } #[derive(Debug, FromForm)] pub struct CreateFirstAccount<'r> { pub username: &'r str, pub password: &'r str, + pub url: &'r str, } #[post("/", data = "
")] @@ -28,9 +33,18 @@ async fn create_first_account( form: Form>, _setup: NeedSetup, mut db: Connection, -) -> Result { +) -> Result>> { let form = form.into_inner(); + // Parse url + return Ok(Either::Right(Flash::new( + Redirect::to(uri!(self::setup)), + FlashKind::Danger, + "Failed to parse url".to_string(), + ))); + + let url = Url::parse(form.url).unwrap(); + // Generate UserID let user_id = task::spawn_blocking(UserID::default).await?; @@ -40,7 +54,7 @@ async fn create_first_account( let mut transaction = db.begin().await?; - // Insert in database + // Insert user in database User::insert( &mut transaction, &user_id, @@ -53,11 +67,14 @@ async fn create_first_account( // Store UserID in settings Settings::set_first_admin(&mut transaction, &user_id).await?; + // Store URL in settings + Settings::set_url(&mut transaction, &url).await?; + transaction.commit().await?; // TODO: login with openid/oauth - Ok(Redirect::to(uri!("/"))) + Ok(Either::Left(Redirect::to(uri!("/")))) } #[cfg(test)] @@ -77,10 +94,9 @@ mod test { let create_account = client .post(uri!("/setup")) .header(ContentType::Form) - .body(r#"username=phil&password=password"#) + .body(r#"username=phil&password=password&url=https://example.com"#) .dispatch(); - assert_ne!(create_account.status(), Status::UnprocessableEntity); - assert_ne!(create_account.status(), Status::InternalServerError); + assert_eq!(create_account.status(), Status::SeeOther); // Make request again, make sure its not OK let setup_page_after_creation = client.get(uri!("/setup")).dispatch(); diff --git a/crates/ezidam/templates/setup.html.tera b/crates/ezidam/templates/setup.html.tera index cd8e93c..d6b4a84 100644 --- a/crates/ezidam/templates/setup.html.tera +++ b/crates/ezidam/templates/setup.html.tera @@ -16,27 +16,35 @@

Welcome to Ezidam!

Initial setup

+ +
first admin account
- +
- +
+ + +
settings
+
+
+ + +
+
- - - - +
diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 9db2b5f..21eb208 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" database = { path = "../database" } id = { path = "../id" } thiserror = { workspace = true } -chrono = { workspace = true } \ No newline at end of file +chrono = { workspace = true } +url = { workspace = true } diff --git a/crates/settings/src/database.rs b/crates/settings/src/database.rs index 82fff88..e53af79 100644 --- a/crates/settings/src/database.rs +++ b/crates/settings/src/database.rs @@ -3,6 +3,7 @@ use crate::Settings; use database::sqlx::SqliteExecutor; use database::Settings as DatabaseSettings; use id::UserID; +use url::Url; const DEFAULT_BUSINESS_NAME: &str = "ezidam"; pub const DEFAULT_BUSINESS_LOGO: &[u8] = include_bytes!("../../../logo/ezidam.png"); @@ -18,6 +19,7 @@ impl From for Settings { business_logo: db .business_logo .unwrap_or_else(|| DEFAULT_BUSINESS_LOGO.to_vec()), + url: db.url, } } } @@ -55,4 +57,10 @@ impl Settings { Ok(()) } + + pub async fn set_url(conn: impl SqliteExecutor<'_>, url: &Url) -> Result<(), Error> { + DatabaseSettings::set_url(conn, url.as_str()).await?; + + Ok(()) + } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index ffb88e2..987cdf7 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -13,6 +13,7 @@ pub struct Settings { updated_at: DateTime, business_name: String, business_logo: Vec, + url: Option, } impl Settings {