Merge pull request #66 from Plume-org/setup-script

Setup script
This commit is contained in:
Baptiste Gelez
2018-06-20 09:53:26 +01:00
committed by GitHub
9 changed files with 306 additions and 88 deletions
+6 -16
View File
@@ -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<ConnectionManager<PgConnection>>;
/// Initializes a database pool.
fn init_pool() -> PgPool {
dotenv().ok();
let manager = ConnectionManager::<PgConnection>::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);
}))
+6 -6
View File
@@ -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
}
})
}
}
-27
View File
@@ -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<User>) -> Template {
}
}
#[get("/configure")]
fn configure() -> Template {
Template::render("instance/configure", json!({}))
}
#[derive(FromForm)]
struct NewInstanceForm {
name: String
}
#[post("/configure", data = "<data>")]
fn post_config(conn: DbConn, data: Form<NewInstanceForm>) -> 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 = "<data>")]
fn shared_inbox(conn: DbConn, data: String) -> String {
let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
+5 -6
View File
@@ -164,7 +164,6 @@ struct NewUserForm {
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: Form<NewUserForm>) -> Result<Redirect, String> {
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<NewUserForm>) -> Result<Redirect, String> {
} 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"))
+261
View File
@@ -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<ConnectionManager<PgConnection>>;
/// Initializes a database pool.
fn init_pool() -> Option<PgPool> {
dotenv().ok();
let manager = ConnectionManager::<PgConnection>::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<DbConn>) {
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>) -> 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");
}