settings: add base url, WIP flash system
This commit is contained in:
parent
f2bea92272
commit
c670201b86
18 changed files with 190 additions and 68 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -565,6 +565,7 @@ dependencies = [
|
|||
"rocket_db_pools",
|
||||
"rocket_dyn_templates",
|
||||
"settings",
|
||||
"url",
|
||||
"users",
|
||||
]
|
||||
|
||||
|
|
@ -2071,6 +2072,7 @@ dependencies = [
|
|||
"database",
|
||||
"id",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ members = [
|
|||
thiserror = "1"
|
||||
chrono = "0.4.23"
|
||||
sqlx = "0.5.13"
|
||||
url = "2.3.1"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
alter table settings
|
||||
drop column url;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
alter table settings
|
||||
add column url TEXT;
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
5
crates/database/queries/settings/set_url.sql
Normal file
5
crates/database/queries/settings/set_url.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
update settings
|
||||
|
||||
set url = ?
|
||||
|
||||
where id is 0
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
29
crates/ezidam/src/page/flash.rs
Normal file
29
crates/ezidam/src/page/flash.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ database = { path = "../database" }
|
|||
id = { path = "../id" }
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub struct Settings {
|
|||
updated_at: DateTime<Utc>,
|
||||
business_name: String,
|
||||
business_logo: Vec<u8>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue