settings: add base url, WIP flash system

This commit is contained in:
Philippe Loctaux 2023-03-07 08:42:23 +01:00
parent f2bea92272
commit c670201b86
18 changed files with 190 additions and 68 deletions

2
Cargo.lock generated
View file

@ -565,6 +565,7 @@ dependencies = [
"rocket_db_pools",
"rocket_dyn_templates",
"settings",
"url",
"users",
]
@ -2071,6 +2072,7 @@ dependencies = [
"database",
"id",
"thiserror",
"url",
]
[[package]]

View file

@ -8,3 +8,4 @@ members = [
thiserror = "1"
chrono = "0.4.23"
sqlx = "0.5.13"
url = "2.3.1"

View file

@ -0,0 +1,2 @@
alter table settings
drop column url;

View file

@ -0,0 +1,2 @@
alter table settings
add column url TEXT;

View file

@ -2,7 +2,8 @@ select id,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
business_name,
business_logo
business_logo,
url
from settings

View file

@ -0,0 +1,5 @@
update settings
set url = ?
where id is 0

View file

@ -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<Utc>",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "updated_at: DateTime<Utc>",
"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<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\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<Utc>\",\n u.updated_at as \"updated_at: DateTime<Utc>\",\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<Utc>",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "updated_at: DateTime<Utc>",
"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<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n business_name,\n business_logo\n\nfrom settings\n\nwhere id is 0\n"
}
}

View file

@ -10,6 +10,7 @@ pub struct Settings {
pub updated_at: DateTime<Utc>,
pub business_name: Option<String>,
pub business_logo: Option<Vec<u8>>,
pub url: Option<String>,
}
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<Option<()>, 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(()))
}
}

View file

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

View file

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

View file

@ -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<String> for FlashKind {
fn into(self) -> String {
self.to_string()
}
}

View file

@ -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<S: Serialize> {
title: &'static str,
version: &'static str,
flash: Option<(String, String)>,
#[serde(flatten)]
content: S,
}
impl From<Page> for Template {
fn from(p: Page) -> Self {
Self::render(
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<Page> for Template {
fn from(p: Page) -> Self {
render(p, None)
}
}
impl Page {
pub fn with_flash(self, flash: FlashMessage) -> Template {
render(self, Some(flash.into_inner()))
}
}

View file

@ -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<T> = std::result::Result<T, Error>;
}

View file

@ -1,6 +1,7 @@
use super::prelude::*;
use rocket::{get, post};
use settings::Settings;
use url::Url;
use users::User;
pub fn routes() -> Vec<Route> {
@ -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<FlashMessage<'_>>) -> 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 = "<form>")]
@ -28,9 +33,18 @@ async fn create_first_account(
form: Form<CreateFirstAccount<'_>>,
_setup: NeedSetup,
mut db: Connection<Database>,
) -> Result<Redirect> {
) -> Result<Either<Redirect, Flash<Redirect>>> {
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();

View file

@ -16,27 +16,35 @@
<h1 class="">Welcome to Ezidam!</h1>
<p class="text-muted">Initial setup</p>
</div>
<!-- First admin account -->
<div class="hr-text hr-text-center hr-text-spaceless">first admin account</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label required" for="username">Username</label>
<input name="username" id="username" type="text" class="form-control" required>
<input name="username" id="username" type="text" placeholder="Enter a username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label required" for="password">Password</label>
<div class="input-group input-group-flat">
<input name="password" id="password" type="password" class="form-control" autocomplete="off" required>
<input name="password" id="password" type="password" placeholder="Enter password" class="form-control" autocomplete="off" required>
</div>
</div>
</div>
<!-- Settings -->
<div class="hr-text hr-text-center hr-text-spaceless">settings</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label required" for="url">Base URL</label>
<input name="url" id="url" type="url" placeholder="https://example.com" class="form-control" required>
</div>
</div>
</div>
<div class="row align-items-center mt-3">
<div class="col">
<div class="btn-list justify-content-end">
<button type="submit" class="btn btn-primary">Create account</button>
<!-- <a href="#" class="btn btn-primary">-->
<!-- Create account-->
<!-- </a>-->
<button type="submit" class="btn btn-primary">Finish setup</button>
</div>
</div>
</div>

View file

@ -8,3 +8,4 @@ database = { path = "../database" }
id = { path = "../id" }
thiserror = { workspace = true }
chrono = { workspace = true }
url = { workspace = true }

View file

@ -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<DatabaseSettings> 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(())
}
}

View file

@ -13,6 +13,7 @@ pub struct Settings {
updated_at: DateTime<Utc>,
business_name: String,
business_logo: Vec<u8>,
url: Option<String>,
}
impl Settings {