diff --git a/.gitignore b/.gitignore index a4c7fe2b..925d54ce 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ rls rls translations po/*.po~ +.env diff --git a/Cargo.lock b/Cargo.lock index 1708ea54..fc705f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,14 @@ dependencies = [ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "colored" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "comrak" version = "0.2.12" @@ -999,6 +1007,7 @@ dependencies = [ "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "bcrypt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "comrak 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "diesel 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1015,6 +1024,7 @@ dependencies = [ "rocket_codegen 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", "rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", "rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)", + "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1282,6 +1292,16 @@ dependencies = [ "tera 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rpassword" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.7" @@ -1998,6 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" "checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" +"checksum colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b0aa3473e85a3161b59845d6096b289bb577874cafeaf75ea1b1beaa6572c7fc" "checksum comrak 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "053b26c8ce23b4c505a9479beace98f95899e0bf5c5255cf0219e9b0f48cf6ea" "checksum cookie 0.11.0-dev (git+https://github.com/alexcrichton/cookie-rs?rev=0365a18)" = "" "checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" @@ -2110,6 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "" "checksum rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "" "checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)" = "" +"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451" "checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "85fd9df495640643ad2d00443b3d78aae69802ad488debab4f1dd52fc1806ade" diff --git a/Cargo.toml b/Cargo.toml index 87c2e879..62865e72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ ammonia = "1.1.0" array_tool = "1.0" base64 = "0.9" bcrypt = "0.2" +colored = "1.6" comrak = "0.2" dotenv = "*" failure = "0.1" @@ -19,6 +20,7 @@ hyper = "*" lazy_static = "*" openssl = "0.10.6" reqwest = "0.8" +rpassword = "2.0" serde = "*" serde_derive = "1.0" serde_json = "1.0" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c82d7db0..391f3a72 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -42,46 +42,16 @@ Now, you can use the following command to start Postgres on a one-time basis. pg_ctl -D /usr/local/var/postgres start ``` -After starting Postgres, we need to enter [PSQL](http://postgresguide.com/utilities/psql.html), the interactive terminal for running postgres queries. We'll be running this as the user `postgres` which is an admin-type postgres user. - -``` -psql postgres -``` - -Now that you are in psql, enter the following queries to prepare the database for Plume. - -``` -CREATE DATABASE plume; -CREATE USER plume WITH PASSWORD 'plume'; -GRANT ALL PRIVILEGES ON DATABASE plume to plume; -\q -``` - -The final command `\q` lets us exit psql and returns us to the Terminal. Now, we will open psql again, this time as the `plume` user we just created. Then we'll give all privileges on all tables and sequences to our `plume` user. This is for local development use only and it's not recommend to give complete access to this user in a production environment. - -``` -psql plume -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO plume; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO plume; -\q -``` +When you will launch Plume for the first time, it will setup the database by itself. #### Database Migration -Now that the Postgres database is set up and the `plume` user has the privileges it needs, we can set up the database using the diesel CLI. If this was your time installing Rust, you -will probably need to run that using `cargo`. `cargo` is installed with `rustc` so if you followed the earlier instructions it will already be available. +To run migrations and correctly setup the database, Plume use the `diesel` CLI tool under the hood. Therefore you should install it before running Plume. If this was your time installing Rust, you will probably need to run that using `cargo`. `cargo` is installed with `rustc` so if you followed the earlier instructions it will already be available. ``` cargo install diesel_cli ``` -The first time you run this, you can run setup. After that, every time you pull the repository you will want to run the migration command in case there were any migrations. Those commands are - -``` -diesel setup --database-url='postgres://localhost/plume' -diesel migration run --database-url='postgres://localhost/plume' -``` - #### Running Plume To run Plume locally, make sure you are once again in the Plume directory, such as `~/dev/Plume`. Now you will be able to run the application using the command @@ -92,7 +62,7 @@ cargo run #### Configuration -Now Plume should be running on your machine at [http://localhost:8000](http://localhost:8000). The first time you run the application, you'll want to configure your blog name on the [http://localhost:8000/configure](http://localhost:8000/configure) page. You'll be able to change this name later. +The first time you'll run Plume, it will help you setup your instance through an interactive tool. Once you'll have answered all its question, your instance will start. #### Testing the federation diff --git a/src/main.rs b/src/main.rs index 5455d854..57ecff65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ extern crate array_tool; extern crate base64; extern crate bcrypt; extern crate chrono; +extern crate colored; extern crate comrak; extern crate failure; #[macro_use] @@ -26,6 +27,7 @@ extern crate reqwest; extern crate rocket; extern crate rocket_contrib; extern crate rocket_i18n; +extern crate rpassword; extern crate serde; #[macro_use] extern crate serde_derive; @@ -35,18 +37,17 @@ extern crate tera; extern crate url; extern crate webfinger; -use diesel::{pg::PgConnection, r2d2::{ConnectionManager, Pool}}; -use dotenv::dotenv; use rocket_contrib::Template; use std::env; mod activity_pub; mod db_conn; mod models; +mod safe_string; mod schema; +mod setup; mod routes; mod utils; -mod safe_string; lazy_static! { pub static ref BASE_URL: String = env::var("BASE_URL") @@ -56,17 +57,8 @@ lazy_static! { .unwrap_or(format!("postgres://plume:plume@localhost/{}", env::var("DB_NAME").unwrap_or(String::from("plume")))); } -type PgPool = Pool>; - -/// Initializes a database pool. -fn init_pool() -> PgPool { - dotenv().ok(); - - let manager = ConnectionManager::::new(DB_URL.as_str()); - Pool::new(manager).expect("DB pool error") -} - fn main() { + let pool = setup::check(); rocket::ignite() .mount("/", routes![ routes::blogs::details, @@ -81,8 +73,6 @@ fn main() { routes::comments::create, routes::instance::index, - routes::instance::configure, - routes::instance::post_config, routes::instance::shared_inbox, routes::instance::nodeinfo, @@ -133,7 +123,7 @@ fn main() { routes::errors::not_found, routes::errors::server_error ]) - .manage(init_pool()) + .manage(pool) .attach(Template::custom(|engines| { rocket_i18n::tera(&mut engines.tera); })) diff --git a/src/models/users.rs b/src/models/users.rs index 64b6ddd2..c0dc4902 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -484,16 +484,16 @@ impl Signer for User { impl NewUser { /// Creates a new local user pub fn new_local( + conn: &PgConnection, username: String, display_name: String, is_admin: bool, summary: String, email: String, - password: String, - instance_id: i32 - ) -> NewUser { + password: String + ) -> User { let (pub_key, priv_key) = gen_keypair(); - NewUser { + User::insert(conn, NewUser { username: username, display_name: display_name, outbox_url: String::from(""), @@ -502,11 +502,11 @@ impl NewUser { summary: SafeString::new(&summary), email: Some(email), hashed_password: Some(password), - instance_id: instance_id, + instance_id: Instance::local_id(conn), ap_url: String::from(""), public_key: String::from_utf8(pub_key).unwrap(), private_key: Some(String::from_utf8(priv_key).unwrap()), shared_inbox_url: None - } + }) } } diff --git a/src/routes/instance.rs b/src/routes/instance.rs index be1deb42..9463511f 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -1,9 +1,7 @@ use gettextrs::gettext; -use rocket::{request::Form, response::Redirect}; use rocket_contrib::{Json, Template}; use serde_json; -use BASE_URL; use activity_pub::inbox::Inbox; use db_conn::DbConn; use models::{ @@ -33,31 +31,6 @@ fn index(conn: DbConn, user: Option) -> Template { } } -#[get("/configure")] -fn configure() -> Template { - Template::render("instance/configure", json!({})) -} - -#[derive(FromForm)] -struct NewInstanceForm { - name: String -} - -#[post("/configure", data = "")] -fn post_config(conn: DbConn, data: Form) -> Redirect { - let form = data.get(); - let inst = Instance::insert(&*conn, NewInstance { - public_domain: BASE_URL.as_str().to_string(), - name: form.name.to_string(), - local: true - }); - if inst.has_admin(&*conn) { - Redirect::to("/") - } else { - Redirect::to("/users/new") - } -} - #[post("/inbox", data = "")] fn shared_inbox(conn: DbConn, data: String) -> String { let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); diff --git a/src/routes/user.rs b/src/routes/user.rs index a8d24f80..7a778469 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -164,7 +164,6 @@ struct NewUserForm { #[post("/users/new", data = "")] fn create(conn: DbConn, data: Form) -> Result { - let inst = Instance::get_local(&*conn).unwrap(); let form = data.get(); if form.username.clone().len() < 1 { @@ -174,15 +173,15 @@ fn create(conn: DbConn, data: Form) -> Result { } else if form.password.clone().len() < 8 { Err(String::from("Password should be at least 8 characters long")) } else if form.password == form.password_confirmation { - User::insert(&*conn, NewUser::new_local( + NewUser::new_local( + &*conn, form.username.to_string(), form.username.to_string(), - !inst.has_admin(&*conn), + false, String::from(""), form.email.to_string(), - User::hash_pass(form.password.to_string()), - inst.id - )).update_boxes(&*conn); + User::hash_pass(form.password.to_string()) + ).update_boxes(&*conn); Ok(Redirect::to(uri!(super::session::new))) } else { Err(String::from("Passwords don't match")) diff --git a/src/setup.rs b/src/setup.rs new file mode 100644 index 00000000..da366d09 --- /dev/null +++ b/src/setup.rs @@ -0,0 +1,261 @@ +use colored::Colorize; +use diesel::{pg::PgConnection, r2d2::{ConnectionManager, Pool}}; +use dotenv::dotenv; +use std::fs::{self, File}; +use std::io; +use std::path::Path; +use std::process::{exit, Command}; +use rpassword; + +use DB_URL; +use db_conn::DbConn; +use models::instance::*; +use models::users::*; + +type PgPool = Pool>; + +/// Initializes a database pool. +fn init_pool() -> Option { + dotenv().ok(); + + let manager = ConnectionManager::::new(DB_URL.as_str()); + Pool::new(manager).ok() +} + +pub fn check() -> PgPool { + if let Some(pool) = init_pool() { + match pool.get() { + Ok(conn) => { + let db_conn = DbConn(conn); + if Instance::get_local(&*db_conn).is_none() { + run_setup(Some(db_conn)); + } + } + Err(_) => panic!("Couldn't connect to database") + } + migrate(); + pool + } else { + run_setup(None); + init_pool().unwrap() + } +} + +fn run_setup(conn: Option) { + println!("\n\n"); + println!("{}\n{}\n{}\n\n{}", + "Welcome in the Plume setup tool.".magenta(), + "It will help you setup your new instance, by asking you a few questions.".magenta(), + "Then you'll be able to enjoy Plume!".magenta(), + "First let's check that you have all the required dependencies. Press Enter to start." + ); + read_line(); + check_native_deps(); + let conn = setup_db(conn); + setup_type(conn); + dotenv().ok(); + + println!("{}\n{}\n{}", + "Your Plume instance is now ready to be used.".magenta(), + "We hope you will enjoy it.".magenta(), + "If you ever encounter a problem, feel free to report it at https://github.com/Plume-org/Plume/issues/".magenta(), + ); + + println!("\nPress Enter to start it.\n"); +} + +fn setup_db(conn: Option) -> DbConn { + write_to_dotenv("DB_URL", DB_URL.as_str().to_string()); + + match conn { + Some(conn) => conn, + None => { + println!("\n{}\n", "We are going to setup the database.".magenta()); + println!("{}\n", "About to create a new PostgreSQL user named 'plume'".blue()); + Command::new("createuser") + .arg("-d") + .arg("-P") + .arg("plume") + .status() + .map(|s| { + if s.success() { + println!("{}\n", " ✔️ Done".green()); + } + }) + .expect("Couldn't create new user"); + + println!("{}\n", "About to create a new PostgreSQL database named 'plume'".blue()); + Command::new("createdb") + .arg("-O") + .arg("plume") + .arg("plume") + .status() + .map(|s| { + if s.success() { + println!("{}\n", " ✔️ Done".green()); + } + }) + .expect("Couldn't create new table"); + + migrate(); + + init_pool() + .expect("Couldn't init DB pool") + .get() + .map(|c| DbConn(c)) + .expect("Couldn't connect to the database") + } + } +} + +fn migrate() { + println!("{}\n", "Running migrations…".blue()); + Command::new("diesel") + .arg("migration") + .arg("run") + .arg("--database-url") + .arg(DB_URL.as_str()) + .status() + .map(|s| { + if s.success() { + println!("{}\n", " ✔️ Done".green()); + } + }) + .expect("Couldn't run migrations"); +} + +fn setup_type(conn: DbConn) { + println!("\nDo you prefer a simple setup, or to customize everything?\n"); + println!(" 1 - Simple setup"); + println!(" 2 - Complete setup"); + match read_line().as_ref() { + "Simple" | "simple" | "s" | "S" | + "1" => quick_setup(conn), + "Complete" | "complete" | "c" | "C" | + "2" => complete_setup(conn), + x => { + println!("Invalid choice. Choose between '1' or '2'. {}", x); + setup_type(conn); + } + } +} + +fn quick_setup(conn: DbConn) { + println!("What is your instance domain?"); + let domain = read_line(); + write_to_dotenv("BASE_URL", domain.clone()); + + println!("\nWhat is your instance name?"); + let name = read_line(); + + let instance = Instance::insert(&*conn, NewInstance { + public_domain: domain, + name: name, + local: true + }); + + println!("{}\n", " ✔️ Your instance was succesfully created!".green()); + + // Generate Rocket secret key. + let key = Command::new("openssl") + .arg("rand") + .arg("-base64") + .arg("32") + .output() + .map(|o| String::from_utf8(o.stdout).expect("Invalid output from openssl")) + .expect("Couldn't generate secret key."); + write_to_dotenv("ROCKET_SECRET_KEY", key); + + create_admin(instance, conn); +} + +fn complete_setup(conn: DbConn) { + quick_setup(conn); + + println!("\nOn which port should Plume listen? (default: 7878)"); + let port = read_line_or("7878"); + write_to_dotenv("ROCKET_PORT", port); + + println!("\nOn which address should Plume listen? (default: 0.0.0.0)"); + let address = read_line_or("0.0.0.0"); + write_to_dotenv("ROCKET_ADDRESS", address); +} + +fn create_admin(instance: Instance, conn: DbConn) { + println!("{}\n\n", "You are now about to create your admin account".magenta()); + + println!("What is your username? (default: admin)"); + let name = read_line_or("admin"); + + println!("What is your email?"); + let email = read_line(); + + println!("What is your password?"); + let password = rpassword::read_password().expect("Couldn't read your password."); + + NewUser::new_local( + &*conn, + name.clone(), + name, + true, + format!("Admin of {}", instance.name), + email, + User::hash_pass(password), + ).update_boxes(&*conn); + + println!("{}\n", " ✔️ Your account was succesfully created!".green()); +} + +fn check_native_deps() { + let mut not_found = Vec::new(); + if !try_run("psql") { + not_found.push(("PostgreSQL", "sudo apt install postgres")); + } + if !try_run("gettext") { + not_found.push(("GetText", "sudo apt install gettext")) + } + if !try_run("diesel") { + not_found.push(("Diesel CLI", "cargo install diesel_cli")) + } + + if not_found.len() > 0 { + println!("{}\n", "Some native dependencies are missing:".red()); + for (dep, install) in not_found.into_iter() { + println!("{}", format!(" - {} (can be installed with `{}`, on Debian based distributions)", dep, install).red()) + } + println!("\nRetry once you have installed them."); + exit(1); + } else { + println!("{}", " ✔️ All native dependencies are present.".green()) + } +} + +fn try_run(command: &'static str) -> bool { + Command::new(command) + .output() + .is_ok() +} + +fn read_line() -> String { + let mut input = String::new(); + io::stdin().read_line(&mut input).expect("Unable to read line"); + input.retain(|c| c != '\n'); + input +} + +fn read_line_or(or: &str) -> String { + let input = read_line(); + if input.len() == 0 { + or.to_string() + } else { + input + } +} + +fn write_to_dotenv(var: &'static str, val: String) { + if !Path::new(".env").exists() { + File::create(".env").expect("Error while creating .env file"); + } + + fs::write(".env", format!("{}\n{}={}", fs::read_to_string(".env").expect("Unable to read .env"), var, val)).expect("Unable to write .env"); +}