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 <dietrich@teilgedanken.de>
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 <database>              The path of the sqlite database [env: PSLINK_DATABASE=]  [default: links.db]
    -i, --hostip <internal_ip>       The host (ip) that will run the pslink service [env: PSLINK_IP=]  [default:
                                     localhost]
    -p, --port <port>                The port the pslink service will run on [env: PSLINK_PORT=]  [default: 8080]
    -t, --protocol <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 <public_url>    The host url or the page that will be part of the short urls. [env:
                                     PSLINK_PUBLIC_URL=]  [default: localhost:8080]
        --secret <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)
```
This commit is contained in:
Dietrich 2021-03-07 19:14:34 +01:00
parent 0553aaa935
commit 9512807eb3
Signed by: dietrich
GPG Key ID: 9F3C20C0F85DF67C
10 changed files with 901 additions and 262 deletions

252
Cargo.lock generated
View File

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

View File

@ -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 <dietrich@teilgedanken.de>"]
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"
actix-web-static-files = { git = "https://github.com/enaut/actix-web-static-files.git", branch = "enaut-must_use" }

0
README.md Normal file
View File

View File

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

340
src/cli.rs Normal file
View File

@ -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::<PathBuf>()
.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::<u32>()
.expect("Failed to parse the portnumber");
let protocol = config
.value_of("protocol")
.expect("Failed to read the protocol value")
.parse::<crate::Protocol>()
.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<Option<crate::ServerConfig>, 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(())
}

View File

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

View File

@ -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<std::env::VarError> for ServerError {
fn from(e: std::env::VarError) -> Self {
error!("Environment error {:?}", e);
eprintln!("Environment error {:?}", e);
Self::Environment
}
}
/* impl From<r2d2::Error> for ServerError {
fn from(_: r2d2::Error) -> ServerError {
ServerError::R2D2Error
impl From<diesel_migrations::RunMigrationsError> for ServerError {
fn from(e: diesel_migrations::RunMigrationsError) -> Self {
Self::Migration(e)
}
} */
}
impl From<diesel::result::Error> for ServerError {
fn from(err: diesel::result::Error) -> Self {
error!("Database error {:?}", err);
eprintln!("Database error {:?}", err);
Self::Diesel(err)
}
}
impl From<argonautica::Error> for ServerError {
fn from(e: argonautica::Error) -> Self {
error!("Authentication error {:?}", e);
eprintln!("Authentication error {:?}", e);
Self::Argonautic
}
}
impl From<tera::Error> for ServerError {
fn from(e: tera::Error) -> Self {
error!("Template error {:?}", e);
eprintln!("Template error {:?}", e);
Self::Template(e)
}
}
impl From<QrError> for ServerError {
fn from(e: QrError) -> Self {
error!("Template error {:?}", e);
eprintln!("Template error {:?}", e);
Self::Qr(e)
}
}
impl From<std::io::Error> 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<Self, Self::Err> {
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<String> {
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);
}
}
}

View File

@ -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<Self, ServerError> {
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<String, ServerError> {
pub(crate) fn hash_password(
password: &str,
config: &ServerConfig,
) -> Result<String, ServerError> {
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,

View File

@ -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<SqliteConnection, ServerError> {
dotenv().ok();
let database_url = std::env::var("DATABASE_URL")?;
match SqliteConnection::establish(&database_url) {
pub(super) fn establish_connection(database_url: &Path) -> Result<SqliteConnection, ServerError> {
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<Role, ServerError> {
pub(crate) fn authenticate(
id: &Identity,
server_config: &ServerConfig,
) -> Result<Role, ServerError> {
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<List<FullLink>, ServerError> {
pub(crate) fn list_all_allowed(
id: &Identity,
server_config: &ServerConfig,
) -> Result<List<FullLink>, 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<List<FullLink>, 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<FullLink> = query
.load(&connection)?
.into_iter()
@ -136,11 +143,14 @@ pub(crate) fn list_all_allowed(id: Identity) -> Result<List<FullLink>, ServerErr
}
/// Only admins can list all users
pub(crate) fn list_users(id: Identity) -> Result<List<User>, ServerError> {
pub(crate) fn list_users(
id: &Identity,
server_config: &ServerConfig,
) -> Result<List<User>, 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<User> = users.load(&connection)?;
Ok(List {
user,
@ -160,16 +170,20 @@ pub struct Item<T> {
}
/// Get a user if permissions are accordingly
pub(crate) fn get_user(id: Identity, user_id: &str) -> Result<Item<User>, ServerError> {
pub(crate) fn get_user(
id: &Identity,
user_id: &str,
server_config: &ServerConfig,
) -> Result<Item<User>, ServerError> {
use super::schema::users;
if let Ok(uid) = user_id.parse::<i32>() {
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::<User>(&connection)?;
@ -191,10 +205,13 @@ pub(crate) fn get_user(id: Identity, user_id: &str) -> Result<Item<User>, Server
}
/// Get a user **without permission checks** (needed for login)
pub(crate) fn get_user_by_name(username: &str) -> Result<User, ServerError> {
pub(crate) fn get_user_by_name(
username: &str,
server_config: &ServerConfig,
) -> Result<User, ServerError> {
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::<User>(&connection)?;
@ -202,27 +219,29 @@ pub(crate) fn get_user_by_name(username: &str) -> Result<User, ServerError> {
}
pub(crate) fn create_user(
id: Identity,
data: web::Form<NewUser>,
id: &Identity,
data: &web::Form<NewUser>,
server_config: &ServerConfig,
) -> Result<Item<User>, 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<NewUser>`] 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<NewUser>,
server_config: &ServerConfig,
data: &web::Form<NewUser>,
) -> Result<Item<User>, ServerError> {
if let Ok(uid) = user_id.parse::<i32>() {
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<Item<User>, ServerError> {
pub(crate) fn toggle_admin(
id: &Identity,
user_id: &str,
server_config: &ServerConfig,
) -> Result<Item<User>, ServerError> {
if let Ok(uid) = user_id.parse::<i32>() {
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::<User>(&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<Item<User>, Se
}
/// Get one link if permissions are accordingly.
pub(crate) fn get_link(id: Identity, link_code: &str) -> Result<Item<Link>, ServerError> {
pub(crate) fn get_link(
id: &Identity,
link_code: &str,
server_config: &ServerConfig,
) -> Result<Item<Link>, 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::<Link>(&connection)?;
@ -337,20 +367,23 @@ pub(crate) fn get_link(id: Identity, link_code: &str) -> Result<Item<Link>, Serv
}
/// Get link **without authentication**
pub(crate) fn get_link_simple(link_code: &str) -> Result<Link, ServerError> {
pub(crate) fn get_link_simple(
link_code: &str,
server_config: &ServerConfig,
) -> Result<Link, ServerError> {
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::<Link>(&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<LinkForm>,
data: &web::Form<LinkForm>,
server_config: &ServerConfig,
) -> Result<Item<Link>, 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<LinkForm>,
server_config: &ServerConfig,
) -> Result<Item<Link>, 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,

View File

@ -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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<NewUser>,
config: web::Data<crate::ServerConfig>,
id: Identity,
user_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<crate::ServerConfig>,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<NewUser>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<String>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<LoginUser>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<HttpResponse, ServerError> {
pub(crate) async fn redirect(
tera: web::Data<Tera>,
config: web::Data<crate::ServerConfig>,
data: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<HttpResponse, ServerError> {
pub(crate) async fn create_link(
tera: web::Data<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<LinkForm>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
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<Tera>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<LinkForm>,
config: web::Data<crate::ServerConfig>,
id: Identity,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<crate::ServerConfig>,
link_code: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
queries::delete_link(id, link_code.0)?;
queries::delete_link(&id, &link_code.0, &config)?;
Ok(redirect_builder("/admin/login/"))
}