[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: cargo\n  directory: \"/\"\n  schedule:\n    interval: daily\n    time: \"04:00\"\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".gitignore",
    "content": "target\nCargo.lock\n.env\n.idea"
  },
  {
    "path": ".travis.yml",
    "content": "language: rust\n\nrust:\n  - stable\n  - beta\n  - nightly\n\nenv:\n  - DATABASE_NAME=boilerplateapp DATABASE_URL=postgres://localhost/boilerplateapp\n\nservices:\n  - postgresql\n\nmatrix:\n  allow_failures:\n    - rust: stable\n    - rust: beta\n\nbefore_script:\n  - cargo install --force diesel_cli\n  - cp .env.example .env\n  - ./reset.sh\n  - |\n    if [[ \"$TRAVIS_RUST_VERSION\" == nightly && \"$TRAVIS_OS_NAME\" == \"linux\" ]]; then\n      RUSTFLAGS=\"--cfg procmacro2_semver_exempt\" cargo install cargo-tarpaulin\n    fi\n\nscript:\n  - cargo build --verbose\n  - cargo test --verbose\n\nafter_success: |\n  if [[ \"$TRAVIS_RUST_VERSION\" == nightly && \"$TRAVIS_OS_NAME\" == \"linux\" ]]; then\n    cargo tarpaulin --out Xml\n    bash <(curl -s https://codecov.io/bash)\n  fi\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"rust-web-boilerplate\"\nversion = \"0.1.0\"\nauthors = [\"Sven-Hendrik Haase <svenstaro@gmail.com>\"]\nedition = \"2018\"\n\n[lib]\nname = \"rust_web_boilerplate\"\npath = \"src/lib.rs\"\n\n[dependencies]\nuuid = { version = \"0.8\", features = [\"serde\", \"v4\"] }\nchrono = { version = \"0.4\", features = [\"serde\"] }\nargon2rs = \"0.2\"\nrocket = \"0.4\"\ndiesel = { version = \"1.4\", features = [\"postgres\", \"uuidv07\", \"chrono\", \"serde_json\"] }\ndotenv = \"0.15\"\nserde = \"1\"\nserde_json = \"1\"\nserde_derive = \"1\"\nvalidator = \"0.16\"\nvalidator_derive = \"0.16\"\nring = \"0.13\"\nrand = \"0.7\"\n\n[dev-dependencies]\nquickcheck = \"0.9\"\nspeculate = \"0.1\"\nparking_lot = { version = \"0.12\", features = [\"nightly\"] }\n\n[dependencies.rocket_contrib]\nversion = \"0.4\"\ndefault-features = false\nfeatures = [\"json\", \"diesel_postgres_pool\"]\n\n[features]\ndefault = []\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Sven-Hendrik Haase\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Rust Web Boilerplate\n\n[![Build Status](https://travis-ci.org/svenstaro/rust-web-boilerplate.svg?branch=master)](https://travis-ci.org/svenstaro/rust-web-boilerplate)\n[![codecov](https://codecov.io/gh/svenstaro/rust-web-boilerplate/branch/master/graph/badge.svg)](https://codecov.io/gh/svenstaro/rust-web-boilerplate)\n[![lines of code](https://tokei.rs/b1/github/svenstaro/rust-web-boilerplate)](https://github.com/svenstaro/rust-web-boilerplate)\n[![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/svenstaro/rust-web-boilerplate/blob/master/LICENSE)\n\n\n## About\nThis is a boilerplate project made using best practices for getting started quickly\nin a new project. I made this for myself but maybe it will help someone else. Pull\nrequests and discussions on best practices welcome!\n\n## Development setup\n\nInstall a few external dependencies and make sure `~/.cargo/bin` is in your `$PATH`:\n\n    cargo install diesel_cli\n    cargo install cargo-watch\n\nOptionally if you want line coverage from your tests, install cargo-tarpaulin:\n\n    cargo-tarpaulin\n\nCopy `.env.example` to `.env` and update your application environment in this file.\n\nMake sure you have a working local postgres setup. Your current user should be\nadmin in your development postgres installation and it should use the \"peer\" or\n\"trust\" auth methods (see `pg_hba.conf`).\n\nNow you can launch the `watch.sh` script which helps you quickly iterate. It\nwill remove and recreate the DB and run the migrations and then the tests on\nall code changes.\n\n    ./watch.sh\n\nTo get line coverage, do\n\n    cargo tarpaulin --ignore-tests\n"
  },
  {
    "path": "diesel.toml",
    "content": "# For documentation on how to configure this file,\n# see diesel.rs/guides/configuring-diesel-cli\n\n[print_schema]\nfile = \"src/schema.rs\"\nwith_docs = true\nimport_types = [\"diesel::sql_types::*\"]\n"
  },
  {
    "path": "migrations/.gitkeep",
    "content": ""
  },
  {
    "path": "migrations/00000000000000_diesel_initial_setup/down.sql",
    "content": "DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);\nDROP FUNCTION IF EXISTS diesel_set_updated_at();\n"
  },
  {
    "path": "migrations/00000000000000_diesel_initial_setup/up.sql",
    "content": "-- This file was automatically created by Diesel to setup helper functions\n-- and other internal bookkeeping. This file is safe to edit, any future\n-- changes will be added to existing projects as new migrations.\n\n\n\n\n-- Sets up a trigger for the given table to automatically set a column called\n-- `updated_at` whenever the row is modified (unless `updated_at` was included\n-- in the modified columns)\n--\n-- # Example\n--\n-- ```sql\n-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());\n--\n-- SELECT diesel_manage_updated_at('users');\n-- ```\nCREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$\nBEGIN\n    EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s\n                    FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$\nBEGIN\n    IF (\n        NEW IS DISTINCT FROM OLD AND\n        NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at\n    ) THEN\n        NEW.updated_at := current_timestamp;\n    END IF;\n    RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n"
  },
  {
    "path": "migrations/20170211131857_create_initial_db/down.sql",
    "content": "DROP TABLE users\n"
  },
  {
    "path": "migrations/20170211131857_create_initial_db/up.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n\nCREATE TABLE users (\n    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n    created_at TIMESTAMP DEFAULT current_timestamp NOT NULL,\n    updated_at TIMESTAMP DEFAULT current_timestamp NOT NULL,\n    email VARCHAR(120) UNIQUE NOT NULL,\n    password_hash BYTEA NOT NULL,\n    current_auth_token VARCHAR(32),\n    last_action TIMESTAMP\n);\nSELECT diesel_manage_updated_at('users');\n\nCREATE UNIQUE INDEX email_idx ON users(email);\nCREATE UNIQUE INDEX current_auth_token_idx ON users(current_auth_token);\n"
  },
  {
    "path": "reset.sh",
    "content": "#!/bin/bash\n\ndropdb --if-exists ${DATABASE_NAME}\ndiesel setup --database-url ${DATABASE_URL}\n"
  },
  {
    "path": "src/api/auth.rs",
    "content": "use diesel;\nuse diesel::prelude::*;\nuse rocket::{post, State};\nuse rocket_contrib::json;\nuse rocket_contrib::json::{Json, JsonValue};\n\nuse crate::config::AppConfig;\nuse crate::database::DbConn;\nuse crate::models::user::{NewUser, UserModel};\nuse crate::responses::{\n    conflict, created, internal_server_error, ok, unauthorized, unprocessable_entity, APIResponse,\n};\nuse crate::schema::users;\nuse crate::schema::users::dsl::*;\nuse crate::validation::user::UserLogin;\n\n/// Log the user in and return a response with an auth token.\n///\n/// Return UNAUTHORIZED in case the user can't be found or if the password is incorrect.\n#[post(\"/login\", data = \"<user_in>\", format = \"application/json\")]\npub fn login(\n    user_in: Json<UserLogin>,\n    app_config: State<AppConfig>,\n    db: DbConn,\n) -> Result<APIResponse, APIResponse> {\n    let user_q = users\n        .filter(email.eq(&user_in.email))\n        .first::<UserModel>(&*db)\n        .optional()?;\n\n    // For privacy reasons, we'll not provide the exact reason for failure here (although this\n    // could probably be timing attacked to find out whether users exist or not.\n    let mut user =\n        user_q.ok_or_else(|| unauthorized().message(\"Username or password incorrect.\"))?;\n\n    if !user.verify_password(user_in.password.as_str()) {\n        return Err(unauthorized().message(\"Username or password incorrect.\"));\n    }\n\n    let token = if user.has_valid_auth_token(app_config.auth_token_timeout_days) {\n        user.current_auth_token.ok_or_else(internal_server_error)?\n    } else {\n        user.generate_auth_token(&db)?\n    };\n\n    Ok(ok().data(json!({\n        \"user_id\": user.id,\n        \"token\": token,\n    })))\n}\n\n/// Register a new user using email and password.\n///\n/// Return CONFLICT is a user with the same email already exists.\n#[post(\"/register\", data = \"<user>\", format = \"application/json\")]\npub fn register(\n    user: Result<UserLogin, JsonValue>,\n    db: DbConn,\n) -> Result<APIResponse, APIResponse> {\n    let user_data = user.map_err(unprocessable_entity)?;\n\n    let new_password_hash = UserModel::make_password_hash(user_data.password.as_str());\n    let new_user = NewUser {\n        email: user_data.email.clone(),\n        password_hash: new_password_hash,\n    };\n\n    let insert_result = diesel::insert_into(users::table)\n        .values(&new_user)\n        .get_result::<UserModel>(&*db);\n    if let Err(diesel::result::Error::DatabaseError(\n        diesel::result::DatabaseErrorKind::UniqueViolation,\n        _,\n    )) = insert_result\n    {\n        return Err(conflict().message(\"User already exists.\"));\n    }\n\n    let user = insert_result?;\n    Ok(created().data(json!(&user)))\n}\n"
  },
  {
    "path": "src/api/hello.rs",
    "content": "use rocket::get;\nuse rocket_contrib::json;\n\nuse crate::models::user::UserModel;\nuse crate::responses::{ok, APIResponse};\n\n#[get(\"/whoami\")]\npub fn whoami(current_user: UserModel) -> APIResponse {\n    ok().data(json!(current_user.email))\n}\n"
  },
  {
    "path": "src/api/mod.rs",
    "content": "pub mod hello;\npub mod auth;\n"
  },
  {
    "path": "src/bin/runner.rs",
    "content": "use dotenv::dotenv;\nuse std::env;\n\nfn main() -> Result<(), String> {\n    dotenv().ok();\n\n    let config_name = env::var(\"CONFIG_ENV\").expect(\"CONFIG must be set\");\n    let rocket = rust_web_boilerplate::rocket_factory(&config_name)?;\n    rocket.launch();\n    Ok(())\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use rocket::config::{Config, ConfigError, Environment, Value};\nuse std::env;\nuse std::collections::HashMap;\nuse chrono::Duration;\n\n\n#[derive(Debug)]\npub struct AppConfig {\n    pub auth_token_timeout_days: Duration,\n    pub cors_allow_origin: String,\n    pub cors_allow_methods: String,\n    pub cors_allow_headers: String,\n    pub environment_name: String,\n}\n\nimpl Default for AppConfig {\n    fn default() -> AppConfig {\n        AppConfig {\n            auth_token_timeout_days: Duration::days(30),\n            cors_allow_origin: String::from(\"*\"),\n            cors_allow_methods: String::from(\"*\"),\n            cors_allow_headers: String::from(\"*\"),\n            environment_name: String::from(\"unconfigured\"),\n        }\n    }\n}\n\n\n/// Return a tuple of an app-specific config and a Rocket config.\npub fn get_rocket_config(config_name: &str) -> Result<(AppConfig, Config), ConfigError> {\n    fn production_config() -> Result<(AppConfig, Config), ConfigError> {\n        let app_config = AppConfig {\n            cors_allow_origin: String::from(\"https://example.com\"),\n            environment_name: String::from(\"production\"),\n            ..Default::default()\n        };\n\n        let mut database_config = HashMap::new();\n        let mut databases = HashMap::new();\n        database_config.insert(\"url\", Value::from(env::var(\"DATABASE_URL\").unwrap()));\n        databases.insert(\"postgres_db\", Value::from(database_config));\n\n        let rocket_config = Config::build(Environment::Production)\n            .address(\"0.0.0.0\")\n            .port(8080)\n            .extra(\"databases\", databases)\n            .finalize()?;\n\n        Ok((app_config, rocket_config))\n    }\n\n    fn staging_config() -> Result<(AppConfig, Config), ConfigError> {\n        let app_config = AppConfig {\n            cors_allow_origin: String::from(\"https://staging.example.com\"),\n            environment_name: String::from(\"staging\"),\n            ..Default::default()\n        };\n\n        let mut database_config = HashMap::new();\n        let mut databases = HashMap::new();\n        database_config.insert(\"url\", Value::from(env::var(\"DATABASE_URL\").unwrap()));\n        databases.insert(\"postgres_db\", Value::from(database_config));\n\n        let rocket_config = Config::build(Environment::Staging)\n            .address(\"0.0.0.0\")\n            .port(8080)\n            .extra(\"databases\", databases)\n            .finalize()?;\n\n        Ok((app_config, rocket_config))\n    }\n\n    fn develop_config() -> Result<(AppConfig, Config), ConfigError> {\n        let app_config = AppConfig {\n            cors_allow_origin: String::from(\"https://develop.example.com\"),\n            environment_name: String::from(\"develop\"),\n            ..Default::default()\n        };\n\n        let mut database_config = HashMap::new();\n        let mut databases = HashMap::new();\n        database_config.insert(\"url\", Value::from(env::var(\"DATABASE_URL\").unwrap()));\n        databases.insert(\"postgres_db\", Value::from(database_config));\n\n        let rocket_config = Config::build(Environment::Staging)\n            .address(\"0.0.0.0\")\n            .port(8080)\n            .extra(\"databases\", databases)\n            .finalize()?;\n\n        Ok((app_config, rocket_config))\n    }\n\n    fn testing_config() -> Result<(AppConfig, Config), ConfigError> {\n        let app_config = AppConfig {\n            environment_name: String::from(\"testing\"),\n            ..Default::default()\n        };\n\n        let mut database_config = HashMap::new();\n        let mut databases = HashMap::new();\n        database_config.insert(\"url\", Value::from(env::var(\"DATABASE_URL\").unwrap()));\n        databases.insert(\"postgres_db\", Value::from(database_config));\n\n        let rocket_config = Config::build(Environment::Staging)\n            .address(\"0.0.0.0\")\n            .port(5000)\n            .extra(\"databases\", databases)\n            .finalize()?;\n\n        Ok((app_config, rocket_config))\n    }\n\n    fn local_config() -> Result<(AppConfig, Config), ConfigError> {\n        let app_config = AppConfig {\n            environment_name: String::from(\"local\"),\n            ..Default::default()\n        };\n\n        let mut database_config = HashMap::new();\n        let mut databases = HashMap::new();\n        database_config.insert(\"url\", Value::from(env::var(\"DATABASE_URL\").unwrap()));\n        databases.insert(\"postgres_db\", Value::from(database_config));\n\n        let rocket_config = Config::build(Environment::Staging)\n            .address(\"0.0.0.0\")\n            .port(5000)\n            .extra(\"databases\", databases)\n            .finalize()?;\n\n        Ok((app_config, rocket_config))\n    }\n\n    match config_name {\n        \"production\" => production_config(),\n        \"staging\" => staging_config(),\n        \"develop\" => develop_config(),\n        \"testing\" => testing_config(),\n        \"local\" => local_config(),\n        _ => Err(ConfigError::BadEnv(format!(\n            \"No valid config chosen: {}\",\n            config_name\n        ))),\n    }\n}\n"
  },
  {
    "path": "src/database.rs",
    "content": "use rocket_contrib::database;\n\n#[database(\"postgres_db\")]\npub struct DbConn(diesel::PgConnection);\n"
  },
  {
    "path": "src/handlers.rs",
    "content": "use rocket::http::Status;\nuse rocket::request::{self, FromRequest, Request};\nuse rocket::{catch, Outcome};\n\nuse crate::database::DbConn;\n\nuse crate::responses::{\n    bad_request, forbidden, internal_server_error, not_found, service_unavailable, unauthorized,\n    APIResponse,\n};\nuse crate::models::user::UserModel;\n\n#[catch(400)]\npub fn bad_request_handler() -> APIResponse {\n    bad_request()\n}\n\n#[catch(401)]\npub fn unauthorized_handler() -> APIResponse {\n    unauthorized()\n}\n\n#[catch(403)]\npub fn forbidden_handler() -> APIResponse {\n    forbidden()\n}\n\n#[catch(404)]\npub fn not_found_handler() -> APIResponse {\n    not_found()\n}\n\n#[catch(500)]\npub fn internal_server_error_handler() -> APIResponse {\n    internal_server_error()\n}\n\n#[catch(503)]\npub fn service_unavailable_handler() -> APIResponse {\n    service_unavailable()\n}\n\nimpl<'a, 'r> FromRequest<'a, 'r> for UserModel {\n    type Error = ();\n\n    fn from_request(request: &'a Request<'r>) -> request::Outcome<UserModel, ()> {\n        let db = <DbConn as FromRequest>::from_request(request)?;\n        let keys: Vec<_> = request.headers().get(\"Authorization\").collect();\n        if keys.len() != 1 {\n            return Outcome::Failure((Status::BadRequest, ()));\n        };\n\n        let token_header = keys[0];\n        let token = token_header.replace(\"Bearer \", \"\");\n\n        match UserModel::get_user_from_login_token(&token, &*db) {\n            Some(user) => Outcome::Success(user),\n            None => Outcome::Failure((Status::Unauthorized, ())),\n        }\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "#![feature(proc_macro_hygiene, decl_macro)]\n#![recursion_limit = \"128\"]\n\n// Keep the pre-2018 style [macro_use] for diesel because it's annoying otherwise:\n// https://github.com/diesel-rs/diesel/issues/1764\n#[macro_use]\nextern crate diesel;\n\nuse rocket::{catchers, routes};\n\npub mod api;\npub mod config;\npub mod database;\npub mod handlers;\npub mod models;\npub mod responses;\npub mod schema;\npub mod validation;\n\n/// Constructs a new Rocket instance.\n///\n/// This function takes care of attaching all routes and handlers of the application.\npub fn rocket_factory(config_name: &str) -> Result<rocket::Rocket, String> {\n    let (app_config, rocket_config) =\n        config::get_rocket_config(config_name).map_err(|x| format!(\"{}\", x))?;\n    let rocket = rocket::custom(rocket_config)\n        .attach(database::DbConn::fairing())\n        .manage(app_config)\n        .mount(\"/hello/\", routes![api::hello::whoami])\n        .mount(\"/auth/\", routes![api::auth::login, api::auth::register,])\n        .register(catchers![\n            handlers::bad_request_handler,\n            handlers::unauthorized_handler,\n            handlers::forbidden_handler,\n            handlers::not_found_handler,\n            handlers::internal_server_error_handler,\n            handlers::service_unavailable_handler,\n        ]);\n    Ok(rocket)\n}\n"
  },
  {
    "path": "src/models/mod.rs",
    "content": "pub mod user;\n"
  },
  {
    "path": "src/models/user.rs",
    "content": "// TODO: Silence this until diesel 1.4.\n// See https://github.com/diesel-rs/diesel/issues/1785#issuecomment-422579609.\n#![allow(proc_macro_derive_resolution_fallback)]\n\nuse std::fmt;\n\nuse argon2rs::argon2i_simple;\nuse chrono::{Duration, NaiveDateTime, Utc};\nuse diesel::pg::PgConnection;\nuse diesel::prelude::*;\nuse diesel::result::Error as DieselError;\nuse rand::distributions::Alphanumeric;\nuse rand::{Rng, thread_rng};\nuse ring::constant_time::verify_slices_are_equal;\nuse serde_derive::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::schema::users;\n\n#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable, AsChangeset)]\n#[table_name = \"users\"]\npub struct UserModel {\n    pub id: Uuid,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n    pub email: String,\n    pub password_hash: Vec<u8>,\n    pub current_auth_token: Option<String>,\n    pub last_action: Option<NaiveDateTime>,\n}\n\n#[derive(Insertable)]\n#[table_name = \"users\"]\npub struct NewUser {\n    pub email: String,\n    pub password_hash: Vec<u8>,\n}\n\nimpl fmt::Display for UserModel {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"<User {email}>\", email = self.email)\n    }\n}\n\nimpl UserModel {\n    /// Hash `password` using argon2 and return it.\n    pub fn make_password_hash(password: &str) -> Vec<u8> {\n        argon2i_simple(password, \"loginsalt\").to_vec()\n    }\n\n    /// Verify that `candidate_password` matches the stored password.\n    pub fn verify_password(&self, candidate_password: &str) -> bool {\n        let candidate_hash = argon2i_simple(candidate_password, \"loginsalt\").to_vec();\n        self.password_hash == candidate_hash\n    }\n\n    /// Generate an auth token and save it to the `current_auth_token` column.\n    pub fn generate_auth_token(&mut self, conn: &PgConnection) -> Result<String, DieselError> {\n        let rng = thread_rng();\n        let new_auth_token = rng\n            .sample_iter(&Alphanumeric)\n            .take(32)\n            .collect::<String>();\n        self.current_auth_token = Some(new_auth_token.clone());\n        self.last_action = Some(Utc::now().naive_utc());\n        self.save_changes::<UserModel>(conn)?;\n        Ok(new_auth_token)\n    }\n\n    /// Return whether or not the user has a valid auth token.\n    pub fn has_valid_auth_token(&self, auth_token_timeout: Duration) -> bool {\n        let latest_valid_date = Utc::now() - auth_token_timeout;\n        if let Some(last_action) = self.last_action {\n            if self.current_auth_token.is_some() {\n                last_action > latest_valid_date.naive_utc()\n            } else {\n                false\n            }\n        } else {\n            false\n        }\n    }\n\n    /// Get a `User` from a login token.\n    ///\n    /// A login token has this format:\n    ///     <user uuid>:<auth token>\n    pub fn get_user_from_login_token(token: &str, db: &PgConnection) -> Option<UserModel> {\n        use crate::schema::users::dsl::*;\n\n        let v: Vec<&str> = token.split(':').collect();\n        let user_id = Uuid::parse_str(v.get(0).unwrap_or(&\"\")).unwrap_or_default();\n        let auth_token = v.get(1).unwrap_or(&\"\").to_string();\n\n        let user = users.find(user_id).first::<UserModel>(&*db).optional();\n        if let Ok(Some(u)) = user {\n            if let Some(token) = u.current_auth_token.clone() {\n                if verify_slices_are_equal(token.as_bytes(), auth_token.as_bytes()).is_ok() {\n                    return Some(u);\n                }\n            }\n        }\n        None\n    }\n}\n"
  },
  {
    "path": "src/responses.rs",
    "content": "use diesel::result::Error as DieselError;\nuse rocket::http::{ContentType, Status};\nuse rocket::request::Request;\nuse rocket::response::{Responder, Response};\nuse rocket_contrib::json;\nuse rocket_contrib::json::JsonValue;\nuse std::convert::From;\nuse std::io::Cursor;\n\n#[derive(Debug)]\npub struct APIResponse {\n    data: JsonValue,\n    status: Status,\n}\n\nimpl APIResponse {\n    /// Set the data of the `Response` to `data`.\n    pub fn data(mut self, data: JsonValue) -> APIResponse {\n        self.data = data;\n        self\n    }\n\n    /// Convenience method to set `self.data` to `{\"message\": message}`.\n    pub fn message(mut self, message: &str) -> APIResponse {\n        self.data = json!({ \"message\": message });\n        self\n    }\n}\n\nimpl From<DieselError> for APIResponse {\n    fn from(_: DieselError) -> Self {\n        internal_server_error()\n    }\n}\n\nimpl<'r> Responder<'r> for APIResponse {\n    fn respond_to(self, _req: &Request) -> Result<Response<'r>, Status> {\n        let body = self.data;\n\n        Response::build()\n            .status(self.status)\n            .sized_body(Cursor::new(body.to_string()))\n            .header(ContentType::JSON)\n            .ok()\n    }\n}\n\npub fn ok() -> APIResponse {\n    APIResponse {\n        data: json!(null),\n        status: Status::Ok,\n    }\n}\n\npub fn created() -> APIResponse {\n    APIResponse {\n        data: json!(null),\n        status: Status::Created,\n    }\n}\n\npub fn accepted() -> APIResponse {\n    APIResponse {\n        data: json!(null),\n        status: Status::Accepted,\n    }\n}\n\npub fn no_content() -> APIResponse {\n    APIResponse {\n        data: json!(null),\n        status: Status::NoContent,\n    }\n}\n\npub fn bad_request() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Bad Request\"}),\n        status: Status::BadRequest,\n    }\n}\n\npub fn unauthorized() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Unauthorized\"}),\n        status: Status::Unauthorized,\n    }\n}\n\npub fn forbidden() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Forbidden\"}),\n        status: Status::Forbidden,\n    }\n}\n\npub fn not_found() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Not Found\"}),\n        status: Status::NotFound,\n    }\n}\n\npub fn method_not_allowed() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Method Not Allowed\"}),\n        status: Status::MethodNotAllowed,\n    }\n}\n\npub fn conflict() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Conflict\"}),\n        status: Status::Conflict,\n    }\n}\n\npub fn unprocessable_entity(errors: JsonValue) -> APIResponse {\n    APIResponse {\n        data: json!({ \"message\": errors }),\n        status: Status::UnprocessableEntity,\n    }\n}\n\npub fn internal_server_error() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Internal Server Error\"}),\n        status: Status::InternalServerError,\n    }\n}\n\npub fn service_unavailable() -> APIResponse {\n    APIResponse {\n        data: json!({\"message\": \"Service Unavailable\"}),\n        status: Status::ServiceUnavailable,\n    }\n}\n"
  },
  {
    "path": "src/schema.rs",
    "content": "// TODO: Silence this until diesel 1.4.\n// See https://github.com/diesel-rs/diesel/issues/1785#issuecomment-422579609.\n#![allow(proc_macro_derive_resolution_fallback)]\n\ntable! {\n    users (id) {\n        id -> Uuid,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n        email -> Varchar,\n        password_hash -> Bytea,\n        current_auth_token -> Nullable<Varchar>,\n        last_action -> Nullable<Timestamp>,\n    }\n}\n"
  },
  {
    "path": "src/validation/mod.rs",
    "content": "pub mod user;\n"
  },
  {
    "path": "src/validation/user.rs",
    "content": "use rocket::data::{self, FromData, FromDataSimple, Transform};\nuse rocket::http::Status;\nuse rocket::Outcome::*;\nuse rocket::{Data, Request};\nuse rocket_contrib::json;\nuse rocket_contrib::json::{Json, JsonValue};\nuse serde_derive::Deserialize;\nuse std::collections::HashMap;\nuse std::io::Read;\nuse uuid::Uuid;\nuse validator::Validate;\nuse validator_derive::Validate;\n\n#[derive(Deserialize, Debug, Validate)]\npub struct UserLogin {\n    #[serde(skip_deserializing)]\n    pub id: Option<Uuid>,\n    #[validate(email)]\n    pub email: String,\n    pub password: String,\n}\n\nimpl FromDataSimple for UserLogin {\n    type Error = JsonValue;\n\n    fn from_data(req: &Request, data: Data) -> data::Outcome<Self, JsonValue> {\n        let mut d = String::new();\n        if data.open().read_to_string(&mut d).is_err() {\n            return Failure((\n                Status::InternalServerError,\n                json!({\"_schema\": \"Internal server error.\"}),\n            ));\n        }\n        let user =\n            Json::<UserLogin>::from_data(req, Transform::Borrowed(Success(&d))).map_failure(|_| {\n                (\n                    Status::UnprocessableEntity,\n                    json!({\"_schema\": \"Error while parsing user login.\"}),\n                )\n            })?;\n\n        let mut errors = HashMap::new();\n        if user.email == \"\" {\n            errors\n                .entry(\"email\")\n                .or_insert_with(|| vec![])\n                .push(\"Must not be empty.\");\n        } else if !user.email.contains('@') || !user.email.contains('.') {\n            errors\n                .entry(\"email\")\n                .or_insert_with(|| vec![])\n                .push(\"Invalid email.\");\n        }\n\n        if user.password == \"\" {\n            errors\n                .entry(\"password\")\n                .or_insert_with(|| vec![])\n                .push(\"Must not be empty.\");\n        }\n\n        if !errors.is_empty() {\n            return Failure((Status::UnprocessableEntity, json!(errors)));\n        }\n\n        Success(UserLogin {\n            id: user.id,\n            email: user.email.clone(),\n            password: user.password.clone(),\n        })\n    }\n}\n"
  },
  {
    "path": "tests/common/mod.rs",
    "content": "pub fn setup() {\n    dotenv::dotenv().ok();\n}\n"
  },
  {
    "path": "tests/factories/mod.rs",
    "content": "use uuid::Uuid;\nuse diesel;\nuse diesel::prelude::*;\nuse diesel::pg::PgConnection;\n\nuse rust_web_boilerplate::models::user::{UserModel, NewUser};\nuse rust_web_boilerplate::schema::users::dsl::*;\n\n/// Create a new `User` and add it to the database.\n///\n/// The user's email will be set to '<uuid>@example.com'.\npub fn make_user(conn: &PgConnection) -> UserModel {\n    let new_email = format!(\"{username}@example.com\", username=Uuid::new_v4().to_hyphenated().to_string());\n    let new_password_hash = UserModel::make_password_hash(\"testtest\");\n    let new_user = NewUser {\n        email: new_email,\n        password_hash: new_password_hash,\n    };\n\n    diesel::insert_into(users)\n        .values(&new_user)\n        .get_result::<UserModel>(conn)\n        .expect(\"Error saving new post\")\n}\n"
  },
  {
    "path": "tests/test_api_auth.rs",
    "content": "#[allow(unused_imports)]\nuse diesel::prelude::*;\nuse parking_lot::Mutex;\nuse rocket::http::{ContentType, Status};\nuse rocket::local::Client;\nuse rocket_contrib::json;\nuse rocket_contrib::json::JsonValue;\nuse serde_derive::Deserialize;\nuse speculate::speculate;\nuse uuid::Uuid;\n\nuse rust_web_boilerplate::database::DbConn;\nuse rust_web_boilerplate::models::user::UserModel;\nuse rust_web_boilerplate::rocket_factory;\nuse rust_web_boilerplate::schema::users::dsl::*;\n\nuse crate::factories::make_user;\n\nmod common;\nmod factories;\n\nstatic DB_LOCK: Mutex<()> = Mutex::new(());\n\n#[derive(Deserialize)]\nstruct LoginData {\n    user_id: Uuid,\n    token: String,\n}\n\nspeculate! {\n    before {\n        common::setup();\n        let _lock = DB_LOCK.lock();\n        let rocket = rocket_factory(\"testing\").unwrap();\n        let client = Client::new(rocket).unwrap();\n        #[allow(unused_variables)]\n        let conn = DbConn::get_one(client.rocket()).expect(\"Failed to get a database connection for testing!\");\n    }\n\n    describe \"login\" {\n        it \"enables users to login and get back a valid auth token\" {\n            let user = make_user(&conn);\n            let data = json!({\n                \"email\": user.email,\n                \"password\": \"testtest\",\n            });\n            let mut res = client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: LoginData = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            let refreshed_user = users\n                .find(user.id)\n                .first::<UserModel>(&*conn).unwrap();\n            assert_eq!(res.status(), Status::Ok);\n            assert_eq!(body.user_id, refreshed_user.id);\n            assert_eq!(body.token, refreshed_user.current_auth_token.unwrap());\n        }\n\n        it \"can log in and get back the same auth token if there's already a valid one\" {\n            let user = make_user(&conn);\n            let data = json!({\n                \"email\": user.email,\n                \"password\": \"testtest\",\n            });\n\n            // Login the first time and then retrieve and store the token.\n            let first_login_token = {\n                client.post(\"/auth/login\")\n                    .header(ContentType::JSON)\n                    .body(data.to_string())\n                    .dispatch();\n                let user_after_first_login = users\n                    .find(user.id)\n                    .first::<UserModel>(&*conn).unwrap();\n                user_after_first_login.current_auth_token.unwrap()\n            };\n\n            // Login the second time and then retrieve and store the token.\n            let second_login_token = {\n                client.post(\"/auth/login\")\n                    .header(ContentType::JSON)\n                    .body(data.to_string())\n                    .dispatch();\n                let user_after_second_login = users\n                    .find(user.id)\n                    .first::<UserModel>(&*conn).unwrap();\n                user_after_second_login.current_auth_token.unwrap()\n            };\n\n            assert_eq!(first_login_token, second_login_token);\n        }\n\n        it \"fails with a wrong username\" {\n            make_user(&conn);\n            let data = json!({\n                    \"email\": \"invalid@example.com\",\n                    \"password\": \"testtest\",\n            });\n            let mut res = client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::Unauthorized);\n            assert_eq!(body[\"message\"], \"Username or password incorrect.\");\n        }\n\n        it \"fails with a wrong password\" {\n            let user = make_user(&conn);\n            let data = json!({\n                    \"email\": user.email,\n                    \"password\": \"invalid\",\n            });\n            let mut res = client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::Unauthorized);\n            assert_eq!(body[\"message\"], \"Username or password incorrect.\");\n        }\n    }\n\n    describe \"register\" {\n        it \"allows users to register a new account and then login with it\" {\n            let new_email = format!(\"{username}@example.com\", username=Uuid::new_v4().to_hyphenated().to_string());\n            let new_password = \"mypassword\";\n            let data = json!({\n                \"email\": new_email,\n                \"password\": new_password,\n            });\n            let mut res = client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: UserModel = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::Created);\n            assert_eq!(body.email, new_email);\n\n            // Now try to log in using the new account.\n            let data = json!({\n                \"email\": new_email,\n                \"password\": new_password,\n            });\n            let mut res = client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: LoginData = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            let logged_in_user = users\n                .filter(email.eq(new_email))\n                .first::<UserModel>(&*conn).unwrap();\n            assert_eq!(res.status(), Status::Ok);\n            assert_eq!(body.token, logged_in_user.current_auth_token.unwrap());\n        }\n\n        it \"can't register with an existing email\" {\n            let new_email = format!(\"{username}@example.com\", username=Uuid::new_v4().to_hyphenated().to_string());\n            let new_password = \"mypassword\";\n            let data = json!({\n                \"email\": new_email,\n                \"password\": new_password,\n            });\n            client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n\n            let mut res = client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::Conflict);\n            assert_eq!(body[\"message\"], \"User already exists.\");\n        }\n\n        it \"can't register with an invalid email\" {\n            let data = json!({\n                \"email\": \"invalid\",\n                \"password\": \"somepw\",\n            });\n            let mut res = client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::UnprocessableEntity);\n            assert_eq!(body[\"message\"][\"email\"], *json!([\"Invalid email.\"]));\n        }\n\n        it \"can't register with an empty email\" {\n            let data = json!({\n                \"email\": \"\",\n                \"password\": \"somepw\",\n            });\n            let mut res = client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::UnprocessableEntity);\n            assert_eq!(body[\"message\"][\"email\"], *json!([\"Must not be empty.\"]));\n        }\n\n        it \"can't register with an empty password\" {\n            let data = json!({\n                \"email\": \"something@example.com\",\n                \"password\": \"\",\n            });\n            let mut res = client.post(\"/auth/register\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: JsonValue = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n\n            assert_eq!(res.status(), Status::UnprocessableEntity);\n            assert_eq!(body[\"message\"][\"password\"], *json!([\"Must not be empty.\"]));\n        }\n    }\n}\n"
  },
  {
    "path": "tests/test_api_hello.rs",
    "content": "#[allow(unused_imports)]\nuse diesel::prelude::*;\nuse parking_lot::Mutex;\nuse rocket::http::{ContentType, Header, Status};\nuse rocket::local::Client;\nuse rocket_contrib::json;\nuse serde_derive::Deserialize;\nuse speculate::speculate;\nuse uuid::Uuid;\n\nuse rust_web_boilerplate::database::DbConn;\nuse rust_web_boilerplate::rocket_factory;\n\nuse crate::factories::make_user;\n\nmod common;\nmod factories;\n\nstatic DB_LOCK: Mutex<()> = Mutex::new(());\n\n#[derive(Deserialize)]\nstruct LoginData {\n    token: String,\n}\n\nspeculate! {\n    before {\n        common::setup();\n        let _lock = DB_LOCK.lock();\n        let rocket = rocket_factory(\"testing\").unwrap();\n        let client = Client::new(rocket).unwrap();\n        #[allow(unused_variables)]\n        let conn = DbConn::get_one(client.rocket()).expect(\"Failed to get a database connection for testing!\");\n    }\n\n    describe \"whoami\" {\n        it \"echoes back the email\" {\n            let user = make_user(&conn);\n            let data = json!({\n                \"email\": user.email,\n                \"password\": \"testtest\",\n            });\n            let mut res = client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n            let body: LoginData = serde_json::from_str(&res.body_string().unwrap()).unwrap();\n            let token = body.token;\n\n            let res = client.get(\"/hello/whoami\")\n                .header(ContentType::JSON)\n                .header(Header::new(\"Authorization\", format!(\"Bearer {}:{}\", user.id, token)))\n                .dispatch();\n\n            assert_eq!(res.status(), Status::Ok);\n        }\n\n        it \"returns BadRequest when sent no Authorization header\" {\n            let user = make_user(&conn);\n            let data = json!({\n                \"email\": user.email,\n                \"password\": \"testtest\",\n            });\n            client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n\n            let res = client.get(\"/hello/whoami\")\n                .header(ContentType::JSON)\n                .dispatch();\n\n            assert_eq!(res.status(), Status::BadRequest);\n        }\n\n        it \"returns Unauthorized when sent an invalid token\" {\n            let user = make_user(&conn);\n            let data = json!({\n                \"email\": user.email,\n                \"password\": \"testtest\",\n            });\n            client.post(\"/auth/login\")\n                .header(ContentType::JSON)\n                .body(data.to_string())\n                .dispatch();\n\n            let res = client.get(\"/hello/whoami\")\n                .header(ContentType::JSON)\n                .header(Header::new(\"Authorization\", format!(\"Bearer {}:{}\", user.id, Uuid::nil())))\n                .dispatch();\n\n            assert_eq!(res.status(), Status::Unauthorized);\n        }\n    }\n}\n"
  },
  {
    "path": "watch.sh",
    "content": "#!/bin/bash\ncargo watch -s \"./reset.sh && cargo clippy && cargo build && RUST_BACKTRACE=1 cargo test && RUST_BACKTRACE=1 cargo run\"\n"
  }
]