Compare commits
11 Commits
ea75e1e3ee
...
1611cfb9a2
Author | SHA1 | Date | |
---|---|---|---|
1611cfb9a2 | |||
680d28ed58 | |||
b33088057d | |||
84625939de | |||
04170079d6 | |||
7690d301f1 | |||
322c867e94 | |||
ce315c429c | |||
6fd36936a3 | |||
ac172670be | |||
a3b757abad |
22
.github/workflows/rust.yml
vendored
Normal file
22
.github/workflows/rust.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
682
Cargo.lock
generated
682
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
98
Cargo.toml
98
Cargo.toml
@ -1,47 +1,73 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pslink"
|
|
||||||
version = "0.3.1"
|
|
||||||
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>"]
|
authors = ["Dietrich <dietrich@teilgedanken.de>"]
|
||||||
edition = "2018"
|
build = "build.rs"
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["url", "link", "webpage", "actix", "web"]
|
|
||||||
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
||||||
|
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."
|
||||||
|
edition = "2018"
|
||||||
|
keywords = ["url", "link", "webpage", "actix", "web"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
name = "pslink"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/enaut/pslink/"
|
repository = "https://github.com/enaut/pslink/"
|
||||||
build = "build.rs"
|
version = "0.3.1"
|
||||||
|
|
||||||
|
|
||||||
# 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.0"
|
|
||||||
actix-slog = "0.2"
|
|
||||||
tera = "1.6"
|
|
||||||
serde = "1.0"
|
|
||||||
sqlx={version="0.4", features = [ "sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline" ]}
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
actix-identity = "0.3"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
argonautica = "0.2"
|
|
||||||
slog = { version = "2", features = ["max_level_trace", "release_max_level_info"] }
|
|
||||||
slog-term = "2"
|
|
||||||
slog-async = "2"
|
|
||||||
qrcode = "0.12"
|
|
||||||
image = "0.23"
|
|
||||||
rand="0.8"
|
|
||||||
rpassword = "5.0"
|
|
||||||
clap = "2.33"
|
|
||||||
fluent-templates = { version = "0.6", features = ["tera"] }
|
|
||||||
fluent-langneg = "0.13"
|
|
||||||
thiserror = "1.0"
|
|
||||||
anyhow = "1.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
actix-web-static-files = "3.0"
|
actix-web-static-files = "3.0"
|
||||||
|
|
||||||
# optimize for size at cost of compilation speed.
|
[dependencies]
|
||||||
|
actix-identity = "0.3"
|
||||||
|
actix-rt = "1.1"
|
||||||
|
actix-slog = "0.2"
|
||||||
|
actix-web = "3"
|
||||||
|
actix-web-static-files = "3.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
argonautica = "0.2"
|
||||||
|
clap = "2.33"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
fluent-langneg = "0.13"
|
||||||
|
image = "0.23"
|
||||||
|
opentelemetry = "0.13"
|
||||||
|
opentelemetry-jaeger = "0.12"
|
||||||
|
qrcode = "0.12"
|
||||||
|
rand = "0.8"
|
||||||
|
rpassword = "5.0"
|
||||||
|
serde = "1.0"
|
||||||
|
tera = "1.6"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tracing-actix-web = "0.2.1"
|
||||||
|
tracing-bunyan-formatter = "0.2.0"
|
||||||
|
tracing-opentelemetry = "0.12"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.fluent-templates]
|
||||||
|
features = ["tera"]
|
||||||
|
version = "0.6"
|
||||||
|
|
||||||
|
[dependencies.sqlx]
|
||||||
|
features = ["sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.tracing]
|
||||||
|
features = ["log"]
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[dependencies.tracing-subscriber]
|
||||||
|
features = ["registry", "env-filter"]
|
||||||
|
version = "0.2.17"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-server = "1.0.4"
|
||||||
|
tempdir = "0.3"
|
||||||
|
test_bin = "0.3"
|
||||||
|
tokio = "0.2.25"
|
||||||
|
|
||||||
|
[dev-dependencies.reqwest]
|
||||||
|
features = ["cookies"]
|
||||||
|
version = "0.10.10"
|
||||||
|
|
||||||
|
[profile]
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
#codegen-units = 1
|
#codegen-units = 1
|
@ -12,7 +12,7 @@ use std::{
|
|||||||
|
|
||||||
use pslink::{models::NewUser, models::User, ServerConfig, ServerError};
|
use pslink::{models::NewUser, models::User, ServerConfig, ServerError};
|
||||||
|
|
||||||
use slog::{Drain, Logger};
|
use tracing::{error, info, trace, warn};
|
||||||
|
|
||||||
static MIGRATOR: Migrator = sqlx::migrate!();
|
static MIGRATOR: Migrator = sqlx::migrate!();
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ fn generate_cli() -> App<'static, 'static> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConfig {
|
async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
|
||||||
let secret = config
|
let secret = config
|
||||||
.value_of("secret")
|
.value_of("secret")
|
||||||
.expect("Failed to read the secret")
|
.expect("Failed to read the secret")
|
||||||
@ -132,20 +132,11 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
|
|||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
if secret.is_empty() {
|
if secret.is_empty() {
|
||||||
slog_warn!(
|
warn!("No secret was found! Use the environment variable PSLINK_SECRET to set one.");
|
||||||
log,
|
warn!("If you change the secret all passwords will be invalid");
|
||||||
"No secret was found! Use the environment variable PSLINK_SECRET to set one."
|
warn!("Using an auto generated one for this run.");
|
||||||
);
|
|
||||||
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 {
|
} else {
|
||||||
slog_warn!(
|
warn!("The provided secret was too short. Using an autogenerated one.")
|
||||||
log,
|
|
||||||
"The provided secret was too short. Using an autogenerated one."
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_rng()
|
thread_rng()
|
||||||
@ -156,6 +147,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
|
|||||||
} else {
|
} else {
|
||||||
secret
|
secret
|
||||||
};
|
};
|
||||||
|
let secret = pslink::Secret::new(secret);
|
||||||
let db = config
|
let db = config
|
||||||
.value_of("database")
|
.value_of("database")
|
||||||
.expect(concat!(
|
.expect(concat!(
|
||||||
@ -195,8 +187,6 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
|
|||||||
.parse::<pslink::Protocol>()
|
.parse::<pslink::Protocol>()
|
||||||
.expect("Failed to parse the protocol");
|
.expect("Failed to parse the protocol");
|
||||||
|
|
||||||
let log = log.new(slog_o!("host" => public_url.clone()));
|
|
||||||
|
|
||||||
crate::ServerConfig {
|
crate::ServerConfig {
|
||||||
secret,
|
secret,
|
||||||
db,
|
db,
|
||||||
@ -205,7 +195,6 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
|
|||||||
internal_ip,
|
internal_ip,
|
||||||
port,
|
port,
|
||||||
protocol,
|
protocol,
|
||||||
log,
|
|
||||||
empty_forward_url,
|
empty_forward_url,
|
||||||
brand_name,
|
brand_name,
|
||||||
}
|
}
|
||||||
@ -214,16 +203,9 @@ async fn parse_args_to_config(config: ArgMatches<'_>, log: Logger) -> ServerConf
|
|||||||
pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
|
pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
// initiallize the logger
|
|
||||||
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"));
|
|
||||||
|
|
||||||
// Print launch info
|
// Print launch info
|
||||||
slog_info!(log, "Launching Pslink a 'Private short link generator'");
|
info!("Launching Pslink a 'Private short link generator'");
|
||||||
slog_trace!(log, "logging initialized");
|
trace!("logging initialized");
|
||||||
|
|
||||||
let app = generate_cli();
|
let app = generate_cli();
|
||||||
|
|
||||||
@ -238,9 +220,12 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
|
|||||||
))
|
))
|
||||||
.parse::<PathBuf>()
|
.parse::<PathBuf>()
|
||||||
.expect("Failed to parse Database path.");
|
.expect("Failed to parse Database path.");
|
||||||
|
|
||||||
if !db.exists() {
|
if !db.exists() {
|
||||||
slog_trace!(log, "No database file found {}", db.display());
|
trace!("No database file found {}", db.display());
|
||||||
if config.subcommand_matches("migrate-database").is_none() {
|
if !(config.subcommand_matches("migrate-database").is_none()
|
||||||
|
| config.subcommand_matches("generate-env").is_none())
|
||||||
|
{
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
concat!(
|
concat!(
|
||||||
"Database not found at {}!",
|
"Database not found at {}!",
|
||||||
@ -249,17 +234,16 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
|
|||||||
),
|
),
|
||||||
db.display()
|
db.display()
|
||||||
);
|
);
|
||||||
slog_error!(log, "{}", msg);
|
error!("{}", msg);
|
||||||
eprintln!("{}", msg);
|
eprintln!("{}", msg);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
slog_trace!(log, "Creating database: {}", db.display());
|
trace!("Creating database: {}", db.display());
|
||||||
|
|
||||||
// create an empty database file. The if above makes sure that this file does not exist.
|
// create an empty database file. The if above makes sure that this file does not exist.
|
||||||
File::create(db)?;
|
File::create(db)?;
|
||||||
};
|
};
|
||||||
|
let server_config: crate::ServerConfig = parse_args_to_config(config.clone()).await;
|
||||||
let server_config: crate::ServerConfig = parse_args_to_config(config.clone(), log).await;
|
|
||||||
|
|
||||||
if let Some(_migrate_config) = config.subcommand_matches("generate-env") {
|
if let Some(_migrate_config) = config.subcommand_matches("generate-env") {
|
||||||
return match generate_env_file(&server_config) {
|
return match generate_env_file(&server_config) {
|
||||||
@ -284,21 +268,15 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
|
|||||||
let num_users = User::count_admins(&server_config).await?;
|
let num_users = User::count_admins(&server_config).await?;
|
||||||
|
|
||||||
if num_users.number < 1 {
|
if num_users.number < 1 {
|
||||||
slog_warn!(
|
warn!(concat!(
|
||||||
&server_config.log,
|
"No admin user created you will not be",
|
||||||
concat!(
|
" able to do anything as the service is invite only.",
|
||||||
"No admin user created you will not be",
|
" Create a user with `pslink create-admin`"
|
||||||
" able to do anything as the service is invite only.",
|
));
|
||||||
" Create a user with `pslink create-admin`"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
slog_trace!(&server_config.log, "At least one admin user is found.");
|
trace!("At least one admin user is found.");
|
||||||
}
|
}
|
||||||
slog_trace!(
|
trace!("Initialization finished starting the service.");
|
||||||
&server_config.log,
|
|
||||||
"Initialization finished starting the service."
|
|
||||||
);
|
|
||||||
Ok(Some(server_config))
|
Ok(Some(server_config))
|
||||||
} else {
|
} else {
|
||||||
println!("{}", config.usage());
|
println!("{}", config.usage());
|
||||||
@ -308,7 +286,7 @@ pub(crate) async fn setup() -> Result<Option<crate::ServerConfig>, ServerError>
|
|||||||
|
|
||||||
/// Interactively create a new admin user.
|
/// Interactively create a new admin user.
|
||||||
async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
|
async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
|
||||||
slog_info!(&config.log, "Creating an admin user.");
|
info!("Creating an admin user.");
|
||||||
let sin = io::stdin();
|
let sin = io::stdin();
|
||||||
|
|
||||||
// wait for logging:
|
// wait for logging:
|
||||||
@ -325,11 +303,9 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
|
|||||||
print!("Please enter the password for {}: ", new_username);
|
print!("Please enter the password for {}: ", new_username);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
let password = rpassword::read_password().unwrap();
|
let password = rpassword::read_password().unwrap();
|
||||||
slog_info!(
|
info!(
|
||||||
&config.log,
|
|
||||||
"Creating {} ({}) with given password ",
|
"Creating {} ({}) with given password ",
|
||||||
&new_username,
|
&new_username, &new_email
|
||||||
&new_email
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_admin = NewUser::new(new_username.clone(), new_email.clone(), &password, config)?;
|
let new_admin = NewUser::new(new_username.clone(), new_email.clone(), &password, config)?;
|
||||||
@ -338,14 +314,13 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
|
|||||||
let created_user = User::get_user_by_name(&new_username, config).await?;
|
let created_user = User::get_user_by_name(&new_username, config).await?;
|
||||||
created_user.toggle_admin(config).await?;
|
created_user.toggle_admin(config).await?;
|
||||||
|
|
||||||
slog_info!(&config.log, "Admin user created: {}", new_username);
|
info!("Admin user created: {}", new_username);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
|
async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
|
||||||
slog_info!(
|
info!(
|
||||||
config.log,
|
|
||||||
"Creating a database file and running the migrations in the file {}:",
|
"Creating a database file and running the migrations in the file {}:",
|
||||||
&config.db.display()
|
&config.db.display()
|
||||||
);
|
);
|
||||||
@ -355,31 +330,25 @@ async fn apply_migrations(config: &ServerConfig) -> Result<(), ServerError> {
|
|||||||
|
|
||||||
fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> {
|
fn generate_env_file(server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||||
if std::path::Path::new(".env").exists() {
|
if std::path::Path::new(".env").exists() {
|
||||||
slog_error!(
|
return Err(ServerError::User(
|
||||||
server_config.log,
|
"ERROR: There already is a .env file - ABORT!".to_string(),
|
||||||
"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!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
r#"Creating a .env file with default options
|
||||||
|
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();
|
||||||
|
|
||||||
|
for line in &conf_file_content {
|
||||||
|
file.write_all(line.as_bytes())
|
||||||
|
.expect("failed to write .env file")
|
||||||
|
}
|
||||||
|
info!("Successfully created the env file!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,212 +1,62 @@
|
|||||||
extern crate sqlx;
|
extern crate sqlx;
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[macro_use(
|
|
||||||
slog_o,
|
|
||||||
slog_trace,
|
|
||||||
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 cli;
|
||||||
mod views;
|
|
||||||
|
|
||||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
use pslink::ServerConfig;
|
||||||
use actix_web::{web, App, HttpServer};
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use fluent_templates::{static_loader, FluentLoader};
|
|
||||||
use tera::Tera;
|
|
||||||
|
|
||||||
use pslink::{ServerConfig, ServerError};
|
use tracing::instrument;
|
||||||
|
use tracing::{subscriber::set_global_default, Subscriber};
|
||||||
|
use tracing_opentelemetry::OpenTelemetryLayer;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
/// Compose multiple layers into a `tracing`'s subscriber.
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_subscriber(name: &str, env_filter: &str) -> impl Subscriber + Send + Sync {
|
||||||
|
let env_filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
|
||||||
|
// Create a jaeger exporter pipeline for a `trace_demo` service.
|
||||||
|
let tracer = opentelemetry_jaeger::new_pipeline()
|
||||||
|
.with_service_name(name)
|
||||||
|
.install_simple()
|
||||||
|
.expect("Error initializing Jaeger exporter");
|
||||||
|
let formatting_layer = tracing_subscriber::fmt::layer().with_target(false);
|
||||||
|
|
||||||
static_loader! {
|
// Create a layer with the configured tracer
|
||||||
static LOCALES = {
|
let otel_layer = OpenTelemetryLayer::new(tracer);
|
||||||
locales: "./locales",
|
|
||||||
fallback_language: "en",
|
// Use the tracing subscriber `Registry`, or any other subscriber
|
||||||
};
|
// that impls `LookupSpan`
|
||||||
|
Registry::default()
|
||||||
|
.with(otel_layer)
|
||||||
|
.with(env_filter)
|
||||||
|
.with(formatting_layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_tera() -> Result<Tera> {
|
/// Register a subscriber as global default to process span data.
|
||||||
let mut tera = Tera::default();
|
///
|
||||||
|
/// It should only be called once!
|
||||||
// Add translation support
|
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
|
||||||
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
|
set_global_default(subscriber).expect("Failed to set subscriber");
|
||||||
|
|
||||||
tera.add_raw_templates(vec![
|
|
||||||
("admin.html", include_str!("../../../templates/admin.html")),
|
|
||||||
("base.html", include_str!("../../../templates/base.html")),
|
|
||||||
(
|
|
||||||
"edit_link.html",
|
|
||||||
include_str!("../../../templates/edit_link.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"edit_profile.html",
|
|
||||||
include_str!("../../../templates/edit_profile.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"index_users.html",
|
|
||||||
include_str!("../../../templates/index_users.html"),
|
|
||||||
),
|
|
||||||
("index.html", include_str!("../../../templates/index.html")),
|
|
||||||
("login.html", include_str!("../../../templates/login.html")),
|
|
||||||
(
|
|
||||||
"not_found.html",
|
|
||||||
include_str!("../../../templates/not_found.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"signup.html",
|
|
||||||
include_str!("../../../templates/signup.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"submission.html",
|
|
||||||
include_str!("../../../templates/submission.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"view_link.html",
|
|
||||||
include_str!("../../../templates/view_link.html"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"view_profile.html",
|
|
||||||
include_str!("../../../templates/view_profile.html"),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
.context("Failed to load Templates")?;
|
|
||||||
Ok(tera)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::future_not_send, clippy::too_many_lines)]
|
|
||||||
async fn webservice(server_config: ServerConfig) -> Result<()> {
|
|
||||||
let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port);
|
|
||||||
let cfg = server_config.clone();
|
|
||||||
slog_info!(
|
|
||||||
cfg.log,
|
|
||||||
"Running on: {}://{}/admin/login/",
|
|
||||||
&server_config.protocol,
|
|
||||||
host_port
|
|
||||||
);
|
|
||||||
slog_info!(
|
|
||||||
cfg.log,
|
|
||||||
"If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
|
|
||||||
&server_config.protocol,
|
|
||||||
&server_config.public_url
|
|
||||||
);
|
|
||||||
let tera = build_tera()?;
|
|
||||||
slog_trace!(cfg.log, "The tera templates are ready");
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
|
||||||
let generated = generate();
|
|
||||||
App::new()
|
|
||||||
.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")
|
|
||||||
.secure(false),
|
|
||||||
))
|
|
||||||
.data(tera.clone())
|
|
||||||
.service(actix_web_static_files::ResourceFiles::new(
|
|
||||||
"/static", generated,
|
|
||||||
))
|
|
||||||
// directly go to the main page set the target with the environment variable.
|
|
||||||
.route("/", web::get().to(views::redirect_empty))
|
|
||||||
// admin block
|
|
||||||
.service(
|
|
||||||
web::scope("/admin")
|
|
||||||
// list all links
|
|
||||||
.route("/index/", web::get().to(views::index))
|
|
||||||
// invite users
|
|
||||||
.route("/signup/", web::get().to(views::signup))
|
|
||||||
.route("/signup/", web::post().to(views::process_signup))
|
|
||||||
// logout
|
|
||||||
.route("/logout/", web::to(views::logout))
|
|
||||||
// submit a new url for shortening
|
|
||||||
.route("/submit/", web::get().to(views::create_link))
|
|
||||||
.route("/submit/", web::post().to(views::process_link_creation))
|
|
||||||
// view an existing url
|
|
||||||
.service(
|
|
||||||
web::scope("/view")
|
|
||||||
.service(
|
|
||||||
web::scope("/link")
|
|
||||||
.route("/{redirect_id}", web::get().to(views::view_link))
|
|
||||||
.route("/", web::get().to(views::view_link_empty)),
|
|
||||||
)
|
|
||||||
.service(
|
|
||||||
web::scope("/profile")
|
|
||||||
.route("/{user_id}", web::get().to(views::view_profile)),
|
|
||||||
)
|
|
||||||
.route("/users/", web::get().to(views::index_users)),
|
|
||||||
)
|
|
||||||
.service(
|
|
||||||
web::scope("/edit")
|
|
||||||
.service(
|
|
||||||
web::scope("/link")
|
|
||||||
.route("/{redirect_id}", web::get().to(views::edit_link))
|
|
||||||
.route(
|
|
||||||
"/{redirect_id}",
|
|
||||||
web::post().to(views::process_link_edit),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.service(
|
|
||||||
web::scope("/profile")
|
|
||||||
.route("/{user_id}", web::get().to(views::edit_profile))
|
|
||||||
.route(
|
|
||||||
"/{user_id}",
|
|
||||||
web::post().to(views::process_edit_profile),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route("/set_admin/{user_id}", web::get().to(views::toggle_admin))
|
|
||||||
.route(
|
|
||||||
"/set_language/{language}",
|
|
||||||
web::get().to(views::set_language),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.service(
|
|
||||||
web::scope("/delete").service(
|
|
||||||
web::scope("/link")
|
|
||||||
.route("/{redirect_id}", web::get().to(views::process_link_delete)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.service(
|
|
||||||
web::scope("/download")
|
|
||||||
.route("/png/{redirect_id}", web::get().to(views::download_png)),
|
|
||||||
)
|
|
||||||
// login to the admin area
|
|
||||||
.route("/login/", web::get().to(views::login))
|
|
||||||
.route("/login/", web::post().to(views::process_login)),
|
|
||||||
)
|
|
||||||
// redirect to the url hidden behind the code
|
|
||||||
.route("/{redirect_id}", web::get().to(views::redirect))
|
|
||||||
})
|
|
||||||
.bind(host_port)
|
|
||||||
.context("Failed to bind to port")
|
|
||||||
.map_err(|e| {
|
|
||||||
slog_error!(cfg.log, "Failed to bind to port!");
|
|
||||||
e
|
|
||||||
})?
|
|
||||||
.run()
|
|
||||||
.await
|
|
||||||
.context("Failed to run the webservice")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::result::Result<(), ServerError> {
|
async fn main() -> std::result::Result<(), std::io::Error> {
|
||||||
|
let subscriber = get_subscriber("fhs.li", "info");
|
||||||
|
init_subscriber(subscriber);
|
||||||
|
|
||||||
match cli::setup().await {
|
match cli::setup().await {
|
||||||
Ok(Some(server_config)) => webservice(server_config).await.map_err(|e| {
|
Ok(Some(server_config)) => {
|
||||||
println!("{:?}", e);
|
pslink::webservice(server_config)
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
.await
|
||||||
std::process::exit(0);
|
.map_err(|e| {
|
||||||
}),
|
println!("{:?}", e);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
std::process::exit(0);
|
||||||
|
})
|
||||||
|
.expect("Failed to launch the service")
|
||||||
|
.await
|
||||||
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
|
225
src/lib.rs
225
src/lib.rs
@ -1,30 +1,23 @@
|
|||||||
extern crate sqlx;
|
extern crate sqlx;
|
||||||
#[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;
|
|
||||||
|
|
||||||
pub mod forms;
|
pub mod forms;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
|
mod views;
|
||||||
|
|
||||||
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||||
|
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use fluent_templates::{static_loader, FluentLoader};
|
||||||
use qrcode::types::QrError;
|
use qrcode::types::QrError;
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
|
use std::{fmt::Display, path::PathBuf, str::FromStr};
|
||||||
|
use tera::Tera;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::instrument;
|
||||||
|
use tracing::{error, info, trace};
|
||||||
|
use tracing_actix_web::TracingLogger;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ServerError {
|
pub enum ServerError {
|
||||||
#[error("Failed to encrypt the password {0} - aborting!")]
|
#[error("Failed to encrypt the password {0} - aborting!")]
|
||||||
@ -162,16 +155,39 @@ impl FromStr for Protocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Secret {
|
||||||
|
secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Secret {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(secret: String) -> Self {
|
||||||
|
Self { secret }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Secret {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("*****SECRET*****")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Secret {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("*****SECRET*****")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub secret: String,
|
pub secret: Secret,
|
||||||
pub db: PathBuf,
|
pub db: PathBuf,
|
||||||
pub db_pool: Pool<Sqlite>,
|
pub db_pool: Pool<Sqlite>,
|
||||||
pub public_url: String,
|
pub public_url: String,
|
||||||
pub internal_ip: String,
|
pub internal_ip: String,
|
||||||
pub port: u32,
|
pub port: u32,
|
||||||
pub protocol: Protocol,
|
pub protocol: Protocol,
|
||||||
pub log: slog::Logger,
|
|
||||||
pub empty_forward_url: String,
|
pub empty_forward_url: String,
|
||||||
pub brand_name: String,
|
pub brand_name: String,
|
||||||
}
|
}
|
||||||
@ -192,7 +208,176 @@ impl ServerConfig {
|
|||||||
"# If it is changed all existing passwords are invalid.\n"
|
"# If it is changed all existing passwords are invalid.\n"
|
||||||
)
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
format!("PSLINK_SECRET=\"{}\"\n", self.secret),
|
format!("PSLINK_SECRET=\"{}\"\n", self.secret.secret),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||||
|
|
||||||
|
static_loader! {
|
||||||
|
static LOCALES = {
|
||||||
|
locales: "./locales",
|
||||||
|
fallback_language: "en",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn build_tera() -> Result<Tera, ServerError> {
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
|
||||||
|
// Add translation support
|
||||||
|
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
|
||||||
|
|
||||||
|
tera.add_raw_templates(vec![
|
||||||
|
("admin.html", include_str!("../templates/admin.html")),
|
||||||
|
("base.html", include_str!("../templates/base.html")),
|
||||||
|
(
|
||||||
|
"edit_link.html",
|
||||||
|
include_str!("../templates/edit_link.html"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"edit_profile.html",
|
||||||
|
include_str!("../templates/edit_profile.html"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"index_users.html",
|
||||||
|
include_str!("../templates/index_users.html"),
|
||||||
|
),
|
||||||
|
("index.html", include_str!("../templates/index.html")),
|
||||||
|
("login.html", include_str!("../templates/login.html")),
|
||||||
|
(
|
||||||
|
"not_found.html",
|
||||||
|
include_str!("../templates/not_found.html"),
|
||||||
|
),
|
||||||
|
("signup.html", include_str!("../templates/signup.html")),
|
||||||
|
(
|
||||||
|
"submission.html",
|
||||||
|
include_str!("../templates/submission.html"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"view_link.html",
|
||||||
|
include_str!("../templates/view_link.html"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"view_profile.html",
|
||||||
|
include_str!("../templates/view_profile.html"),
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
Ok(tera)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch the pslink-webservice
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This produces a [`ServerError`] if:
|
||||||
|
/// * Tera failed to build its templates
|
||||||
|
/// * The server failed to bind to the designated port.
|
||||||
|
#[allow(clippy::future_not_send, clippy::too_many_lines)]
|
||||||
|
pub async fn webservice(
|
||||||
|
server_config: ServerConfig,
|
||||||
|
) -> Result<actix_web::dev::Server, std::io::Error> {
|
||||||
|
let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port);
|
||||||
|
info!(
|
||||||
|
"Running on: {}://{}/admin/login/",
|
||||||
|
&server_config.protocol, host_port
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
|
||||||
|
&server_config.protocol, &server_config.public_url
|
||||||
|
);
|
||||||
|
let tera = build_tera().expect("Failed to build Templates");
|
||||||
|
trace!("The tera templates are ready");
|
||||||
|
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
let generated = generate();
|
||||||
|
App::new()
|
||||||
|
.data(server_config.clone())
|
||||||
|
.wrap(TracingLogger)
|
||||||
|
.wrap(IdentityService::new(
|
||||||
|
CookieIdentityPolicy::new(&[0; 32])
|
||||||
|
.name("auth-cookie")
|
||||||
|
.secure(false),
|
||||||
|
))
|
||||||
|
.data(tera.clone())
|
||||||
|
.service(actix_web_static_files::ResourceFiles::new(
|
||||||
|
"/static", generated,
|
||||||
|
))
|
||||||
|
// directly go to the main page set the target with the environment variable.
|
||||||
|
.route("/", web::get().to(views::redirect_empty))
|
||||||
|
// admin block
|
||||||
|
.service(
|
||||||
|
web::scope("/admin")
|
||||||
|
// list all links
|
||||||
|
.route("/index/", web::get().to(views::index))
|
||||||
|
// invite users
|
||||||
|
.route("/signup/", web::get().to(views::signup))
|
||||||
|
.route("/signup/", web::post().to(views::process_signup))
|
||||||
|
// logout
|
||||||
|
.route("/logout/", web::to(views::logout))
|
||||||
|
// submit a new url for shortening
|
||||||
|
.route("/submit/", web::get().to(views::create_link))
|
||||||
|
.route("/submit/", web::post().to(views::process_link_creation))
|
||||||
|
// view an existing url
|
||||||
|
.service(
|
||||||
|
web::scope("/view")
|
||||||
|
.service(
|
||||||
|
web::scope("/link")
|
||||||
|
.route("/{redirect_id}", web::get().to(views::view_link))
|
||||||
|
.route("/", web::get().to(views::view_link_empty)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/profile")
|
||||||
|
.route("/{user_id}", web::get().to(views::view_profile)),
|
||||||
|
)
|
||||||
|
.route("/users/", web::get().to(views::index_users)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/edit")
|
||||||
|
.service(
|
||||||
|
web::scope("/link")
|
||||||
|
.route("/{redirect_id}", web::get().to(views::edit_link))
|
||||||
|
.route(
|
||||||
|
"/{redirect_id}",
|
||||||
|
web::post().to(views::process_link_edit),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/profile")
|
||||||
|
.route("/{user_id}", web::get().to(views::edit_profile))
|
||||||
|
.route(
|
||||||
|
"/{user_id}",
|
||||||
|
web::post().to(views::process_edit_profile),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route("/set_admin/{user_id}", web::get().to(views::toggle_admin))
|
||||||
|
.route(
|
||||||
|
"/set_language/{language}",
|
||||||
|
web::get().to(views::set_language),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/delete").service(
|
||||||
|
web::scope("/link")
|
||||||
|
.route("/{redirect_id}", web::get().to(views::process_link_delete)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/download")
|
||||||
|
.route("/png/{redirect_id}", web::get().to(views::download_png)),
|
||||||
|
)
|
||||||
|
// login to the admin area
|
||||||
|
.route("/login/", web::get().to(views::login))
|
||||||
|
.route("/login/", web::post().to(views::process_login)),
|
||||||
|
)
|
||||||
|
// redirect to the url hidden behind the code
|
||||||
|
.route("/{redirect_id}", web::get().to(views::redirect))
|
||||||
|
})
|
||||||
|
.bind(host_port)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to bind to port!");
|
||||||
|
e
|
||||||
|
})?
|
||||||
|
.run();
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
@ -146,7 +146,7 @@ impl NewUser {
|
|||||||
|
|
||||||
let hash = Hasher::default()
|
let hash = Hasher::default()
|
||||||
.with_password(password)
|
.with_password(password)
|
||||||
.with_secret_key(secret)
|
.with_secret_key(&secret.secret)
|
||||||
.hash()?;
|
.hash()?;
|
||||||
|
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
@ -197,7 +197,7 @@ impl Link {
|
|||||||
let link = sqlx::query_as!(Self, "Select * from links where code = ? ", code)
|
let link = sqlx::query_as!(Self, "Select * from links where code = ? ", code)
|
||||||
.fetch_one(&server_config.db_pool)
|
.fetch_one(&server_config.db_pool)
|
||||||
.await;
|
.await;
|
||||||
slog_info!(server_config.log, "Found link: {:?}", &link);
|
tracing::info!("Found link: {:?}", &link);
|
||||||
link.map_err(ServerError::Database)
|
link.map_err(ServerError::Database)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use super::models::{Count, Link, NewUser, User};
|
use super::models::{Count, Link, NewUser, User};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -170,7 +171,7 @@ pub async fn get_user(
|
|||||||
server_config: &ServerConfig,
|
server_config: &ServerConfig,
|
||||||
) -> Result<Item<User>, ServerError> {
|
) -> Result<Item<User>, ServerError> {
|
||||||
if let Ok(uid) = user_id.parse::<i64>() {
|
if let Ok(uid) = user_id.parse::<i64>() {
|
||||||
slog_info!(server_config.log, "Getting user {}", uid);
|
info!("Getting user {}", uid);
|
||||||
let auth = authenticate(id, server_config).await?;
|
let auth = authenticate(id, server_config).await?;
|
||||||
if auth.admin_or_self(uid) {
|
if auth.admin_or_self(uid) {
|
||||||
match auth {
|
match auth {
|
||||||
@ -214,7 +215,7 @@ pub async fn create_user(
|
|||||||
data: &web::Form<NewUser>,
|
data: &web::Form<NewUser>,
|
||||||
server_config: &ServerConfig,
|
server_config: &ServerConfig,
|
||||||
) -> Result<Item<User>, ServerError> {
|
) -> Result<Item<User>, ServerError> {
|
||||||
slog_info!(server_config.log, "Creating a User: {:?}", &data);
|
info!("Creating a User: {:?}", &data);
|
||||||
let auth = authenticate(id, server_config).await?;
|
let auth = authenticate(id, server_config).await?;
|
||||||
match auth {
|
match auth {
|
||||||
Role::Admin { user } => {
|
Role::Admin { user } => {
|
||||||
@ -258,7 +259,7 @@ pub async fn update_user(
|
|||||||
if auth.admin_or_self(uid) {
|
if auth.admin_or_self(uid) {
|
||||||
match auth {
|
match auth {
|
||||||
Role::Admin { .. } | Role::Regular { .. } => {
|
Role::Admin { .. } | Role::Regular { .. } => {
|
||||||
slog_info!(server_config.log, "Updating userinfo: ");
|
info!("Updating userinfo: ");
|
||||||
let password = if data.password.len() > 3 {
|
let password = if data.password.len() > 3 {
|
||||||
NewUser::hash_password(&data.password, server_config)?
|
NewUser::hash_password(&data.password, server_config)?
|
||||||
} else {
|
} else {
|
||||||
@ -303,21 +304,17 @@ pub async fn toggle_admin(
|
|||||||
let auth = authenticate(id, server_config).await?;
|
let auth = authenticate(id, server_config).await?;
|
||||||
match auth {
|
match auth {
|
||||||
Role::Admin { .. } => {
|
Role::Admin { .. } => {
|
||||||
slog_info!(server_config.log, "Changing administrator priviledges: ");
|
info!("Changing administrator priviledges: ");
|
||||||
|
|
||||||
let unchanged_user = User::get_user(uid, server_config).await?;
|
let unchanged_user = User::get_user(uid, server_config).await?;
|
||||||
|
|
||||||
let old = unchanged_user.role;
|
let old = unchanged_user.role;
|
||||||
unchanged_user.toggle_admin(server_config).await?;
|
unchanged_user.toggle_admin(server_config).await?;
|
||||||
|
|
||||||
slog_info!(server_config.log, "Toggling role: old was {}", old);
|
info!("Toggling role: old was {}", old);
|
||||||
|
|
||||||
let changed_user = User::get_user(uid, server_config).await?;
|
let changed_user = User::get_user(uid, server_config).await?;
|
||||||
slog_info!(
|
info!("Toggled role: new is {}", changed_user.role);
|
||||||
server_config.log,
|
|
||||||
"Toggled role: new is {}",
|
|
||||||
changed_user.role
|
|
||||||
);
|
|
||||||
Ok(Item {
|
Ok(Item {
|
||||||
user: changed_user.clone(),
|
user: changed_user.clone(),
|
||||||
item: changed_user,
|
item: changed_user,
|
||||||
@ -382,10 +379,10 @@ pub async fn get_link_simple(
|
|||||||
link_code: &str,
|
link_code: &str,
|
||||||
server_config: &ServerConfig,
|
server_config: &ServerConfig,
|
||||||
) -> Result<Link, ServerError> {
|
) -> Result<Link, ServerError> {
|
||||||
slog_info!(server_config.log, "Getting link for {:?}", link_code);
|
info!("Getting link for {:?}", link_code);
|
||||||
|
|
||||||
let link = Link::get_link_by_code(link_code, server_config).await?;
|
let link = Link::get_link_by_code(link_code, server_config).await?;
|
||||||
slog_info!(server_config.log, "Foun d link for {:?}", link);
|
info!("Foun d link for {:?}", link);
|
||||||
Ok(link)
|
Ok(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +391,7 @@ pub async fn get_link_simple(
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// Fails with [`ServerError`] if access to the database fails.
|
/// Fails with [`ServerError`] if access to the database fails.
|
||||||
pub async fn click_link(link_id: i64, server_config: &ServerConfig) -> Result<(), ServerError> {
|
pub async fn click_link(link_id: i64, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||||
slog_info!(server_config.log, "Clicking on {:?}", link_id);
|
info!("Clicking on {:?}", link_id);
|
||||||
let new_click = NewClick::new(link_id);
|
let new_click = NewClick::new(link_id);
|
||||||
new_click.insert_click(server_config).await?;
|
new_click.insert_click(server_config).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -429,12 +426,7 @@ pub async fn update_link(
|
|||||||
data: web::Form<LinkForm>,
|
data: web::Form<LinkForm>,
|
||||||
server_config: &ServerConfig,
|
server_config: &ServerConfig,
|
||||||
) -> Result<Item<Link>, ServerError> {
|
) -> Result<Item<Link>, ServerError> {
|
||||||
slog_info!(
|
info!("Changing link to: {:?} {:?}", &data, &link_code);
|
||||||
server_config.log,
|
|
||||||
"Changing link to: {:?} {:?}",
|
|
||||||
&data,
|
|
||||||
&link_code
|
|
||||||
);
|
|
||||||
let auth = authenticate(id, server_config).await?;
|
let auth = authenticate(id, server_config).await?;
|
||||||
match auth {
|
match auth {
|
||||||
Role::Admin { .. } | Role::Regular { .. } => {
|
Role::Admin { .. } | Role::Regular { .. } => {
|
||||||
@ -472,9 +464,9 @@ pub async fn create_link(
|
|||||||
match auth {
|
match auth {
|
||||||
Role::Admin { user } | Role::Regular { user } => {
|
Role::Admin { user } | Role::Regular { user } => {
|
||||||
let code = data.code.clone();
|
let code = data.code.clone();
|
||||||
slog_info!(server_config.log, "Creating link for: {}", &code);
|
info!("Creating link for: {}", &code);
|
||||||
let new_link = NewLink::from_link_form(data.into_inner(), user.id);
|
let new_link = NewLink::from_link_form(data.into_inner(), user.id);
|
||||||
slog_info!(server_config.log, "Creating link for: {:?}", &new_link);
|
info!("Creating link for: {:?}", &new_link);
|
||||||
|
|
||||||
new_link.insert(server_config).await?;
|
new_link.insert(server_config).await?;
|
||||||
let new_link = get_link_simple(&code, server_config).await?;
|
let new_link = get_link_simple(&code, server_config).await?;
|
||||||
|
@ -15,12 +15,14 @@ use image::{DynamicImage, ImageOutputFormat, Luma};
|
|||||||
use qrcode::{render::svg, QrCode};
|
use qrcode::{render::svg, QrCode};
|
||||||
use queries::{authenticate, Role};
|
use queries::{authenticate, Role};
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
use tracing::{info, instrument, trace, warn};
|
||||||
|
|
||||||
use pslink::forms::LinkForm;
|
use crate::forms::LinkForm;
|
||||||
use pslink::models::{LoginUser, NewUser};
|
use crate::models::{LoginUser, NewUser};
|
||||||
use pslink::queries;
|
use crate::queries;
|
||||||
use pslink::ServerError;
|
use crate::ServerError;
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn redirect_builder(target: &str) -> HttpResponse {
|
fn redirect_builder(target: &str) -> HttpResponse {
|
||||||
HttpResponse::SeeOther()
|
HttpResponse::SeeOther()
|
||||||
.set(CacheControl(vec![
|
.set(CacheControl(vec![
|
||||||
@ -33,6 +35,7 @@ fn redirect_builder(target: &str) -> HttpResponse {
|
|||||||
.body(format!("Redirect to {}", target))
|
.body(format!("Redirect to {}", target))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
|
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
|
||||||
let requested = parse_accepted_languages(
|
let requested = parse_accepted_languages(
|
||||||
request
|
request
|
||||||
@ -62,9 +65,11 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show the list of all available links if a user is authenticated
|
/// Show the list of all available links if a user is authenticated
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
if let Ok(links) = queries::list_all_allowed(&id, &config).await {
|
if let Ok(links) = queries::list_all_allowed(&id, &config).await {
|
||||||
@ -80,9 +85,10 @@ pub async fn index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show the list of all available links if a user is authenticated
|
/// Show the list of all available links if a user is authenticated
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn index_users(
|
pub async fn index_users(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
if let Ok(users) = queries::list_users(&id, &config).await {
|
if let Ok(users) = queries::list_users(&id, &config).await {
|
||||||
@ -97,17 +103,20 @@ pub async fn index_users(
|
|||||||
Ok(redirect_builder("/admin/login"))
|
Ok(redirect_builder("/admin/login"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn view_link_empty(
|
pub async fn view_link_empty(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
view_link(tera, config, id, web::Path::from("".to_owned())).await
|
view_link(tera, config, id, web::Path::from("".to_owned())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn view_link(
|
pub async fn view_link(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
link_id: web::Path<String>,
|
link_id: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
@ -144,13 +153,14 @@ pub async fn view_link(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn view_profile(
|
pub async fn view_profile(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
user_id: web::Path<String>,
|
user_id: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
slog_info!(config.log, "Viewing Profile!");
|
info!("Viewing Profile!");
|
||||||
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
|
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
|
||||||
let mut data = Context::new();
|
let mut data = Context::new();
|
||||||
data.insert("user", &query.user);
|
data.insert("user", &query.user);
|
||||||
@ -171,13 +181,14 @@ pub async fn view_profile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn edit_profile(
|
pub async fn edit_profile(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
user_id: web::Path<String>,
|
user_id: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
slog_info!(config.log, "Editing Profile!");
|
info!("Editing Profile!");
|
||||||
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
|
if let Ok(query) = queries::get_user(&id, &user_id.0, &config).await {
|
||||||
let mut data = Context::new();
|
let mut data = Context::new();
|
||||||
data.insert("user", &query.user);
|
data.insert("user", &query.user);
|
||||||
@ -197,9 +208,10 @@ pub async fn edit_profile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn process_edit_profile(
|
pub async fn process_edit_profile(
|
||||||
data: web::Form<NewUser>,
|
data: web::Form<NewUser>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
user_id: web::Path<String>,
|
user_id: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
@ -210,9 +222,10 @@ pub async fn process_edit_profile(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn download_png(
|
pub async fn download_png(
|
||||||
id: Identity,
|
id: Identity,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
link_code: web::Path<String>,
|
link_code: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
match queries::get_link(&id, &link_code.0, &config).await {
|
match queries::get_link(&id, &link_code.0, &config).await {
|
||||||
@ -234,9 +247,10 @@ pub async fn download_png(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id, tera))]
|
||||||
pub async fn signup(
|
pub async fn signup(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
match queries::authenticate(&id, &config).await? {
|
match queries::authenticate(&id, &config).await? {
|
||||||
@ -254,12 +268,13 @@ pub async fn signup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn process_signup(
|
pub async fn process_signup(
|
||||||
data: web::Form<NewUser>,
|
data: web::Form<NewUser>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
slog_info!(config.log, "Creating a User: {:?}", &data);
|
info!("Creating a User: {:?}", &data);
|
||||||
match queries::create_user(&id, &data, &config).await {
|
match queries::create_user(&id, &data, &config).await {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
Ok(HttpResponse::Ok().body(format!("Successfully saved user: {}", item.item.username)))
|
Ok(HttpResponse::Ok().body(format!("Successfully saved user: {}", item.item.username)))
|
||||||
@ -268,9 +283,10 @@ pub async fn process_signup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn toggle_admin(
|
pub async fn toggle_admin(
|
||||||
data: web::Path<String>,
|
data: web::Path<String>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
let update = queries::toggle_admin(&id, &data.0, &config).await?;
|
let update = queries::toggle_admin(&id, &data.0, &config).await?;
|
||||||
@ -280,23 +296,25 @@ pub async fn toggle_admin(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn set_language(
|
pub async fn set_language(
|
||||||
data: web::Path<String>,
|
data: web::Path<String>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
queries::set_language(&id, &data.0, &config).await?;
|
queries::set_language(&id, &data.0, &config).await?;
|
||||||
Ok(redirect_builder("/admin/index/"))
|
Ok(redirect_builder("/admin/index/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(tera, id))]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
let language_code = detect_language(&req)?;
|
let language_code = detect_language(&req).unwrap_or_else(|_| "en".to_string());
|
||||||
slog_info!(config.log, "Detected languagecode: {}", &language_code);
|
info!("Detected languagecode: {}", &language_code);
|
||||||
let mut data = Context::new();
|
let mut data = Context::new();
|
||||||
data.insert("title", "Login");
|
data.insert("title", "Login");
|
||||||
data.insert("language", &language_code);
|
data.insert("language", &language_code);
|
||||||
@ -305,8 +323,7 @@ pub async fn login(
|
|||||||
if let Ok(r) = authenticate(&id, &config).await {
|
if let Ok(r) = authenticate(&id, &config).await {
|
||||||
match r {
|
match r {
|
||||||
Role::Admin { user } | Role::Regular { user } => {
|
Role::Admin { user } | Role::Regular { user } => {
|
||||||
slog_trace!(
|
trace!(
|
||||||
config.log,
|
|
||||||
"This user ({}) is already logged in redirecting to /admin/index/",
|
"This user ({}) is already logged in redirecting to /admin/index/",
|
||||||
user.username
|
user.username
|
||||||
);
|
);
|
||||||
@ -315,7 +332,7 @@ pub async fn login(
|
|||||||
Role::Disabled | Role::NotAuthenticated => (),
|
Role::Disabled | Role::NotAuthenticated => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slog_warn!(config.log, "Invalid user session. The user might be deleted or something tampered with the cookies.");
|
warn!("Invalid user session. The user might be deleted or something tampered with the cookies.");
|
||||||
id.forget();
|
id.forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,9 +340,10 @@ pub async fn login(
|
|||||||
Ok(HttpResponse::Ok().body(rendered))
|
Ok(HttpResponse::Ok().body(rendered))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn process_login(
|
pub async fn process_login(
|
||||||
data: web::Form<LoginUser>,
|
data: web::Form<LoginUser>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
let user = queries::get_user_by_name(&data.username, &config).await;
|
let user = queries::get_user_by_name(&data.username, &config).await;
|
||||||
@ -336,11 +354,11 @@ pub async fn process_login(
|
|||||||
let valid = Verifier::default()
|
let valid = Verifier::default()
|
||||||
.with_hash(&u.password)
|
.with_hash(&u.password)
|
||||||
.with_password(&data.password)
|
.with_password(&data.password)
|
||||||
.with_secret_key(secret)
|
.with_secret_key(&secret.secret)
|
||||||
.verify()?;
|
.verify()?;
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
slog_info!(config.log, "Log-in of user: {}", &u.username);
|
info!("Log-in of user: {}", &u.username);
|
||||||
let session_token = u.username;
|
let session_token = u.username;
|
||||||
id.remember(session_token);
|
id.remember(session_token);
|
||||||
Ok(redirect_builder("/admin/index/"))
|
Ok(redirect_builder("/admin/index/"))
|
||||||
@ -349,42 +367,42 @@ pub async fn process_login(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
slog_info!(config.log, "Failed to login: {}", e);
|
info!("Failed to login: {}", e);
|
||||||
Ok(redirect_builder("/admin/login/"))
|
Ok(redirect_builder("/admin/login/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
|
pub async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
|
||||||
|
info!("Logging out the user");
|
||||||
id.forget();
|
id.forget();
|
||||||
Ok(redirect_builder("/admin/login/"))
|
Ok(redirect_builder("/admin/login/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
pub async fn redirect(
|
pub async fn redirect(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
data: web::Path<String>,
|
data: web::Path<String>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
slog_info!(config.log, "Redirecting to {:?}", data);
|
info!("Redirecting to {:?}", data);
|
||||||
let link = queries::get_link_simple(&data.0, &config).await;
|
let link = queries::get_link_simple(&data.0, &config).await;
|
||||||
slog_info!(config.log, "link: {:?}", link);
|
info!("link: {:?}", link);
|
||||||
match link {
|
match link {
|
||||||
Ok(link) => {
|
Ok(link) => {
|
||||||
queries::click_link(link.id, &config).await?;
|
queries::click_link(link.id, &config).await?;
|
||||||
Ok(redirect_builder(&link.target))
|
Ok(redirect_builder(&link.target))
|
||||||
}
|
}
|
||||||
Err(ServerError::Database(e)) => {
|
Err(ServerError::Database(e)) => {
|
||||||
slog_info!(
|
info!(
|
||||||
config.log,
|
|
||||||
"Link was not found: http://{}/{} \n {}",
|
"Link was not found: http://{}/{} \n {}",
|
||||||
&config.public_url,
|
&config.public_url, &data.0, e
|
||||||
&data.0,
|
|
||||||
e
|
|
||||||
);
|
);
|
||||||
let mut data = Context::new();
|
let mut data = Context::new();
|
||||||
data.insert("title", "Wurde gel\u{f6}scht");
|
data.insert("title", "Wurde gel\u{f6}scht");
|
||||||
let language = detect_language(&req)?;
|
let language = detect_language(&req).unwrap_or_else(|_| "en".to_string());
|
||||||
data.insert("language", &language);
|
data.insert("language", &language);
|
||||||
let rendered = tera.render("not_found.html", &data)?;
|
let rendered = tera.render("not_found.html", &data)?;
|
||||||
Ok(HttpResponse::NotFound().body(rendered))
|
Ok(HttpResponse::NotFound().body(rendered))
|
||||||
@ -393,15 +411,17 @@ pub async fn redirect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
pub async fn redirect_empty(
|
pub async fn redirect_empty(
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
Ok(redirect_builder(&config.empty_forward_url))
|
Ok(redirect_builder(&config.empty_forward_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn create_link(
|
pub async fn create_link(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
match queries::authenticate(&id, &config).await? {
|
match queries::authenticate(&id, &config).await? {
|
||||||
@ -419,9 +439,10 @@ pub async fn create_link(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn process_link_creation(
|
pub async fn process_link_creation(
|
||||||
data: web::Form<LinkForm>,
|
data: web::Form<LinkForm>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
let new_link = queries::create_link(&id, data, &config).await?;
|
let new_link = queries::create_link(&id, data, &config).await?;
|
||||||
@ -431,9 +452,10 @@ pub async fn process_link_creation(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn edit_link(
|
pub async fn edit_link(
|
||||||
tera: web::Data<Tera>,
|
tera: web::Data<Tera>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
link_id: web::Path<String>,
|
link_id: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
@ -450,7 +472,7 @@ pub async fn edit_link(
|
|||||||
}
|
}
|
||||||
pub async fn process_link_edit(
|
pub async fn process_link_edit(
|
||||||
data: web::Form<LinkForm>,
|
data: web::Form<LinkForm>,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
link_code: web::Path<String>,
|
link_code: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
@ -463,9 +485,10 @@ pub async fn process_link_edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(id))]
|
||||||
pub async fn process_link_delete(
|
pub async fn process_link_delete(
|
||||||
id: Identity,
|
id: Identity,
|
||||||
config: web::Data<pslink::ServerConfig>,
|
config: web::Data<crate::ServerConfig>,
|
||||||
link_code: web::Path<String>,
|
link_code: web::Path<String>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
queries::delete_link(&id, &link_code.0, &config).await?;
|
queries::delete_link(&id, &link_code.0, &config).await?;
|
349
tests/integration-tests.rs
Normal file
349
tests/integration-tests.rs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
#[test]
|
||||||
|
fn test_help_of_command_for_breaking_changes() {
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start pslink");
|
||||||
|
assert!(String::from_utf8_lossy(&output.stdout).contains("USAGE"));
|
||||||
|
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["--help"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start pslink");
|
||||||
|
let outstring = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
let args = &[
|
||||||
|
"USAGE",
|
||||||
|
"-h",
|
||||||
|
"--help",
|
||||||
|
"-b",
|
||||||
|
"-e",
|
||||||
|
"-i",
|
||||||
|
"-p",
|
||||||
|
"-t",
|
||||||
|
"-u",
|
||||||
|
"runserver",
|
||||||
|
"create-admin",
|
||||||
|
"generate-env",
|
||||||
|
"migrate-database",
|
||||||
|
"help",
|
||||||
|
];
|
||||||
|
|
||||||
|
for s in args {
|
||||||
|
assert!(
|
||||||
|
outstring.contains(s),
|
||||||
|
"{} was not found in the help - this is a breaking change",
|
||||||
|
s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_env() {
|
||||||
|
use std::io::BufRead;
|
||||||
|
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["generate-env", "--secret", "abcdefghijklmnopqrstuvw"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start pslink");
|
||||||
|
let envfile = tmp_dir.path().join(".env");
|
||||||
|
let dbfile = tmp_dir.path().join("links.db");
|
||||||
|
println!("{}", envfile.display());
|
||||||
|
println!("{}", dbfile.display());
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
assert!(envfile.exists(), "No .env-file was created!");
|
||||||
|
assert!(dbfile.exists(), "No database-file was created!");
|
||||||
|
|
||||||
|
let envfile = std::fs::File::open(envfile).unwrap();
|
||||||
|
let envcontent: Vec<Result<String, _>> = std::io::BufReader::new(envfile).lines().collect();
|
||||||
|
assert!(
|
||||||
|
envcontent
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.as_ref().unwrap().starts_with("PSLINK_PORT=")),
|
||||||
|
"Failed to find PSLINK_PORT in the generated .env file."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
envcontent
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.as_ref().unwrap().starts_with("PSLINK_SECRET=")),
|
||||||
|
"Failed to find PSLINK_SECRET in the generated .env file."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!envcontent.iter().any(|s| {
|
||||||
|
let r = s.as_ref().unwrap().contains("***SECRET***");
|
||||||
|
r
|
||||||
|
}),
|
||||||
|
"It seems that a censored secret was used in the .env file."
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
envcontent.iter().any(|s| {
|
||||||
|
let r = s.as_ref().unwrap().contains("abcdefghijklmnopqrstuvw");
|
||||||
|
r
|
||||||
|
}),
|
||||||
|
"The secret has not made it into the .env file!"
|
||||||
|
);
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["generate-env"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to start pslink");
|
||||||
|
let second_out = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(!second_out.contains("secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_migrate_database() {
|
||||||
|
use std::io::Write;
|
||||||
|
#[derive(serde::Serialize, Debug)]
|
||||||
|
pub struct Count {
|
||||||
|
pub number: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
|
||||||
|
// generate .env file
|
||||||
|
let _output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["generate-env"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed generate .env");
|
||||||
|
|
||||||
|
// migrate the database
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["migrate-database"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to migrate the database");
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
|
||||||
|
// check if the users table exists by counting the number of admins.
|
||||||
|
let db_pool = sqlx::pool::Pool::<sqlx::sqlite::Sqlite>::connect(
|
||||||
|
&tmp_dir.path().join("links.db").display().to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Error: Failed to connect to database!");
|
||||||
|
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// initially no admin is present
|
||||||
|
assert_eq!(num.number, 0, "Failed to create the database!");
|
||||||
|
|
||||||
|
// create a new admin
|
||||||
|
let mut input = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["create-admin"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to migrate the database");
|
||||||
|
let mut procin = input.stdin.take().unwrap();
|
||||||
|
|
||||||
|
procin.write_all(b"test\n").unwrap();
|
||||||
|
procin.write_all(b"test@mail.test\n").unwrap();
|
||||||
|
procin.write_all(b"testpw\n").unwrap();
|
||||||
|
|
||||||
|
let r = input.wait().unwrap();
|
||||||
|
println!("Exitstatus is: {}", r);
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// now 1 admin is there
|
||||||
|
assert_eq!(num.number, 1, "Failed to create an admin!");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_server() {
|
||||||
|
use std::io::Write;
|
||||||
|
#[derive(serde::Serialize, Debug)]
|
||||||
|
pub struct Count {
|
||||||
|
pub number: i32,
|
||||||
|
}
|
||||||
|
let tmp_dir = tempdir::TempDir::new("pslink_test_env").expect("create temp dir");
|
||||||
|
// generate .env file
|
||||||
|
let _output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["generate-env", "--secret", "abcdefghijklmnopqrstuvw"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed generate .env");
|
||||||
|
// migrate the database
|
||||||
|
let output = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["migrate-database"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to migrate the database");
|
||||||
|
|
||||||
|
// create a database connection.
|
||||||
|
let db_pool = sqlx::pool::Pool::<sqlx::sqlite::Sqlite>::connect(
|
||||||
|
&tmp_dir.path().join("links.db").display().to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Error: Failed to connect to database!"); // create a new admin
|
||||||
|
let mut input = test_bin::get_test_bin("pslink")
|
||||||
|
.args(&["create-admin"])
|
||||||
|
.current_dir(&tmp_dir)
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to migrate the database");
|
||||||
|
let mut procin = input.stdin.take().unwrap();
|
||||||
|
|
||||||
|
procin.write_all(b"test\n").unwrap();
|
||||||
|
procin.write_all(b"test@mail.test\n").unwrap();
|
||||||
|
procin.write_all(b"testpw\n").unwrap();
|
||||||
|
|
||||||
|
let r = input.wait().unwrap();
|
||||||
|
println!("Exitstatus is: {}", r);
|
||||||
|
|
||||||
|
println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// now 1 admin is there
|
||||||
|
assert_eq!(
|
||||||
|
num.number, 1,
|
||||||
|
"Failed to create an admin! See previous tests!"
|
||||||
|
);
|
||||||
|
|
||||||
|
let server_config = pslink::ServerConfig {
|
||||||
|
secret: pslink::Secret::new("abcdefghijklmnopqrstuvw".to_string()),
|
||||||
|
db: std::path::PathBuf::from("links.db"),
|
||||||
|
db_pool,
|
||||||
|
public_url: "localhost:8080".to_string(),
|
||||||
|
internal_ip: "localhost".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
protocol: pslink::Protocol::Http,
|
||||||
|
empty_forward_url: "https://github.com/enaut/pslink".to_string(),
|
||||||
|
brand_name: "Pslink".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = pslink::webservice(server_config);
|
||||||
|
|
||||||
|
let _neveruse = tokio::spawn(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_web_paths() {
|
||||||
|
run_server().await;
|
||||||
|
|
||||||
|
// We need to bring in `reqwest`
|
||||||
|
// to perform HTTP requests against our application.
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.cookie_store(true)
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.get("http://localhost:8080/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// The basic redirection is working!
|
||||||
|
assert!(response.status().is_redirection());
|
||||||
|
let location = response.headers().get("location").unwrap();
|
||||||
|
assert!(location.to_str().unwrap().contains("github"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.get("http://localhost:8080/admin/login/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// The Loginpage is reachable and contains a password field!
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
let content = response.text().await.unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains(r#"<input type="password"#),
|
||||||
|
"No password field was found!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let formdata = &[("username", "test"), ("password", "testpw")];
|
||||||
|
let response = client
|
||||||
|
.post("http://localhost:8080/admin/login/")
|
||||||
|
.form(formdata)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// It is possible to login
|
||||||
|
assert!(response.status().is_redirection());
|
||||||
|
let location = response.headers().get("location").unwrap();
|
||||||
|
assert_eq!("/admin/index/", location.to_str().unwrap());
|
||||||
|
assert!(
|
||||||
|
response.headers().get("set-cookie").is_some(),
|
||||||
|
"A auth cookie is not set even though authentication succeeds"
|
||||||
|
);
|
||||||
|
|
||||||
|
// After login this should return a redirect
|
||||||
|
let response = client
|
||||||
|
.get("http://localhost:8080/admin/login/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// The Loginpage redirects to link index when logged in
|
||||||
|
assert!(
|
||||||
|
response.status().is_redirection(),
|
||||||
|
"/admin/login/ is not redirecting correctly when logged in!"
|
||||||
|
);
|
||||||
|
let location = response.headers().get("location").unwrap();
|
||||||
|
assert_eq!("/admin/index/", location.to_str().unwrap());
|
||||||
|
|
||||||
|
// After login this should return a redirect
|
||||||
|
let response = client
|
||||||
|
.get("http://localhost:8080/admin/index/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// The Loginpage redirects to link index when logged in
|
||||||
|
assert!(
|
||||||
|
response.status().is_success(),
|
||||||
|
"Could not access /admin/index/"
|
||||||
|
);
|
||||||
|
let content = response.text().await.unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains(r#"<a href="/admin/logout/">"#),
|
||||||
|
"No Logout Button was found on /admin/index/!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act title=haupt&target=http%3A%2F%2Fdas.geht%2Fjetzt%2F&code=tpuah
|
||||||
|
let formdata = &[
|
||||||
|
("title", "haupt"),
|
||||||
|
("target", "https://das.geht/jetzt/"),
|
||||||
|
("code", "tpuah"),
|
||||||
|
];
|
||||||
|
let response = client
|
||||||
|
.post("http://localhost:8080/admin/submit/")
|
||||||
|
.form(formdata)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// It is possible to login
|
||||||
|
assert!(response.status().is_redirection());
|
||||||
|
let location = response.headers().get("location").unwrap();
|
||||||
|
assert_eq!("/admin/view/link/tpuah", location.to_str().unwrap());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let response = client
|
||||||
|
.get("http://localhost:8080/tpuah")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
|
||||||
|
// The basic redirection is working!
|
||||||
|
assert!(response.status().is_redirection());
|
||||||
|
let location = response.headers().get("location").unwrap();
|
||||||
|
assert!(location
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("https://das.geht/jetzt/"));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user