bazzar/api/src/main.rs

286 lines
7.7 KiB
Rust
Raw Normal View History

2022-04-19 16:49:30 +02:00
use std::io::Write;
2022-04-14 21:40:26 +02:00
use std::sync::Arc;
use actix::Actor;
2022-04-18 22:07:52 +02:00
use actix_session::storage::RedisActorSessionStore;
use actix_session::SessionMiddleware;
2022-04-14 21:40:26 +02:00
use actix_web::cookie::Key;
use actix_web::middleware::Logger;
use actix_web::web::Data;
use actix_web::{App, HttpServer};
2022-04-14 21:40:26 +02:00
use gumdrop::Options;
2022-04-20 14:30:59 +02:00
use jemallocator::Jemalloc;
2022-04-14 21:40:26 +02:00
use password_hash::SaltString;
use validator::{validate_email, validate_length};
2022-04-20 16:09:09 +02:00
use crate::actors::{database, order_manager, token_manager};
use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role};
2022-04-14 21:40:26 +02:00
pub mod actors;
pub mod logic;
pub mod model;
pub mod routes;
2022-04-20 14:30:59 +02:00
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
2022-04-14 21:40:26 +02:00
trait ResolveDbUrl {
fn own_db_url(&self) -> Option<String>;
fn db_url(&self) -> String {
self.own_db_url()
.or_else(|| std::env::var("DATABASE_URL").ok())
.unwrap_or_else(|| String::from("postgres://postgres@localhost/bazzar"))
}
}
2022-04-14 21:40:26 +02:00
#[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 {
2022-04-18 22:07:52 +02:00
Self {
help: false,
bind: "0.0.0.0".to_string(),
port: 8080,
db_url: None,
}
2022-04-14 21:40:26 +02:00
}
}
impl ResolveDbUrl for ServerOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
2022-04-14 21:40:26 +02:00
}
}
#[derive(Options, Debug)]
struct MigrateOpts {
help: bool,
db_url: Option<String>,
}
impl ResolveDbUrl for MigrateOpts {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
2022-04-14 21:40:26 +02:00
}
}
#[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 ResolveDbUrl for CreateAccountDefinition {
fn own_db_url(&self) -> Option<String> {
self.db_url.as_deref().map(String::from)
2022-04-14 21:40:26 +02:00
}
}
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<()> {
2022-04-17 22:15:09 +02:00
let secret_key = {
let key_secret = std::env::var("KEY_SECRET")
.expect("session requires secret key with 64 or more characters");
Key::from(key_secret.as_bytes())
};
2022-04-14 21:40:26 +02:00
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();
2022-04-18 22:07:52 +02:00
let token_manager = token_manager::TokenManager::new(db.clone()).start();
2022-04-20 16:09:09 +02:00
let order_manager = order_manager::OrderManager::new(db.clone()).start();
2022-04-14 21:40:26 +02:00
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.wrap(actix_web::middleware::Compress::default())
2022-04-16 12:48:38 +02:00
.wrap(actix_web::middleware::NormalizePath::trim())
2022-04-14 21:40:26 +02:00
.wrap(SessionMiddleware::new(
RedisActorSessionStore::new(redis_connection_string),
secret_key.clone(),
))
.app_data(Data::new(config.clone()))
.app_data(Data::new(db.clone()))
2022-04-18 22:07:52 +02:00
.app_data(Data::new(token_manager.clone()))
2022-04-20 16:09:09 +02:00
.app_data(Data::new(order_manager.clone()))
2022-04-14 21:40:26 +02:00
.configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok))
2022-04-14 21:40:26 +02:00
})
.bind((opts.bind, opts.port))
.map_err(Error::Boot)?
.run()
.await
.map_err(Error::Boot)
}
async fn migrate(opts: MigrateOpts) -> Result<()> {
2022-04-18 08:08:55 +02:00
use sqlx::migrate::MigrateError;
2022-04-14 21:40:26 +02:00
let db = database::Database::build(&opts.db_url()).await?;
2022-04-18 08:08:55 +02:00
let res: std::result::Result<(), MigrateError> =
sqlx::migrate!("../db/migrate").run(db.pool()).await;
match res {
2022-04-19 16:49:30 +02:00
Ok(()) => Ok(()),
2022-04-18 08:08:55 +02:00
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
2022-04-19 16:49:30 +02:00
}
2022-04-14 21:40:26 +02:00
}
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);
2022-04-18 22:07:52 +02:00
{
2022-04-19 16:49:30 +02:00
let mut std_out = std::io::stdout();
let std_in = std::io::stdin();
2022-04-18 22:07:52 +02:00
std_out
.write_all(b"PASS > ")
.expect("Failed to write to stdout");
std_out.flush().expect("Failed to write to stdout");
std_in.read_line(&mut s).map_err(Error::ReadPass)?;
}
if let Some(pos) = s.chars().position(|c| c == '\n') {
s.remove(pos);
}
2022-04-14 21:40:26 +02:00
s
}
};
2022-04-18 22:07:52 +02:00
if pass.trim().is_empty() {
panic!("Password cannot be empty!");
}
2022-04-14 21:40:26 +02:00
let config = Config::load();
2022-04-19 16:49:30 +02:00
let hash = encrypt_password(&Password::from(pass), &config.pass_salt).unwrap();
2022-04-14 21:40:26 +02:00
db.send(database::CreateAccount {
2022-04-19 16:49:30 +02:00
email: Email::from(opts.email),
login: Login::from(opts.login),
pass_hash: PassHash::from(hash),
2022-04-14 21:40:26 +02:00
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
}