Add demo mode + various fixes.

This commit is contained in:
Dietrich 2021-10-03 07:53:18 +02:00
parent 341a5ad826
commit 952e2ca296
9 changed files with 386 additions and 345 deletions

532
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -168,3 +168,26 @@ ExecStart=/var/pslink/pslink runserver
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
### Setup a demo container
First build the standalone binary:
```bash
$ cargo make build_standalone
```
Create a temporary directory and copy the binary from above:
```bash
$ mkdir /tmp/pslink-container/
$ cp target/x86_64-unknown-linux-musl/release/pslink /tmp/pslink-container/
```
Run the container (podman is used here but docker could be used exactly the same):
```bash
$ podman run --expose 8080 -p=8080:8080 -it pslink-container ./pslink demo -i 0.0.0.0
```
Note that this is **absolutely not for a production use** and only for demo purposes as the links are **deleted on every restart**.

View File

@ -16,7 +16,7 @@ use crate::Msg;
pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node<Msg> { pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node<Msg> {
// A shortcut for translating strings. // A shortcut for translating strings.
let t = move |key: &str| i18n.translate(key, None); let t = move |key: &str| i18n.translate(key, None);
// Translate the wellcome message // Translate the welcome message
let welcome = i18n.translate( let welcome = i18n.translate(
"welcome-user", "welcome-user",
Some(&fluent_args![ "username" => user.username.clone()]), Some(&fluent_args![ "username" => user.username.clone()]),

View File

@ -91,7 +91,7 @@ impl<T> Deref for Cached<T> {
} }
} }
/// There can allways be only one dialog. /// There can always be only one dialog.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Dialog { enum Dialog {
EditLink { EditLink {
@ -150,7 +150,7 @@ pub enum Msg {
Query(QueryMsg), // Messages related to querying links Query(QueryMsg), // Messages related to querying links
Edit(EditMsg), // Messages related to editing links Edit(EditMsg), // Messages related to editing links
ClearAll, // Clear all messages ClearAll, // Clear all messages
SetupObserver, // Make an observer for endles scroll SetupObserver, // Make an observer for endless scroll
Observed(Vec<IntersectionObserverEntry>), Observed(Vec<IntersectionObserverEntry>),
SetMessage(String), // Set a message to the user SetMessage(String), // Set a message to the user
} }
@ -244,7 +244,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
log!("element not yet registered! "); log!("element not yet registered! ");
}; };
} else { } else {
log!("Failed to get observer!") log!("Failed to get observer!");
}; };
} }
} }
@ -255,13 +255,13 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp
match msg { match msg {
QueryMsg::Fetch => { QueryMsg::Fetch => {
orders.skip(); // No need to rerender orders.skip(); // No need to rerender
initial_load(model, orders) initial_load(model, orders);
} }
QueryMsg::FetchAdditional => { QueryMsg::FetchAdditional => {
orders.skip(); // No need to rerender orders.skip(); // No need to rerender
consecutive_load(model, orders) consecutive_load(model, orders);
} }
// Default to ascending ordering but if the links are already sorted according to this collumn toggle between ascending and descending ordering. // Default to ascending ordering but if the links are already sorted according to this column toggle between ascending and descending ordering.
QueryMsg::OrderBy(column) => { QueryMsg::OrderBy(column) => {
model.formconfig.order = model.formconfig.order.as_ref().map_or_else( model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|| { || {
@ -316,7 +316,7 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp
QueryMsg::ReceivedAdditional(response) => { QueryMsg::ReceivedAdditional(response) => {
if response.len() < model.formconfig.amount { if response.len() < model.formconfig.amount {
log!("There are no more links! "); log!("There are no more links! ");
model.everything_loaded = true model.everything_loaded = true;
}; };
let mut new_links = response let mut new_links = response
.into_iter() .into_iter()
@ -376,7 +376,7 @@ fn load_links(orders: &mut impl Orders<Msg>, data: LinkRequestForm) {
.json(&data), .json(&data),
Msg::SetMessage("Failed to parse data".to_string()) Msg::SetMessage("Failed to parse data".to_string())
); );
// send the request and recieve a response // send the request and receive a response
let response = unwrap_or_return!( let response = unwrap_or_return!(
fetch(request).await, fetch(request).await,
Msg::SetMessage("Failed to send data".to_string()) Msg::SetMessage("Failed to send data".to_string())
@ -554,7 +554,7 @@ fn delete_link(link_delta: LinkDelta, orders: &mut impl Orders<Msg>) {
.json(&link_delta), .json(&link_delta),
Msg::SetMessage("serialization failed".to_string()) Msg::SetMessage("serialization failed".to_string())
); );
// perform the request and recieve a respnse // perform the request and receive a response
let response = let response =
unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToDeleteLink)); unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToDeleteLink));
@ -746,7 +746,8 @@ fn view_link(l: &Cached<FullLink>, logged_in_user: &User) -> Node<Msg> {
C!["table_qr"], C!["table_qr"],
a![ a![
ev(Ev::Click, |event| event.stop_propagation()), ev(Ev::Click, |event| event.stop_propagation()),
attrs![At::Href => format!["/admin/download/png/{}", &l.link.code], At::Download => true.as_at_value()], attrs![At::Href => format!("/admin/download/png/{}", &l.link.code),
At::Download => true.as_at_value()],
raw!(&l.cache) raw!(&l.cache)
] ]
] ]

View File

@ -62,7 +62,7 @@ struct FilterInput {
filter_input: ElRef<web_sys::HtmlInputElement>, filter_input: ElRef<web_sys::HtmlInputElement>,
} }
/// The message splits the contained message into messages related to querrying and messages related to editing. /// The message splits the contained message into messages related to querying and messages related to editing.
#[derive(Clone)] #[derive(Clone)]
pub enum Msg { pub enum Msg {
Query(UserQueryMsg), Query(UserQueryMsg),
@ -161,7 +161,7 @@ pub fn process_query_messages(msg: UserQueryMsg, model: &mut Model, orders: &mut
} }
UserQueryMsg::EmailFilterChanged(s) => { UserQueryMsg::EmailFilterChanged(s) => {
log!("Filter is: ", &s); log!("Filter is: ", &s);
// FIXME: Sanitazion does not work for @ // FIXME: Sanitation does not work for @
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect(); let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[UserOverviewColumns::Email].sieve = sanit; model.formconfig.filter[UserOverviewColumns::Email].sieve = sanit;
orders.send_msg(Msg::Query(UserQueryMsg::Fetch)); orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
@ -238,7 +238,7 @@ pub fn process_user_edit_messages(
let data = model let data = model
.user_edit .user_edit
.take() .take()
.expect("A user should allways be there on save"); .expect("A user should always be there on save");
log!("Saving User: ", &data.username); log!("Saving User: ", &data.username);
save_user(data, orders); save_user(data, orders);
} }

View File

@ -1,6 +1,6 @@
use clap::{ use clap::{
app_from_crate, crate_authors, crate_description, crate_name, crate_version, App, Arg, app_from_crate, crate_authors, crate_description, crate_name, crate_version, App, AppSettings,
ArgMatches, SubCommand, Arg, ArgMatches, SubCommand,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use pslink_shared::datatypes::{Secret, User}; use pslink_shared::datatypes::{Secret, User};
@ -12,7 +12,7 @@ use std::{
}; };
use pslink::{ use pslink::{
models::{NewUser, UserDbOperations}, models::{NewLink, NewUser, UserDbOperations},
ServerConfig, ServerError, ServerConfig, ServerError,
}; };
@ -20,7 +20,7 @@ use tracing::{error, info, trace, warn};
static MIGRATOR: Migrator = sqlx::migrate!(); static MIGRATOR: Migrator = sqlx::migrate!();
/// generate the commandline options available /// generate the command line options available
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn generate_cli() -> App<'static, 'static> { fn generate_cli() -> App<'static, 'static> {
app_from_crate!() app_from_crate!()
@ -47,7 +47,7 @@ fn generate_cli() -> App<'static, 'static> {
.short("u") .short("u")
.help("The host url or the page that will be part of the short urls.") .help("The host url or the page that will be part of the short urls.")
.env("PSLINK_PUBLIC_URL") .env("PSLINK_PUBLIC_URL")
.default_value("localhost:8080") .default_value("127.0.0.1:8080")
.global(true), .global(true),
) )
.arg( .arg(
@ -63,7 +63,7 @@ fn generate_cli() -> App<'static, 'static> {
Arg::with_name("brand_name") Arg::with_name("brand_name")
.long("brand-name") .long("brand-name")
.short("b") .short("b")
.help("The Brandname that will apper in various places.") .help("The brand name that will appear in various places.")
.env("PSLINK_BRAND_NAME") .env("PSLINK_BRAND_NAME")
.default_value("Pslink") .default_value("Pslink")
.global(true), .global(true),
@ -74,7 +74,7 @@ fn generate_cli() -> App<'static, 'static> {
.short("i") .short("i")
.help("The host (ip) that will run the pslink service") .help("The host (ip) that will run the pslink service")
.env("PSLINK_IP") .env("PSLINK_IP")
.default_value("localhost") .default_value("127.0.0.1")
.global(true), .global(true),
) )
.arg( .arg(
@ -95,11 +95,11 @@ fn generate_cli() -> App<'static, 'static> {
.long("secret") .long("secret")
.help(concat!( .help(concat!(
"The secret that is used to encrypt the", "The secret that is used to encrypt the",
" password database keep this as inacessable as possible.", " password database keep this as inaccessible as possible.",
" As commandlineparameters are visible", " As command line parameters are visible",
" to all users", " to all users",
" it is not wise to use this as", " it is not wise to use this as",
" a commandline parameter but rather as an environment variable.", " a command line parameter but rather as an environment variable.",
)) ))
.env("PSLINK_SECRET") .env("PSLINK_SECRET")
.default_value("") .default_value("")
@ -125,6 +125,12 @@ fn generate_cli() -> App<'static, 'static> {
.about("Create an admin user.") .about("Create an admin user.")
.display_order(2), .display_order(2),
) )
.subcommand(
SubCommand::with_name("demo")
.about("Create a database and demo user.")
.display_order(3)
.setting(AppSettings::Hidden),
)
} }
/// parse the options to the [`ServerConfig`] struct /// parse the options to the [`ServerConfig`] struct
@ -142,7 +148,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
warn!("If you change the secret all passwords will be invalid"); warn!("If you change the secret all passwords will be invalid");
warn!("Using an auto generated one for this run."); warn!("Using an auto generated one for this run.");
} else { } else {
warn!("The provided secret was too short. Using an autogenerated one."); warn!("The provided secret was too short. Using an auto generated one.");
} }
thread_rng() thread_rng()
@ -158,7 +164,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
.value_of("database") .value_of("database")
.expect(concat!( .expect(concat!(
"Neither the DATABASE_URL environment variable", "Neither the DATABASE_URL environment variable",
" nor the commandline parameters", " nor the command line parameters",
" contain a valid database location." " contain a valid database location."
)) ))
.parse::<PathBuf>() .parse::<PathBuf>()
@ -186,7 +192,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
.value_of("port") .value_of("port")
.expect("Failed to read the port value") .expect("Failed to read the port value")
.parse::<u32>() .parse::<u32>()
.expect("Failed to parse the portnumber"); .expect("Failed to parse the port number");
let protocol = config let protocol = config
.value_of("protocol") .value_of("protocol")
.expect("Failed to read the protocol value") .expect("Failed to read the protocol value")
@ -209,7 +215,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
/// Setup and launch the command /// Setup and launch the command
/// ///
/// # Panics /// # Panics
/// This funcion panics if preconditions like the availability of the database are not met. /// This function panics if preconditions like the availability of the database are not met.
pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> { pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
// load the environment .env file if available. // load the environment .env file if available.
dotenv().ok(); dotenv().ok();
@ -225,7 +231,7 @@ pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
.value_of("database") .value_of("database")
.expect(concat!( .expect(concat!(
"Neither the DATABASE_URL environment variable", "Neither the DATABASE_URL environment variable",
" nor the commandline parameters", " nor the command line parameters",
" contain a valid database location." " contain a valid database location."
)) ))
.parse::<PathBuf>() .parse::<PathBuf>()
@ -234,13 +240,14 @@ pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
if !db.exists() { if !db.exists() {
trace!("No database file found {}", db.display()); trace!("No database file found {}", db.display());
if !(config.subcommand_matches("migrate-database").is_none() if !(config.subcommand_matches("migrate-database").is_none()
| config.subcommand_matches("generate-env").is_none()) | config.subcommand_matches("generate-env").is_none()
| config.subcommand_matches("demo").is_none())
{ {
let msg = format!( let msg = format!(
concat!( concat!(
"Database not found at {}!", "Database not found at {}!",
" Create a new database with: `pslink migrate-database`", "Create a new database with: `pslink migrate-database`",
"or adjust the databasepath." "or adjust the database path."
), ),
db.display() db.display()
); );
@ -268,12 +275,40 @@ pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
}; };
} }
if let Some(_create_config) = config.subcommand_matches("create-admin") { if let Some(_create_config) = config.subcommand_matches("create-admin") {
return match create_admin(&server_config).await { return match request_admin_credentials(&server_config).await {
Ok(_) => Ok(None), Ok(_) => Ok(None),
Err(e) => Err(e), Err(e) => Err(e),
}; };
} }
if let Some(_runserver_config) = config.subcommand_matches("demo") {
let num_users = User::count_admins(&server_config).await;
match num_users {
Err(_) => {
generate_env_file(&server_config).expect("Failed to generate env file.");
apply_migrations(&server_config)
.await
.expect("Failed to apply migrations.");
let new_admin = NewUser::new(
"demo".to_string(),
"demo@teilgedanken.de".to_string(),
"demo",
&server_config.secret,
)
.expect("Failed to generate new user credentials.");
create_admin(&new_admin, &server_config)
.await
.expect("Failed to create admin");
add_example_links(&server_config).await;
return Ok(Some(server_config));
}
_ => {
return Err(ServerError::User("The database is not empty aborting because this could mean that creating a demo instance would lead in data loss.".to_string()));
}
}
}
if let Some(_runserver_config) = config.subcommand_matches("runserver") { if let Some(_runserver_config) = config.subcommand_matches("runserver") {
let num_users = User::count_admins(&server_config).await?; let num_users = User::count_admins(&server_config).await?;
@ -294,8 +329,54 @@ pub async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
} }
} }
async fn add_example_links(server_config: &ServerConfig) {
NewLink {
title: "Pslink Repository".to_owned(),
target: "https://github.com/enaut/pslink".to_owned(),
code: "pslink".to_owned(),
author: 1,
created_at: chrono::Local::now().naive_utc(),
}
.insert(server_config)
.await
.expect("Failed to insert example 1");
NewLink {
title: "Seed".to_owned(),
target: "https://seed-rs.org/".to_owned(),
code: "seed".to_owned(),
author: 1,
created_at: chrono::Local::now().naive_utc(),
}
.insert(server_config)
.await
.expect("Failed to insert example 1");
NewLink {
title: "actix".to_owned(),
target: "https://actix.rs/".to_owned(),
code: "actix".to_owned(),
author: 1,
created_at: chrono::Local::now().naive_utc(),
}
.insert(server_config)
.await
.expect("Failed to insert example 1");
NewLink {
title: "rust".to_owned(),
target: "https://www.rust-lang.org/".to_owned(),
code: "rust".to_owned(),
author: 1,
created_at: chrono::Local::now().naive_utc(),
}
.insert(server_config)
.await
.expect("Failed to insert example 1");
}
/// Interactively create a new admin user. /// Interactively create a new admin user.
async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> { async fn request_admin_credentials(config: &ServerConfig) -> Result<(), ServerError> {
info!("Creating an admin user."); info!("Creating an admin user.");
let sin = io::stdin(); let sin = io::stdin();
@ -306,7 +387,7 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
let new_username = sin.lock().lines().next().unwrap().unwrap(); let new_username = sin.lock().lines().next().unwrap().unwrap();
print!("Please enter the emailadress for {}: ", new_username); print!("Please enter the email address for {}: ", new_username);
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
let new_email = sin.lock().lines().next().unwrap().unwrap(); let new_email = sin.lock().lines().next().unwrap().unwrap();
@ -325,16 +406,20 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
&config.secret, &config.secret,
)?; )?;
new_admin.insert_user(config).await?; create_admin(&new_admin, config).await
let created_user = User::get_user_by_name(&new_username, config).await?; }
async fn create_admin(new_user: &NewUser, config: &ServerConfig) -> Result<(), ServerError> {
new_user.insert_user(config).await?;
let created_user = User::get_user_by_name(&new_user.username, config).await?;
created_user.toggle_admin(config).await?; created_user.toggle_admin(config).await?;
info!("Admin user created: {}", new_username); info!("Admin user created: {}", &new_user.username);
Ok(()) Ok(())
} }
/// Apply any pending migrations to the database. The migrations are embedded in the binary and don't need any addidtional files. /// Apply any pending migrations to the database. The migrations are embedded in the binary and don't need any additional files.
async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> { async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
info!( info!(
"Creating a database file and running the migrations in the file {}:", "Creating a database file and running the migrations in the file {}:",
@ -344,7 +429,7 @@ async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
Ok(()) Ok(())
} }
/// The commandline parameters provided or if missing the default parameters can be converted and written to a .env file. That way the configuration is saved and automatically reused for subsequent launches. /// The command line parameters provided or if missing the default parameters can be converted and written to a .env file. That way the configuration is saved and automatically reused for subsequent launches.
fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> { fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> {
if std::path::Path::new(".env").exists() { if std::path::Path::new(".env").exists() {
return Err(ServerError::User( return Err(ServerError::User(

View File

@ -79,7 +79,7 @@ async fn main() -> std::result::Result<(), std::io::Error> {
// include the static files into the binary // include the static files into the binary
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
/// Launch the pslink-webservice /// Launch the pslink-web-service
/// ///
/// # Errors /// # Errors
/// This produces a [`ServerError`] if: /// This produces a [`ServerError`] if:

View File

@ -67,9 +67,9 @@ fn detect_language(request: &HttpRequest) -> Result<Lang, ServerError> {
); );
info!("supported languages: {:?}", supported); info!("supported languages: {:?}", supported);
if let Some(languagecode) = supported.get(0) { if let Some(language_code) = supported.get(0) {
info!("Supported Language: {}", languagecode); info!("Supported Language: {}", language_code);
Ok(languagecode Ok(language_code
.to_string() .to_string()
.parse() .parse()
.expect("Failed to parse 2 language")) .expect("Failed to parse 2 language"))

View File

@ -368,7 +368,7 @@ impl NewLink {
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the database cannot be acessed or constraints are not met. /// fails with [`ServerError`] if the database cannot be acessed or constraints are not met.
pub(crate) async fn insert(self, server_config: &ServerConfig) -> Result<(), ServerError> { pub async fn insert(self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!( sqlx::query!(
"Insert into links ( "Insert into links (
title, title,