351 lines
12 KiB
Rust
351 lines
12 KiB
Rust
#[macro_use]
|
|
extern crate diesel;
|
|
#[macro_use]
|
|
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 std::{fmt::Display, path::PathBuf, str::FromStr};
|
|
|
|
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
|
use actix_web::{web, App, HttpResponse, HttpServer};
|
|
|
|
use qrcode::types::QrError;
|
|
use tera::Tera;
|
|
|
|
#[derive(Debug)]
|
|
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 {
|
|
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::Diesel(e) => {
|
|
HttpResponse::InternalServerError().json(format!("Diesel Error: {:?}", e))
|
|
}
|
|
Self::Environment => HttpResponse::InternalServerError().json("Environment Error"),
|
|
Self::Template(e) => {
|
|
HttpResponse::InternalServerError().json(format!("Template Error: {:?}", e))
|
|
}
|
|
Self::Qr(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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<std::env::VarError> for ServerError {
|
|
fn from(e: std::env::VarError) -> Self {
|
|
eprintln!("Environment error {:?}", e);
|
|
Self::Environment
|
|
}
|
|
}
|
|
|
|
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 {
|
|
eprintln!("Database error {:?}", err);
|
|
Self::Diesel(err)
|
|
}
|
|
}
|
|
|
|
impl From<argonautica::Error> for ServerError {
|
|
fn from(e: argonautica::Error) -> Self {
|
|
eprintln!("Authentication error {:?}", e);
|
|
Self::Argonautic
|
|
}
|
|
}
|
|
impl From<tera::Error> for ServerError {
|
|
fn from(e: tera::Error) -> Self {
|
|
eprintln!("Template error {:?}", e);
|
|
Self::Template(e)
|
|
}
|
|
}
|
|
impl From<QrError> for ServerError {
|
|
fn from(e: QrError) -> Self {
|
|
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/");
|
|
|
|
fn build_tera() -> Tera {
|
|
let mut tera = Tera::default();
|
|
|
|
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"),
|
|
),
|
|
])
|
|
.expect("failed to parse templates");
|
|
tera
|
|
}
|
|
|
|
#[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);
|
|
|
|
slog_info!(
|
|
server_config.log,
|
|
"Running on: {}://{}/admin/login/",
|
|
&server_config.protocol,
|
|
host_port
|
|
);
|
|
slog_info!(
|
|
server_config.log,
|
|
"If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
|
|
&server_config.protocol,
|
|
&server_config.public_url
|
|
);
|
|
|
|
HttpServer::new(move || {
|
|
let tera = build_tera(); //Tera::new("templates/**/*").expect("failed to initialize the templates");
|
|
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)
|
|
.service(actix_web_static_files::ResourceFiles::new(
|
|
"/static", generated,
|
|
))
|
|
// directly go to the main page of Freie-Hochschule-Stuttgart
|
|
.route("/", web::get().to(views::redirect_fhs))
|
|
// 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_fhs)),
|
|
)
|
|
.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)),
|
|
)
|
|
.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)?
|
|
.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);
|
|
}
|
|
}
|
|
}
|