From 9512807eb305dbcb92fdb02ef60dc1abd1052180 Mon Sep 17 00:00:00 2001 From: Dietrich Date: Sun, 7 Mar 2021 19:14:34 +0100 Subject: [PATCH] Add command line interface Add a command line interface to the binary and remove parts that were hardcoded. new --help is: ``` pslink 0.1.0 Dietrich A simple webservice that allows registered users to create short links including qr-codes. Anyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers. USAGE: pslink [OPTIONS] [SUBCOMMAND] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: --db The path of the sqlite database [env: PSLINK_DATABASE=] [default: links.db] -i, --hostip The host (ip) that will run the pslink service [env: PSLINK_IP=] [default: localhost] -p, --port The port the pslink service will run on [env: PSLINK_PORT=] [default: 8080] -t, --protocol The protocol that is used in the qr-codes (http results in slightly smaller codes in some cases) [env: PSLINK_PROTOCOL=] [default: http] [possible values: http, https] -u, --public-url The host url or the page that will be part of the short urls. [env: PSLINK_PUBLIC_URL=] [default: localhost:8080] --secret The secret that is used to encrypt the password database keep this as inacessable as possible. As commandlineparameters are visible to all users it is not wise to use this as a commandline parameter but rather as an environment variable. [env: PSLINK_SECRET=] [default: ] SUBCOMMANDS: runserver Run the server create-admin Create an admin user. generate-env Generate an .env file template using default settings and exit migrate-database Apply any pending migrations and exit help Prints this message or the help of the given subcommand(s) ``` --- Cargo.lock | 252 ++++++++++--- Cargo.toml | 22 +- README.md | 0 .../up.sql | 21 -- src/cli.rs | 340 ++++++++++++++++++ src/forms.rs | 2 +- src/main.rs | 212 +++++++---- src/models.rs | 22 +- src/queries.rs | 184 ++++++---- src/views.rs | 108 +++--- 10 files changed, 901 insertions(+), 262 deletions(-) create mode 100644 README.md create mode 100644 src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index ea97442..e8de2c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,19 @@ dependencies = [ "pin-project 0.4.27", ] +[[package]] +name = "actix-slog" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697a62a2f51c5c26af6b1dded0622f15bec690da191615947e0c1b2b7b75198" +dependencies = [ + "actix-web", + "chrono", + "futures 0.3.12", + "pin-project 0.4.27", + "slog", +] + [[package]] name = "actix-testing" version = "1.0.1" @@ -278,11 +291,11 @@ dependencies = [ [[package]] name = "actix-web-static-files" version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5bced189d398750426bd73719acfa63e2b3707e3804891e82e9cfa189ce9c8" +source = "git+https://github.com/enaut/actix-web-static-files.git?branch=enaut-must_use#d33061edf44bff0e0d1fcdf5e8116a8967a4a974" dependencies = [ "actix-service", "actix-web", + "change-detection", "derive_more", "futures 0.3.12", "mime_guess", @@ -524,7 +537,7 @@ dependencies = [ "cfg-if 0.1.10", "clang-sys", "clap", - "env_logger 0.6.2", + "env_logger", "hashbrown 0.1.8", "lazy_static", "log", @@ -675,6 +688,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "change-detection" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c334929165c9f014ac007721d56fec81eda30496131cc88cb0fc8ff6d0b59b3" +dependencies = [ + "path-matchers", + "path-slash", +] + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -720,7 +743,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ef0c1bcf2e99c649104bd7a7012d8f8802684400e03db0ec0af48583c6fa0e4" dependencies = [ - "glob", + "glob 0.2.11", "libc", "libloading", ] @@ -964,6 +987,16 @@ dependencies = [ "diesel 0.16.0", ] +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + [[package]] name = "digest" version = "0.8.1" @@ -982,6 +1015,27 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -1033,20 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" dependencies = [ "atty", - "humantime 1.3.0", - "log", - "regex 1.4.3", - "termcolor", -] - -[[package]] -name = "env_logger" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" -dependencies = [ - "atty", - "humantime 2.1.0", + "humantime", "log", "regex 1.4.3", "termcolor", @@ -1153,6 +1194,7 @@ checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1185,6 +1227,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "futures-executor" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.12" @@ -1320,6 +1373,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "globset" version = "0.4.6" @@ -1461,12 +1520,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "idna" version = "0.2.0" @@ -1693,6 +1746,27 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "migrations_internals" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel 1.4.5", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.60", +] + [[package]] name = "mime" version = "0.3.16" @@ -1886,7 +1960,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "smallvec", "winapi 0.3.9", ] @@ -1900,6 +1974,15 @@ dependencies = [ "regex 1.4.3", ] +[[package]] +name = "path-matchers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ebf24bbe8ea30c5d59eec679c6442eb9d58dce349d58db8b8e9219c1690260" +dependencies = [ + "glob 0.3.0", +] + [[package]] name = "path-slash" version = "0.1.4" @@ -2084,6 +2167,32 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "pslink" +version = "0.1.0" +dependencies = [ + "actix-identity", + "actix-slog", + "actix-web", + "actix-web-static-files", + "argonautica", + "chrono", + "clap", + "diesel 1.4.5", + "diesel_codegen", + "diesel_migrations", + "dotenv", + "image", + "qrcode", + "rand 0.8.3", + "rpassword", + "serde", + "slog", + "slog-async", + "slog-term", + "tera", +] + [[package]] name = "qrcode" version = "0.12.0" @@ -2364,6 +2473,25 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.2", + "redox_syscall 0.2.5", +] + [[package]] name = "regex" version = "0.2.11" @@ -2423,6 +2551,16 @@ dependencies = [ "quick-error", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "rustc-demangle" version = "0.1.18" @@ -2438,6 +2576,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + [[package]] name = "ryu" version = "1.0.5" @@ -2589,23 +2733,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] -name = "slink" -version = "0.1.0" +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60813879f820c85dbc4eabf3269befe374591289019775898d56a81a804fbdc" dependencies = [ - "actix-identity", - "actix-web", - "actix-web-static-files", - "argonautica", + "crossbeam-channel", + "slog", + "take_mut", + "thread_local 1.1.2", +] + +[[package]] +name = "slog-term" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c1e7e5aab61ced6006149ea772770b84a0d16ce0f7885def313e4829946d76" +dependencies = [ + "atty", "chrono", - "diesel 1.4.5", - "diesel_codegen", - "dotenv", - "env_logger 0.8.2", - "image", - "log", - "qrcode", - "serde", - "tera", + "slog", + "term", + "thread_local 1.1.2", ] [[package]] @@ -2747,6 +2902,12 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tempdir" version = "0.3.7" @@ -2779,6 +2940,17 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi 0.3.9", +] + [[package]] name = "termcolor" version = "1.1.2" diff --git a/Cargo.toml b/Cargo.toml index 5599a07..608396f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,39 @@ [package] -name = "slink" +name = "pslink" version = "0.1.0" +description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers." authors = ["Dietrich "] edition = "2018" +license = "MIT OR Apache-2.0" +keywords = ["url", "link", "webpage", "actix"] +categories = ["os", "os::linux-apis", "parser-implementations", "command-line-utilities"] +readme = "README.md" +repository = "https://git.teilgedanken.de/dietrich/Pslink" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] actix-web = "3" -actix-web-static-files = "3" +actix-web-static-files = { git = "https://github.com/enaut/actix-web-static-files.git", branch = "enaut-must_use" } +actix-slog = "0.2" tera = "1.6" serde = "1.0" diesel = { version = "1.4", features = ["sqlite", "chrono"] } diesel_codegen = { version = "0.16.1", features = ["sqlite"] } +diesel_migrations = "1.4" dotenv = "0.10.1" actix-identity = "0.3" chrono = { version = "0.4", features = ["serde"] } argonautica = "0.2" -env_logger = "0.8" -log = "0.4" +slog = "2" +slog-term = "2" +slog-async = "2" qrcode = "0.12" image = "0.23" +rand="0.8" +rpassword = "5.0" +clap = "2.33" [build-dependencies] -actix-web-static-files = "3" \ No newline at end of file +actix-web-static-files = { git = "https://github.com/enaut/actix-web-static-files.git", branch = "enaut-must_use" } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2021-02-10-072528_add_user_profiles/up.sql b/migrations/2021-02-10-072528_add_user_profiles/up.sql index 944f56b..dfcf41f 100644 --- a/migrations/2021-02-10-072528_add_user_profiles/up.sql +++ b/migrations/2021-02-10-072528_add_user_profiles/up.sql @@ -1,27 +1,6 @@ -- Your SQL goes here ALTER TABLE users ADD COLUMN role INTEGER DEFAULT 1 NOT NULL; -CREATE TABLE usersnew -( - id INTEGER PRIMARY KEY NOT NULL, - username VARCHAR NOT NULL, - email VARCHAR NOT NULL, - password VARCHAR NOT NULL, - role INTEGER DEFAULT 1 NOT NULL, - - UNIQUE(username, email), - FOREIGN KEY - (role) REFERENCES user_roles - (id) -); - -INSERT INTO usersnew -SELECT * -FROM users; -DROP TABLE users; - -ALTER TABLE usersnew -RENAME TO users; UPDATE users SET role=2 where id is 1; diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..a8bfe8d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,340 @@ +use clap::{ + app_from_crate, crate_authors, crate_description, crate_name, crate_version, App, Arg, + ArgMatches, SubCommand, +}; +use diesel::prelude::*; +use dotenv::dotenv; +use std::{ + io::{self, BufRead, Write}, + path::PathBuf, +}; + +use crate::{models::NewUser, ServerConfig, ServerError}; +use crate::{queries, schema}; + +use slog::{Drain, Logger}; + +fn generate_cli() -> App<'static, 'static> { + app_from_crate!() + .arg( + Arg::with_name("database") + .long("db") + .help("The path of the sqlite database") + .env("PSLINK_DATABASE") + .default_value("links.db") + .global(true), + ) + .arg( + Arg::with_name("port") + .long("port") + .short("p") + .help("The port the pslink service will run on") + .env("PSLINK_PORT") + .default_value("8080") + .global(true), + ) + .arg( + Arg::with_name("public_url") + .long("public-url") + .short("u") + .help("The host url or the page that will be part of the short urls.") + .env("PSLINK_PUBLIC_URL") + .default_value("localhost:8080") + .global(true), + ) + .arg( + Arg::with_name("internal_ip") + .long("hostip") + .short("i") + .help("The host (ip) that will run the pslink service") + .env("PSLINK_IP") + .default_value("localhost") + .global(true), + ) + .arg( + Arg::with_name("protocol") + .long("protocol") + .short("t") + .help(concat!( + "The protocol that is used in the qr-codes", + " (http results in slightly smaller codes in some cases)" + )) + .env("PSLINK_PROTOCOL") + .default_value("http") + .possible_values(&["http", "https"]) + .global(true), + ) + .arg( + Arg::with_name("secret") + .long("secret") + .help(concat!( + "The secret that is used to encrypt the", + " password database keep this as inacessable as possible.", + " As commandlineparameters are visible", + " to all users", + " it is not wise to use this as", + " a commandline parameter but rather as an environment variable.", + )) + .env("PSLINK_SECRET") + .default_value("") + .global(true), + ) + .subcommand( + SubCommand::with_name("runserver") + .about("Run the server") + .display_order(1), + ) + .subcommand( + SubCommand::with_name("migrate-database") + .about("Apply any pending migrations and exit") + .display_order(2), + ) + .subcommand( + SubCommand::with_name("generate-env") + .about("Generate an .env file template using default settings and exit") + .display_order(2), + ) + .subcommand( + SubCommand::with_name("create-admin") + .about("Create an admin user.") + .display_order(2), + ) +} + +fn parse_args_to_config(config: &ArgMatches, log: &Logger) -> ServerConfig { + let secret = config + .value_of("secret") + .expect("Failed to read the secret") + .to_owned(); + let secret = if secret.len() < 5 { + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + + if secret.is_empty() { + slog_warn!( + log, + "No secret was found! Use the environment variable PSLINK_SECRET to set one." + ); + slog_warn!( + log, + "If you change the secret all passwords will be invalid" + ); + slog_warn!(log, "Using an auto generated one for this run."); + } else { + slog_warn!( + log, + "The provided secret was too short. Using an autogenerated one." + ) + } + + thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect() + } else { + secret + }; + let db = config + .value_of("database") + .expect(concat!( + "Neither the DATABASE_URL environment variable", + " nor the commandline parameters", + " contain a valid database location." + )) + .parse::() + .expect("Failed to parse Database path."); + let public_url = config + .value_of("public_url") + .expect("Failed to read the host value") + .to_owned(); + let internal_ip = config + .value_of("internal_ip") + .expect("Failed to read the host value") + .to_owned(); + let port = config + .value_of("port") + .expect("Failed to read the port value") + .parse::() + .expect("Failed to parse the portnumber"); + let protocol = config + .value_of("protocol") + .expect("Failed to read the protocol value") + .parse::() + .expect("Failed to parse the protocol"); + + let log = log.new(slog_o!("host" => public_url.clone())); + + crate::ServerConfig { + secret, + db, + public_url, + internal_ip, + port, + protocol, + log, + } +} + +pub(crate) fn setup() -> Result, ServerError> { + dotenv().ok(); + + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + + let log = slog::Logger::root(drain, slog_o!("name" => "Pslink")); + + slog_info!(log, "Launching Pslink a 'Private short link generator'"); + slog_info!(log, ".env file setup, logging initialized"); + + let app = generate_cli(); + let config = app.get_matches(); + + let server_config: crate::ServerConfig = parse_args_to_config(&config, &log); + + if let Some(_migrate_config) = config.subcommand_matches("generate-env") { + return match generate_env_file(&server_config) { + Ok(_) => Ok(None), + Err(e) => Err(e), + }; + } + if let Some(_migrate_config) = config.subcommand_matches("migrate-database") { + return match apply_migrations(&server_config) { + Ok(_) => Ok(None), + Err(e) => Err(e), + }; + } + if let Some(_create_config) = config.subcommand_matches("create-admin") { + return match create_admin(&server_config) { + Ok(_) => Ok(None), + Err(e) => Err(e), + }; + } + + if let Some(_runserver_config) = config.subcommand_matches("runserver") { + let connection = if server_config.db.exists() { + queries::establish_connection(&server_config.db)? + } else { + let msg = format!( + concat!( + "Database not found at {}!", + " Create a new database with: `pslink migrate-database`", + "or adjust the databasepath." + ), + server_config.db.display() + ); + slog_error!(&server_config.log, "{}", msg); + eprintln!("{}", msg); + return Ok(None); + }; + let num_users: i64 = schema::users::dsl::users + .filter(schema::users::dsl::role.eq(2)) + .select(diesel::dsl::count_star()) + .first(&connection) + .expect("Failed to count the users"); + + if num_users < 1 { + slog_warn!( + &server_config.log, + concat!( + "No user created you will not be", + " able to do anything as the service is invite only.", + " Create a user with `pslink create-admin`" + ) + ); + } + slog_info!( + &server_config.log, + "Initialization finished starting the service." + ); + Ok(Some(server_config)) + } else { + println!("{}", config.usage()); + Err(ServerError::User("Print usage.".into())) + } +} + +fn create_admin(config: &ServerConfig) -> Result<(), ServerError> { + use schema::users; + use schema::users::dsl::{id, role}; + slog_info!(&config.log, "Creating an admin user."); + let sin = io::stdin(); + + let connection = queries::establish_connection(&config.db)?; + + // wait for logging: + std::thread::sleep(std::time::Duration::from_millis(100)); + + print!("Please enter the Username of the admin: "); + io::stdout().flush().unwrap(); + let new_username = sin.lock().lines().next().unwrap().unwrap(); + + print!("Please enter the emailadress for {}: ", new_username); + io::stdout().flush().unwrap(); + let email = sin.lock().lines().next().unwrap().unwrap(); + + print!("Please enter the password for {}: ", new_username); + io::stdout().flush().unwrap(); + let password = rpassword::read_password().unwrap(); + slog_info!( + &config.log, + "Creating {} ({}) with given password ", + &new_username, + &email + ); + + let new_admin = NewUser::new(new_username, email, &password, config)?; + + diesel::insert_into(users::table) + .values(&new_admin) + .execute(&connection)?; + + // Add admin rights to the first user (which should be the only one) + diesel::update(users::dsl::users.filter(id.eq(&1))) + .set((role.eq(2),)) + .execute(&connection)?; + slog_info!(&config.log, "Admin user created: {}", &new_admin.username); + Ok(()) +} + +fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> { + slog_info!( + config.log, + "Creating a database file and running the migrations:" + ); + let connection = queries::establish_connection(&config.db)?; + crate::embedded_migrations::run_with_output(&connection, &mut std::io::stdout())?; + Ok(()) +} + +fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> { + if std::path::Path::new(".env").exists() { + slog_error!( + server_config.log, + "ERROR: There already is a .env file - ABORT!" + ) + } else { + slog_info!( + server_config.log, + "Creating a .env file with default options" + ); + slog_info!( + server_config.log, + concat!( + "The SECRET_KEY variable is used for password encryption.", + "If it is changed all existing passwords are invalid." + ) + ); + let mut file = std::fs::File::create(".env")?; + + let conf_file_content = server_config.to_env_strings(); + + conf_file_content.iter().for_each(|l| { + file.write_all(l.as_bytes()) + .expect("failed to write .env file") + }); + slog_info!(server_config.log, "Successfully created the env file!") + } + Ok(()) +} diff --git a/src/forms.rs b/src/forms.rs index 3410cde..122b47f 100644 --- a/src/forms.rs +++ b/src/forms.rs @@ -1,6 +1,6 @@ use serde::Deserialize; #[derive(Deserialize, Debug)] -pub(crate) struct LinkForm { +pub struct LinkForm { pub title: String, pub target: String, pub code: String, diff --git a/src/main.rs b/src/main.rs index 9430fca..a9566ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,34 @@ #[macro_use] extern crate diesel; #[macro_use] -extern crate log; +extern crate diesel_migrations; +#[allow(unused_imports)] +#[macro_use( + slog_o, + slog_info, + slog_warn, + slog_error, + slog_log, + slog_record, + slog_record_static, + slog_b, + slog_kv +)] +extern crate slog; +extern crate slog_async; + +mod cli; mod forms; pub mod models; mod queries; pub mod schema; mod views; -use actix_identity::{CookieIdentityPolicy, IdentityService}; -use actix_web::middleware::Logger; -use actix_web::{web, App, HttpResponse, HttpServer}; -use diesel::prelude::*; +use std::{fmt::Display, path::PathBuf, str::FromStr}; + +use actix_identity::{CookieIdentityPolicy, IdentityService}; +use actix_web::{web, App, HttpResponse, HttpServer}; -use dotenv::dotenv; -use models::NewUser; use qrcode::types::QrError; use tera::Tera; @@ -22,31 +36,46 @@ use tera::Tera; pub enum ServerError { Argonautic, Diesel(diesel::result::Error), + Migration(diesel_migrations::RunMigrationsError), Environment, Template(tera::Error), Qr(QrError), + Io(std::io::Error), User(String), } impl std::fmt::Display for ServerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Test") + match self { + Self::Argonautic => write!(f, "Argonautica Error"), + Self::Diesel(e) => write!(f, "Diesel Error: {}", e), + Self::Environment => write!(f, "Environment Error"), + Self::Template(e) => write!(f, "Template Error: {:?}", e), + Self::Qr(e) => write!(f, "Qr Code Error: {:?}", e), + Self::Io(e) => write!(f, "IO Error: {:?}", e), + Self::Migration(e) => write!(f, "Migration Error: {:?}", e), + Self::User(data) => write!(f, "{}", data), + } } } impl actix_web::error::ResponseError for ServerError { fn error_response(&self) -> HttpResponse { match self { - Self::Argonautic => HttpResponse::InternalServerError().json("Argonautica Error."), + Self::Argonautic => HttpResponse::InternalServerError().json("Argonautica Error"), Self::Diesel(e) => { - HttpResponse::InternalServerError().json(format!("Diesel Error.{}", e)) + HttpResponse::InternalServerError().json(format!("Diesel Error: {:?}", e)) } - Self::Environment => HttpResponse::InternalServerError().json("Environment Error."), + Self::Environment => HttpResponse::InternalServerError().json("Environment Error"), Self::Template(e) => { - HttpResponse::InternalServerError().json(format!("Template Error. {:?}", e)) + HttpResponse::InternalServerError().json(format!("Template Error: {:?}", e)) } Self::Qr(e) => { - HttpResponse::InternalServerError().json(format!("Qr Code Error. {:?}", e)) + HttpResponse::InternalServerError().json(format!("Qr Code Error: {:?}", e)) + } + Self::Io(e) => HttpResponse::InternalServerError().json(format!("IO Error: {:?}", e)), + Self::Migration(e) => { + HttpResponse::InternalServerError().json(format!("Migration Error: {:?}", e)) } Self::User(data) => HttpResponse::InternalServerError().json(data), } @@ -55,94 +84,127 @@ impl actix_web::error::ResponseError for ServerError { impl From for ServerError { fn from(e: std::env::VarError) -> Self { - error!("Environment error {:?}", e); + eprintln!("Environment error {:?}", e); Self::Environment } } -/* impl From for ServerError { - fn from(_: r2d2::Error) -> ServerError { - ServerError::R2D2Error +impl From for ServerError { + fn from(e: diesel_migrations::RunMigrationsError) -> Self { + Self::Migration(e) } -} */ +} impl From for ServerError { fn from(err: diesel::result::Error) -> Self { - error!("Database error {:?}", err); + eprintln!("Database error {:?}", err); Self::Diesel(err) } } impl From for ServerError { fn from(e: argonautica::Error) -> Self { - error!("Authentication error {:?}", e); + eprintln!("Authentication error {:?}", e); Self::Argonautic } } impl From for ServerError { fn from(e: tera::Error) -> Self { - error!("Template error {:?}", e); + eprintln!("Template error {:?}", e); Self::Template(e) } } impl From for ServerError { fn from(e: QrError) -> Self { - error!("Template error {:?}", e); + eprintln!("Template error {:?}", e); Self::Qr(e) } } +impl From for ServerError { + fn from(e: std::io::Error) -> Self { + eprintln!("IO error {:?}", e); + Self::Io(e) + } +} + +#[derive(Debug, Clone)] +enum Protocol { + Http, + Https, +} + +impl Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Http => f.write_str("http"), + Self::Https => f.write_str("https"), + } + } +} + +impl FromStr for Protocol { + type Err = ServerError; + + fn from_str(s: &str) -> Result { + match s { + "http" => Ok(Self::Http), + "https" => Ok(Self::Https), + _ => Err(ServerError::User("Failed to parse Protocol".to_owned())), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct ServerConfig { + secret: String, + db: PathBuf, + public_url: String, + internal_ip: String, + port: u32, + protocol: Protocol, + log: slog::Logger, +} + +impl ServerConfig { + pub fn to_env_strings(&self) -> Vec { + vec![ + format!("PSLINK_DATABASE=\"{}\"\n", self.db.display()), + format!("PSLINK_PORT={}\n", self.port), + format!("PSLINK_PUBLIC_URL=\"{}\"\n", self.public_url), + format!("PSLINK_IP=\"{}\"\n", self.internal_ip), + format!("PSLINK_PROTOCOL=\"{}\"\n", self.protocol), + concat!( + "# The SECRET_KEY variable is used for password encryption.\n", + "# If it is changed all existing passwords are invalid.\n" + ) + .to_owned(), + format!("PSLINK_SECRET=\"{}\"\n", self.secret), + ] + } +} include!(concat!(env!("OUT_DIR"), "/generated.rs")); +embed_migrations!("migrations/"); -#[actix_web::main] -async fn main() -> std::io::Result<()> { - dotenv().ok(); - env_logger::init(); +#[allow(clippy::future_not_send)] +async fn webservice(server_config: ServerConfig) -> std::io::Result<()> { + let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port); - let connection = queries::establish_connection().expect("Failed to connect to database!"); - let num_users: i64 = schema::users::dsl::users - .select(diesel::dsl::count_star()) - .first(&connection) - .expect("Failed to count the users"); + slog_info!( + server_config.log, + "Running on: {}://{}/admin/login/", + &server_config.protocol, + host_port + ); - if num_users < 1 { - // It is ok to use expect in this block since it is only run on the start. And if something fails it is probably something major. - use schema::users; - use std::io::{self, BufRead, Write}; - warn!("No usere available Creating one!"); - let sin = io::stdin(); - - print!("Please enter the Username of the admin: "); - io::stdout().flush().unwrap(); - let username = sin.lock().lines().next().unwrap().unwrap(); - - print!("Please enter the emailadress for {}: ", username); - io::stdout().flush().unwrap(); - let email = sin.lock().lines().next().unwrap().unwrap(); - - print!("Please enter the password for {}: ", username); - io::stdout().flush().unwrap(); - let password = sin.lock().lines().next().unwrap().unwrap(); - println!( - "Creating {} ({}) with password {}", - &username, &email, &password - ); - - let new_admin = - NewUser::new(username, email, password).expect("Invalid Input failed to create User"); - - diesel::insert_into(users::table) - .values(&new_admin) - .execute(&connection) - .expect("Failed to create the user!"); - } - - println!("Running on: http://127.0.0.1:8156/admin/login/"); - HttpServer::new(|| { + HttpServer::new(move || { let tera = Tera::new("templates/**/*").expect("failed to initialize the templates"); let generated = generate(); App::new() - .wrap(Logger::default()) + .data(server_config.clone()) + .wrap(actix_slog::StructuredLogger::new( + server_config.log.new(slog_o!("log_type" => "access")), + )) .wrap(IdentityService::new( CookieIdentityPolicy::new(&[0; 32]) .name("auth-cookie") @@ -218,7 +280,23 @@ async fn main() -> std::io::Result<()> { // redirect to the url hidden behind the code .route("/{redirect_id}", web::get().to(views::redirect)) }) - .bind("127.0.0.1:8156")? + .bind(host_port)? .run() .await } + +#[actix_web::main] +async fn main() -> Result<(), std::io::Error> { + match cli::setup() { + Ok(Some(server_config)) => webservice(server_config).await, + Ok(None) => { + std::thread::sleep(std::time::Duration::from_millis(100)); + std::process::exit(0); + } + Err(e) => { + eprintln!("\nError: {}", e); + std::thread::sleep(std::time::Duration::from_millis(100)); + std::process::exit(1); + } + } +} diff --git a/src/models.rs b/src/models.rs index 2291182..3417376 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use crate::{forms::LinkForm, ServerError}; +use crate::{forms::LinkForm, ServerConfig, ServerError}; use super::schema::{clicks, links, users}; use argonautica::Hasher; @@ -27,26 +27,29 @@ impl NewUser { pub(crate) fn new( username: String, email: String, - password: String, + password: &str, + config: &ServerConfig, ) -> Result { - let hash = Self::hash_password(password)?; - dotenv().ok(); + let hash = Self::hash_password(password, config)?; - Ok(NewUser { + Ok(Self { username, email, password: hash, }) } - pub(crate) fn hash_password(password: String) -> Result { + pub(crate) fn hash_password( + password: &str, + config: &ServerConfig, + ) -> Result { dotenv().ok(); - let secret = std::env::var("SECRET_KEY")?; + let secret = &config.secret; let hash = Hasher::default() - .with_password(&password) - .with_secret_key(&secret) + .with_password(password) + .with_secret_key(secret) .hash()?; Ok(hash) @@ -106,6 +109,7 @@ pub struct NewClick { } impl NewClick { + #[must_use] pub fn new(link_id: i32) -> Self { Self { link: link_id, diff --git a/src/queries.rs b/src/queries.rs index cc52e6a..1b0490c 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -1,26 +1,27 @@ +use std::path::Path; + use actix_identity::Identity; use actix_web::web; use diesel::{prelude::*, sqlite::SqliteConnection}; -use dotenv::dotenv; use serde::Serialize; use super::models::{Count, Link, NewUser, User}; use crate::{ forms::LinkForm, models::{NewClick, NewLink}, - ServerError, + ServerConfig, ServerError, }; /// Create a connection to the database -pub(super) fn establish_connection() -> Result { - dotenv().ok(); - - let database_url = std::env::var("DATABASE_URL")?; - - match SqliteConnection::establish(&database_url) { +pub(super) fn establish_connection(database_url: &Path) -> Result { + match SqliteConnection::establish(&database_url.display().to_string()) { Ok(c) => Ok(c), Err(e) => { - info!("Error connecting to database: {}, {}", database_url, e); + eprintln!( + "Error connecting to database: {}, {}", + database_url.display(), + e + ); Err(ServerError::User( "Error connecting to Database".to_string(), )) @@ -39,7 +40,7 @@ pub enum Role { impl Role { /// Determin if the user is admin or the given user id is his own. This is used for things where users can edit or view their own entries, whereas admins can do so for all entries. - fn admin_or_self(&self, id: i32) -> bool { + const fn admin_or_self(&self, id: i32) -> bool { match self { Self::Admin { .. } => true, Self::Regular { user } => user.id == id, @@ -49,10 +50,13 @@ impl Role { } /// queries the user matching the given [`actix_identity::Identity`] and determins its authentication and permission level. Returns a [`Role`] containing the user if it is authenticated. -pub fn authenticate(id: Identity) -> Result { +pub(crate) fn authenticate( + id: &Identity, + server_config: &ServerConfig, +) -> Result { if let Some(username) = id.identity() { use super::schema::users::dsl; - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let user = dsl::users .filter(dsl::username.eq(&username)) @@ -83,7 +87,10 @@ pub struct FullLink { } /// Returns a List of `FullLink` meaning `Links` enriched by their author and statistics. This returns all links if the user is either Admin or Regular user. -pub(crate) fn list_all_allowed(id: Identity) -> Result, ServerError> { +pub(crate) fn list_all_allowed( + id: &Identity, + server_config: &ServerConfig, +) -> Result, ServerError> { use super::schema::clicks; use super::schema::links; use super::schema::users; @@ -113,10 +120,10 @@ pub(crate) fn list_all_allowed(id: Identity) -> Result, ServerErr "COUNT(clicks.id)", ),), )); - match authenticate(id)? { + match authenticate(id, server_config)? { Role::Admin { user } | Role::Regular { user } => { // show all links - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let all_links: Vec = query .load(&connection)? .into_iter() @@ -136,11 +143,14 @@ pub(crate) fn list_all_allowed(id: Identity) -> Result, ServerErr } /// Only admins can list all users -pub(crate) fn list_users(id: Identity) -> Result, ServerError> { +pub(crate) fn list_users( + id: &Identity, + server_config: &ServerConfig, +) -> Result, ServerError> { use super::schema::users::dsl::users; - match authenticate(id)? { + match authenticate(id, server_config)? { Role::Admin { user } => { - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let all_users: Vec = users.load(&connection)?; Ok(List { user, @@ -160,16 +170,20 @@ pub struct Item { } /// Get a user if permissions are accordingly -pub(crate) fn get_user(id: Identity, user_id: &str) -> Result, ServerError> { +pub(crate) fn get_user( + id: &Identity, + user_id: &str, + server_config: &ServerConfig, +) -> Result, ServerError> { use super::schema::users; if let Ok(uid) = user_id.parse::() { - info!("Getting user {}", uid); - let auth = authenticate(id)?; - info!("{:?}", &auth); + slog_info!(server_config.log, "Getting user {}", uid); + let auth = authenticate(id, server_config)?; + slog_info!(server_config.log, "{:?}", &auth); if auth.admin_or_self(uid) { match auth { Role::Admin { user } | Role::Regular { user } => { - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let viewed_user = users::dsl::users .filter(users::dsl::id.eq(&uid)) .first::(&connection)?; @@ -191,10 +205,13 @@ pub(crate) fn get_user(id: Identity, user_id: &str) -> Result, Server } /// Get a user **without permission checks** (needed for login) -pub(crate) fn get_user_by_name(username: &str) -> Result { +pub(crate) fn get_user_by_name( + username: &str, + server_config: &ServerConfig, +) -> Result { use super::schema::users; - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let user = users::dsl::users .filter(users::dsl::username.eq(username)) .first::(&connection)?; @@ -202,27 +219,29 @@ pub(crate) fn get_user_by_name(username: &str) -> Result { } pub(crate) fn create_user( - id: Identity, - data: web::Form, + id: &Identity, + data: &web::Form, + server_config: &ServerConfig, ) -> Result, ServerError> { - info!("Creating a User: {:?}", &data); - let auth = authenticate(id)?; + slog_info!(server_config.log, "Creating a User: {:?}", &data); + let auth = authenticate(id, server_config)?; match auth { Role::Admin { user } => { use super::schema::users; - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let new_user = NewUser::new( data.username.clone(), data.email.clone(), - data.password.clone(), + &data.password, + server_config, )?; diesel::insert_into(users::table) .values(&new_user) .execute(&connection)?; - let new_user = get_user_by_name(&data.username)?; + let new_user = get_user_by_name(&data.username, server_config)?; Ok(Item { user, item: new_user, @@ -235,21 +254,22 @@ pub(crate) fn create_user( } /// Take a [`actix_web::web::Form`] and update the corresponding entry in the database. /// The password is only updated if a new password of at least 4 characters is provided. -/// The user_id is never changed. +/// The `user_id` is never changed. pub(crate) fn update_user( - id: Identity, + id: &Identity, user_id: &str, - data: web::Form, + server_config: &ServerConfig, + data: &web::Form, ) -> Result, ServerError> { if let Ok(uid) = user_id.parse::() { - let auth = authenticate(id)?; + let auth = authenticate(id, server_config)?; if auth.admin_or_self(uid) { match auth { Role::Admin { .. } | Role::Regular { .. } => { use super::schema::users::dsl::{email, id, password, username, users}; - info!("Updating userinfo: "); - let connection = establish_connection()?; + slog_info!(server_config.log, "Updating userinfo: "); + let connection = establish_connection(&server_config.db)?; // Update username and email - if they have not been changed their values will be replaced by the old ones. diesel::update(users.filter(id.eq(&uid))) @@ -260,7 +280,7 @@ pub(crate) fn update_user( .execute(&connection)?; // Update the password only if the user entered something. if data.password.len() > 3 { - let hash = NewUser::hash_password(data.password.clone())?; + let hash = NewUser::hash_password(&data.password, server_config)?; diesel::update(users.filter(id.eq(&uid))) .set((password.eq(hash),)) .execute(&connection)?; @@ -283,25 +303,31 @@ pub(crate) fn update_user( } } -pub(crate) fn toggle_admin(id: Identity, user_id: &str) -> Result, ServerError> { +pub(crate) fn toggle_admin( + id: &Identity, + user_id: &str, + server_config: &ServerConfig, +) -> Result, ServerError> { if let Ok(uid) = user_id.parse::() { - let auth = authenticate(id)?; + let auth = authenticate(id, server_config)?; match auth { Role::Admin { .. } => { use super::schema::users::dsl::{id, role, users}; - info!("Changing administrator priviledges: "); - let connection = establish_connection()?; + slog_info!(server_config.log, "Changing administrator priviledges: "); + let connection = establish_connection(&server_config.db)?; let unchanged_user = users.filter(id.eq(&uid)).first::(&connection)?; let new_role = 2 - (unchanged_user.role + 1) % 2; - info!( + slog_info!( + server_config.log, "Assigning new role: {} - old was {}", - new_role, unchanged_user.role + new_role, + unchanged_user.role ); - // Update username and email - if they have not been changed their values will be replaced by the old ones. + // Update the role eg. admin vs. normal vs. disabled diesel::update(users.filter(id.eq(&uid))) .set((role.eq(new_role),)) .execute(&connection)?; @@ -322,11 +348,15 @@ pub(crate) fn toggle_admin(id: Identity, user_id: &str) -> Result, Se } /// Get one link if permissions are accordingly. -pub(crate) fn get_link(id: Identity, link_code: &str) -> Result, ServerError> { +pub(crate) fn get_link( + id: &Identity, + link_code: &str, + server_config: &ServerConfig, +) -> Result, ServerError> { use super::schema::links::dsl::{code, links}; - match authenticate(id)? { + match authenticate(id, server_config)? { Role::Admin { user } | Role::Regular { user } => { - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let link: Link = links .filter(code.eq(&link_code)) .first::(&connection)?; @@ -337,20 +367,23 @@ pub(crate) fn get_link(id: Identity, link_code: &str) -> Result, Serv } /// Get link **without authentication** -pub(crate) fn get_link_simple(link_code: &str) -> Result { +pub(crate) fn get_link_simple( + link_code: &str, + server_config: &ServerConfig, +) -> Result { use super::schema::links::dsl::{code, links}; - info!("Getting link for {:?}", link_code); - let connection = establish_connection()?; + slog_info!(server_config.log, "Getting link for {:?}", link_code); + let connection = establish_connection(&server_config.db)?; let link: Link = links .filter(code.eq(&link_code)) .first::(&connection)?; Ok(link) } /// Click on a link -pub(crate) fn click_link(link_id: i32) -> Result<(), ServerError> { +pub(crate) fn click_link(link_id: i32, server_config: &ServerConfig) -> Result<(), ServerError> { use super::schema::clicks; let new_click = NewClick::new(link_id); - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; diesel::insert_into(clicks::table) .values(&new_click) @@ -359,11 +392,15 @@ pub(crate) fn click_link(link_id: i32) -> Result<(), ServerError> { } /// Click on a link -pub(crate) fn delete_link(id: Identity, link_code: String) -> Result<(), ServerError> { +pub(crate) fn delete_link( + id: &Identity, + link_code: &str, + server_config: &ServerConfig, +) -> Result<(), ServerError> { use super::schema::links::dsl::{code, links}; - let connection = establish_connection()?; - let auth = authenticate(id)?; - let link = get_link_simple(&link_code)?; + let connection = establish_connection(&server_config.db)?; + let auth = authenticate(id, server_config)?; + let link = get_link_simple(link_code, server_config)?; if auth.admin_or_self(link.author) { diesel::delete(links.filter(code.eq(&link_code))).execute(&connection)?; Ok(()) @@ -373,18 +410,24 @@ pub(crate) fn delete_link(id: Identity, link_code: String) -> Result<(), ServerE } /// Update a link if the user is admin or it is its own link. pub(crate) fn update_link( - id: Identity, + id: &Identity, link_code: &str, - data: web::Form, + data: &web::Form, + server_config: &ServerConfig, ) -> Result, ServerError> { use super::schema::links::dsl::{code, links, target, title}; - info!("Changing link to: {:?} {:?}", &data, &link_code); - let auth = authenticate(id.clone())?; - match auth.clone() { + slog_info!( + server_config.log, + "Changing link to: {:?} {:?}", + &data, + &link_code + ); + let auth = authenticate(id, server_config)?; + match auth { Role::Admin { .. } | Role::Regular { .. } => { - let query = get_link(id.clone(), &link_code)?; + let query = get_link(id, link_code, server_config)?; if auth.admin_or_self(query.item.author) { - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; diesel::update(links.filter(code.eq(&query.item.code))) .set(( code.eq(&data.code), @@ -392,7 +435,7 @@ pub(crate) fn update_link( title.eq(&data.title), )) .execute(&connection)?; - get_link(id, &data.code) + get_link(id, &data.code, server_config) } else { Err(ServerError::User("Not Allowed".to_owned())) } @@ -402,21 +445,22 @@ pub(crate) fn update_link( } pub(crate) fn create_link( - id: Identity, + id: &Identity, data: web::Form, + server_config: &ServerConfig, ) -> Result, ServerError> { - let auth = authenticate(id)?; + let auth = authenticate(id, server_config)?; match auth { Role::Admin { user } | Role::Regular { user } => { use super::schema::links; - let connection = establish_connection()?; + let connection = establish_connection(&server_config.db)?; let new_link = NewLink::from_link_form(data.into_inner(), user.id); diesel::insert_into(links::table) .values(&new_link) .execute(&connection)?; - let new_link = get_link_simple(&new_link.code)?; + let new_link = get_link_simple(&new_link.code, server_config)?; Ok(Item { user, item: new_link, diff --git a/src/views.rs b/src/views.rs index 1fe329c..a89756e 100644 --- a/src/views.rs +++ b/src/views.rs @@ -6,7 +6,6 @@ use actix_web::{ web, HttpResponse, }; use argonautica::Verifier; -use dotenv::dotenv; use image::{DynamicImage, ImageOutputFormat, Luma}; use qrcode::{render::svg, QrCode}; use tera::{Context, Tera}; @@ -24,16 +23,17 @@ fn redirect_builder(target: &str) -> HttpResponse { CacheDirective::MustRevalidate, ])) .set(Expires(SystemTime::now().into())) - .set_header(actix_web::http::header::LOCATION, target.clone()) - .body(format!("Redirect to {}", target.clone())) + .set_header(actix_web::http::header::LOCATION, target) + .body(format!("Redirect to {}", target)) } /// Show the list of all available links if a user is authenticated pub(crate) async fn index( tera: web::Data, + config: web::Data, id: Identity, ) -> Result { - if let Ok(links) = queries::list_all_allowed(id) { + if let Ok(links) = queries::list_all_allowed(&id, &config) { let mut data = Context::new(); data.insert("user", &links.user); data.insert("title", "Links der Freien Hochschule Stuttgart"); @@ -48,9 +48,10 @@ pub(crate) async fn index( /// Show the list of all available links if a user is authenticated pub(crate) async fn index_users( tera: web::Data, + config: web::Data, id: Identity, ) -> Result { - if let Ok(users) = queries::list_users(id) { + if let Ok(users) = queries::list_users(&id, &config) { let mut data = Context::new(); data.insert("user", &users.user); data.insert("title", "Benutzer der Freien Hochschule Stuttgart"); @@ -64,28 +65,29 @@ pub(crate) async fn index_users( } pub(crate) async fn view_link_fhs( tera: web::Data, + config: web::Data, id: Identity, ) -> Result { - view_link(tera, id, web::Path::from("".to_owned())).await + view_link(tera, config, id, web::Path::from("".to_owned())).await } pub(crate) async fn view_link( tera: web::Data, + config: web::Data, id: Identity, link_id: web::Path, ) -> Result { - if let Ok(link) = queries::get_link(id, &link_id.0) { - dotenv().ok(); - let host = std::env::var("SLINK_HOST")?; - let protocol = std::env::var("SLINK_PROTOCOL")?; + if let Ok(link) = queries::get_link(&id, &link_id.0, &config) { + let host = config.public_url.to_string(); + let protocol = config.protocol.to_string(); let qr = QrCode::with_error_correction_level( - &format!("http://{}/{}", &host, &link.item.id), + &format!("http://{}/{}", &host, &link.item.code), qrcode::EcLevel::L, )?; let svg = qr .render() - .min_dimensions(200, 200) + .min_dimensions(100, 100) .dark_color(svg::Color("#000000")) .light_color(svg::Color("#ffffff")) .build(); @@ -94,7 +96,7 @@ pub(crate) async fn view_link( data.insert("user", &link.user); data.insert( "title", - &format!("Links {} der Freien Hochschule Stuttgart", &link.item.id), + &format!("Links {} der Freien Hochschule Stuttgart", &link.item.code), ); data.insert("link", &link.item); data.insert("qr", &svg); @@ -110,11 +112,12 @@ pub(crate) async fn view_link( pub(crate) async fn view_profile( tera: web::Data, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { - info!("Viewing Profile!"); - if let Ok(query) = queries::get_user(id, &user_id.0) { + slog_info!(config.log, "Viewing Profile!"); + if let Ok(query) = queries::get_user(&id, &user_id.0, &config) { let mut data = Context::new(); data.insert("user", &query.user); data.insert( @@ -136,11 +139,12 @@ pub(crate) async fn view_profile( pub(crate) async fn edit_profile( tera: web::Data, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { - info!("Editing Profile!"); - if let Ok(query) = queries::get_user(id, &user_id.0) { + slog_info!(config.log, "Editing Profile!"); + if let Ok(query) = queries::get_user(&id, &user_id.0, &config) { let mut data = Context::new(); data.insert("user", &query.user); data.insert( @@ -161,10 +165,11 @@ pub(crate) async fn edit_profile( pub(crate) async fn process_edit_profile( data: web::Form, + config: web::Data, id: Identity, user_id: web::Path, ) -> Result { - if let Ok(query) = queries::update_user(id, &user_id.0, data) { + if let Ok(query) = queries::update_user(&id, &user_id.0, &config, &data) { Ok(redirect_builder(&format!( "admin/view/profile/{}", query.user.username @@ -176,17 +181,13 @@ pub(crate) async fn process_edit_profile( pub(crate) async fn download_png( id: Identity, + config: web::Data, link_code: web::Path, ) -> Result { - match queries::get_link(id, &link_code.0) { + match queries::get_link(&id, &link_code.0, &config) { Ok(query) => { - dotenv().ok(); let qr = QrCode::with_error_correction_level( - &format!( - "http://{}/{}", - std::env::var("SLINK_HOST")?, - &query.item.code - ), + &format!("http://{}/{}", config.public_url, &query.item.code), qrcode::EcLevel::L, ) .unwrap(); @@ -204,9 +205,10 @@ pub(crate) async fn download_png( pub(crate) async fn signup( tera: web::Data, + config: web::Data, id: Identity, ) -> Result { - match queries::authenticate(id)? { + match queries::authenticate(&id, &config)? { queries::Role::Admin { user } => { let mut data = Context::new(); data.insert("title", "Ein Benutzerkonto erstellen"); @@ -223,10 +225,11 @@ pub(crate) async fn signup( pub(crate) async fn process_signup( data: web::Form, + config: web::Data, id: Identity, ) -> Result { - info!("Creating a User: {:?}", &data); - if let Ok(item) = queries::create_user(id, data) { + slog_info!(config.log, "Creating a User: {:?}", &data); + if let Ok(item) = queries::create_user(&id, &data, &config) { Ok(HttpResponse::Ok().body(format!("Successfully saved user: {}", item.item.username))) } else { Ok(redirect_builder("/admin/login/")) @@ -235,9 +238,10 @@ pub(crate) async fn process_signup( pub(crate) async fn toggle_admin( data: web::Path, + config: web::Data, id: Identity, ) -> Result { - let update = queries::toggle_admin(id, &data.0)?; + let update = queries::toggle_admin(&id, &data.0, &config)?; Ok(redirect_builder(&format!( "/admin/view/profile/{}", update.item.id @@ -261,22 +265,22 @@ pub(crate) async fn login( pub(crate) async fn process_login( data: web::Form, + config: web::Data, id: Identity, ) -> Result { - let user = queries::get_user_by_name(&data.username); + let user = queries::get_user_by_name(&data.username, &config); match user { Ok(u) => { - dotenv().ok(); - let secret = std::env::var("SECRET_KEY")?; + let secret = &config.secret; let valid = Verifier::default() .with_hash(&u.password) .with_password(&data.password) - .with_secret_key(&secret) + .with_secret_key(secret) .verify()?; if valid { - info!("Log-in of user: {}", &u.username); + slog_info!(config.log, "Log-in of user: {}", &u.username); let session_token = u.username; id.remember(session_token); Ok(redirect_builder("/admin/index/")) @@ -285,7 +289,7 @@ pub(crate) async fn process_login( } } Err(e) => { - info!("Failed to login: {}", e); + slog_info!(config.log, "Failed to login: {}", e); Ok(redirect_builder("/admin/login/")) } } @@ -298,30 +302,31 @@ pub(crate) async fn logout(id: Identity) -> Result { pub(crate) async fn redirect( tera: web::Data, + config: web::Data, data: web::Path, ) -> Result { - info!("Redirecting to {:?}", data); - let link = queries::get_link_simple(&data.0); - info!("link: {:?}", link); + slog_info!(config.log, "Redirecting to {:?}", data); + let link = queries::get_link_simple(&data.0, &config); + slog_info!(config.log, "link: {:?}", link); match link { Ok(link) => { - queries::click_link(link.id)?; + queries::click_link(link.id, &config)?; Ok(redirect_builder(&link.target)) } Err(ServerError::Diesel(e)) => { - dotenv().ok(); - info!( + slog_info!( + config.log, "Link was not found: http://{}/{} \n {}", - std::env::var("SLINK_HOST")?, + &config.public_url, &data.0, e ); let mut data = Context::new(); - data.insert("title", "Wurde gelöscht"); + data.insert("title", "Wurde gel\u{f6}scht"); let rendered = tera.render("not_found.html", &data)?; Ok(HttpResponse::NotFound().body(rendered)) } - Err(e) => Err(e.into()), + Err(e) => Err(e), } } @@ -333,9 +338,10 @@ pub(crate) async fn redirect_fhs() -> Result { pub(crate) async fn create_link( tera: web::Data, + config: web::Data, id: Identity, ) -> Result { - match queries::authenticate(id)? { + match queries::authenticate(&id, &config)? { queries::Role::Admin { user } | queries::Role::Regular { user } => { let mut data = Context::new(); data.insert("title", "Einen Kurzlink erstellen"); @@ -352,9 +358,10 @@ pub(crate) async fn create_link( pub(crate) async fn process_link_creation( data: web::Form, + config: web::Data, id: Identity, ) -> Result { - let new_link = queries::create_link(id, data)?; + let new_link = queries::create_link(&id, data, &config)?; Ok(redirect_builder(&format!( "/admin/view/link/{}", new_link.item.code @@ -363,10 +370,11 @@ pub(crate) async fn process_link_creation( pub(crate) async fn edit_link( tera: web::Data, + config: web::Data, id: Identity, link_id: web::Path, ) -> Result { - if let Ok(query) = queries::get_link(id, &link_id.0) { + if let Ok(query) = queries::get_link(&id, &link_id.0, &config) { let mut data = Context::new(); data.insert("title", "Submit a Post"); data.insert("link", &query.item); @@ -379,10 +387,11 @@ pub(crate) async fn edit_link( } pub(crate) async fn process_link_edit( data: web::Form, + config: web::Data, id: Identity, link_code: web::Path, ) -> Result { - match queries::update_link(id, &link_code.0, data) { + match queries::update_link(&id, &link_code.0, &data, &config) { Ok(query) => Ok(redirect_builder(&format!( "/admin/view/link/{}", &query.item.code @@ -393,8 +402,9 @@ pub(crate) async fn process_link_edit( pub(crate) async fn process_link_delete( id: Identity, + config: web::Data, link_code: web::Path, ) -> Result { - queries::delete_link(id, link_code.0)?; + queries::delete_link(&id, &link_code.0, &config)?; Ok(redirect_builder("/admin/login/")) }