2022-04-14 21:40:26 +02:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
use actix::Actor;
|
|
|
|
use actix_session::{storage::RedisActorSessionStore, SessionMiddleware};
|
|
|
|
use actix_web::cookie::Key;
|
|
|
|
use actix_web::middleware::Logger;
|
|
|
|
use actix_web::web::Data;
|
|
|
|
use actix_web::{web, App, HttpResponse, HttpServer};
|
|
|
|
use gumdrop::Options;
|
|
|
|
use password_hash::SaltString;
|
|
|
|
use validator::{validate_email, validate_length};
|
|
|
|
|
|
|
|
use crate::actors::database;
|
|
|
|
use crate::logic::hash_pass;
|
|
|
|
use crate::model::{Email, Login, PassHash, Role};
|
|
|
|
|
|
|
|
pub mod actors;
|
|
|
|
pub mod logic;
|
|
|
|
pub mod model;
|
|
|
|
pub mod routes;
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub enum Error {
|
|
|
|
#[error("Failed to boot. {0:?}")]
|
|
|
|
Boot(std::io::Error),
|
|
|
|
#[error("Unable to read password file. {0:?}")]
|
|
|
|
PassFile(std::io::Error),
|
|
|
|
#[error("Unable to read password from STDIN. {0:?}")]
|
|
|
|
ReadPass(std::io::Error),
|
|
|
|
#[error("{0}")]
|
|
|
|
Database(#[from] database::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
|
|
|
|
#[derive(Options, Debug)]
|
|
|
|
struct Opts {
|
|
|
|
help: bool,
|
|
|
|
#[options(command)]
|
|
|
|
cmd: Option<Command>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Options, Debug)]
|
|
|
|
enum Command {
|
|
|
|
#[options(help = "Run server")]
|
|
|
|
Server(ServerOpts),
|
|
|
|
#[options(help = "Migrate database")]
|
|
|
|
Migrate(MigrateOpts),
|
|
|
|
#[options(help = "Generate new salt for passwords")]
|
|
|
|
GenerateHash(GenerateHashOpts),
|
|
|
|
#[options(help = "Create new account")]
|
|
|
|
CreateAccount(CreateAccountOpts),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Command {
|
|
|
|
fn default() -> Self {
|
|
|
|
Command::Server(ServerOpts::default())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Options, Debug)]
|
|
|
|
struct GenerateHashOpts {
|
|
|
|
help: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Options, Debug)]
|
|
|
|
struct ServerOpts {
|
|
|
|
help: bool,
|
|
|
|
bind: String,
|
|
|
|
port: u16,
|
|
|
|
db_url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for ServerOpts {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self { help: false, bind: "0.0.0.0".to_string(), port: 8080, db_url: None }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ServerOpts {
|
|
|
|
fn db_url(&self) -> String {
|
|
|
|
self.db_url
|
|
|
|
.as_deref()
|
|
|
|
.map(String::from)
|
|
|
|
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
|
|
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Options, Debug)]
|
|
|
|
struct MigrateOpts {
|
|
|
|
help: bool,
|
|
|
|
db_url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MigrateOpts {
|
|
|
|
fn db_url(&self) -> String {
|
|
|
|
self.db_url
|
|
|
|
.as_deref()
|
|
|
|
.map(String::from)
|
|
|
|
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
|
|
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Options)]
|
|
|
|
struct CreateAccountOpts {
|
|
|
|
help: bool,
|
|
|
|
#[options(command)]
|
|
|
|
cmd: Option<CreateAccountCmd>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Options)]
|
|
|
|
enum CreateAccountCmd {
|
|
|
|
Admin(CreateAccountDefinition),
|
|
|
|
User(CreateAccountDefinition),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Options)]
|
|
|
|
struct CreateAccountDefinition {
|
|
|
|
help: bool,
|
|
|
|
#[options(free)]
|
|
|
|
login: String,
|
|
|
|
#[options(free)]
|
|
|
|
email: String,
|
|
|
|
#[options(free)]
|
|
|
|
pass_file: Option<String>,
|
|
|
|
#[options(help = "Database url, it will also look for DATABASE_URL env")]
|
|
|
|
db_url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CreateAccountDefinition {
|
|
|
|
fn db_url(&self) -> String {
|
|
|
|
self.db_url
|
|
|
|
.as_deref()
|
|
|
|
.map(String::from)
|
|
|
|
.or_else(|| std::env::var("DATABASE_URL").ok())
|
|
|
|
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Config {
|
|
|
|
pass_salt: SaltString,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Config {
|
|
|
|
fn load() -> Self {
|
|
|
|
let pass_salt =
|
|
|
|
SaltString::new(&std::env::var("PASS_SALT").expect("PASS_SALT is required"))
|
|
|
|
.expect("Invalid password salt");
|
|
|
|
Self { pass_salt }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn server(opts: ServerOpts) -> Result<()> {
|
|
|
|
let secret_key = Key::generate();
|
|
|
|
let redis_connection_string = "127.0.0.1:6379";
|
|
|
|
|
|
|
|
let config = Arc::new(Config::load());
|
|
|
|
let db = database::Database::build(&opts.db_url()).await?.start();
|
|
|
|
|
|
|
|
HttpServer::new(move || {
|
|
|
|
App::new()
|
|
|
|
.wrap(Logger::default())
|
|
|
|
.wrap(actix_web::middleware::Compress::default())
|
|
|
|
.wrap(SessionMiddleware::new(
|
|
|
|
RedisActorSessionStore::new(redis_connection_string),
|
|
|
|
secret_key.clone(),
|
|
|
|
))
|
|
|
|
.app_data(Data::new(config.clone()))
|
|
|
|
.app_data(Data::new(db.clone()))
|
|
|
|
.configure(routes::configure)
|
|
|
|
.default_service(web::to(HttpResponse::Ok))
|
|
|
|
})
|
|
|
|
.bind((opts.bind, opts.port))
|
|
|
|
.map_err(Error::Boot)?
|
|
|
|
.run()
|
|
|
|
.await
|
|
|
|
.map_err(Error::Boot)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn migrate(opts: MigrateOpts) -> Result<()> {
|
|
|
|
let db = database::Database::build(&opts.db_url()).await?;
|
|
|
|
sqlx::migrate!("../db/migrate").run(db.pool()).await.expect("Failed to migrate");
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn generate_hash(_opts: GenerateHashOpts) -> Result<()> {
|
|
|
|
use argon2::password_hash::rand_core::OsRng;
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
|
|
println!("{salt}");
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
|
|
|
let (role, opts) = match opts.cmd.expect("Account type is mandatory") {
|
|
|
|
CreateAccountCmd::Admin(opts) => (Role::Admin, opts),
|
|
|
|
CreateAccountCmd::User(opts) => (Role::User, opts),
|
|
|
|
};
|
|
|
|
if !validate_email(&opts.email) {
|
|
|
|
panic!("Invalid email address");
|
|
|
|
}
|
|
|
|
if !validate_length(&opts.login, Some(4), Some(100), None) {
|
|
|
|
panic!("Login must have at least 4 characters and no more than 100");
|
|
|
|
}
|
|
|
|
let db = database::Database::build(&opts.db_url()).await?.start();
|
|
|
|
let pass = match opts.pass_file {
|
|
|
|
Some(path) => std::fs::read_to_string(path).map_err(Error::PassFile)?,
|
|
|
|
None => {
|
|
|
|
let mut s = String::with_capacity(100);
|
|
|
|
std::io::stdin().read_line(&mut s).map_err(Error::ReadPass)?;
|
|
|
|
s
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let config = Config::load();
|
|
|
|
let hash = hash_pass(&pass, &config.pass_salt).unwrap();
|
|
|
|
|
|
|
|
db.send(database::CreateAccount {
|
|
|
|
email: Email(opts.email),
|
|
|
|
login: Login(opts.login),
|
|
|
|
pass_hash: PassHash(hash),
|
|
|
|
role,
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
.unwrap()
|
|
|
|
.unwrap();
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[actix_web::main]
|
|
|
|
async fn main() -> Result<()> {
|
|
|
|
dotenv::dotenv().ok();
|
|
|
|
pretty_env_logger::init();
|
|
|
|
|
|
|
|
let opts: Opts = gumdrop::Options::parse_args_default_or_exit();
|
|
|
|
match opts.cmd.unwrap_or_default() {
|
|
|
|
Command::Migrate(opts) => migrate(opts).await,
|
|
|
|
Command::Server(opts) => server(opts).await,
|
|
|
|
Command::GenerateHash(opts) => generate_hash(opts).await,
|
|
|
|
Command::CreateAccount(opts) => create_account(opts).await,
|
|
|
|
}
|
2022-04-14 08:07:59 +02:00
|
|
|
}
|