Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust backend for fedibook #17

Merged
merged 7 commits into from
Nov 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
946 changes: 944 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

48 changes: 46 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
[package]
name = "fedibook"
version = "0.0.0"
authors = ["Eric Chadbourne <sillystring@protonmail.com>", "Peter Alexander <drbanjofox@protonmail.com>", "Elijah Mark Anderson <kd0bpv@gmail.com>"]
version = "0.1.0"
authors = ["Eric Chadbourne <sillystring@protonmail.com>", "Peter Alexander <drbanjofox@protonmail.com>", "Elijah Mark Anderson <kd0bpv@gmail.com>", "Paul Woolcock <paul@woolcock.us>"]

[lib]
name = "_fedibook"
path = "src/fedibook/lib.rs"

[[bin]]
name = "fedibook-server"
path = "src/bin/main.rs"

[dependencies]
base64 = "0.7"
bcrypt = "0.1"
collection_macros = "0.2.0"
derive_builder = "0.5.0"
failure = "0.1.0"
failure_derive = "0.1.1"
libxml = "0.0.7481"
r2d2 = "0.7"
r2d2-diesel = "0.16"
ring = "0.11"
rocket = "0.3.3"
rocket_codegen = "0.3.3"
serde = "1.0.21"
serde_derive = "1.0.21"

[dependencies.chrono]
version = "0.4"
features = ["serde"]

[dependencies.uuid]
version = "0.5"
features = ["v4"]

[dependencies.diesel]
version = "0.16"
default-features = false
features = ["postgres", "uuid", "chrono"]

[dependencies.diesel_codegen]
version = "0.16"
default-features = false
features = ["postgres"]

[dependencies.rocket_contrib]
version = "0.3.3"
default-features = false
features = ["handlebars_templates", "json"]
8 changes: 8 additions & 0 deletions Rocket.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[development]
port = 7878 # https://twitter.com/ag_dubs/status/852559264510070784
database_url = "postgres://localhost/fedibook_development"

[staging]

[production]

Empty file added migrations/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions migrations/00000000000000_diesel_initial_setup/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();
36 changes: 36 additions & 0 deletions migrations/00000000000000_diesel_initial_setup/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.




-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
2 changes: 2 additions & 0 deletions migrations/2017-11-28-112459_create_schema/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP EXTENSION IF EXISTS "pgcrypto";
DROP SCHEMA IF EXISTS fedibook;
3 changes: 3 additions & 0 deletions migrations/2017-11-28-112459_create_schema/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE SCHEMA IF NOT EXISTS fedibook;

CREATE EXTENSION IF NOT EXISTS "pgcrypto";
3 changes: 3 additions & 0 deletions migrations/2017-11-28-112508_create_accounts/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS id_idx;
DROP INDEX IF EXISTS username_domain_unique_idx;
DROP TABLE IF EXISTS fedibook.accounts;
12 changes: 12 additions & 0 deletions migrations/2017-11-28-112508_create_accounts/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS fedibook.accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR NOT NULL DEFAULT '',
domain VARCHAR,
display_name VARCHAR NOT NULL DEFAULT '',

created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc'),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
);

CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON fedibook.accounts (id);
CREATE UNIQUE INDEX IF NOT EXISTS username_domain_unique_idx ON fedibook.accounts (username, domain);
2 changes: 2 additions & 0 deletions migrations/2017-11-28-112517_create_users/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS email_idx;
DROP TABLE IF EXISTS fedibook.users;
24 changes: 24 additions & 0 deletions migrations/2017-11-28-112517_create_users/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS fedibook.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR NOT NULL DEFAULT '',
encrypted_password VARCHAR NOT NULL,
account_id UUID NOT NULL,

-- flags
admin BOOLEAN NOT NULL DEFAULT false,
disabled BOOLEAN NOT NULL DEFAULT false,

-- confirmation stuff
unconfirmed_email VARCHAR NOT NULL DEFAULT '',
confirmation_token BYTEA NOT NULL,
confirmed_at TIMESTAMP WITHOUT TIME ZONE,
confirmation_sent_at TIMESTAMP WITHOUT TIME ZONE,

-- timestamps
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc'),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc'),

FOREIGN KEY (account_id) REFERENCES fedibook.accounts (id)
);

CREATE INDEX IF NOT EXISTS email_idx ON fedibook.users (email);
104 changes: 104 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Rust backend

Since the initial PR for this is rather large, I'm including a write-up
here to hopefully aid in the code review process.

The rust app here uses primarily [Rocket.rs](https://rocket.rs) and
[Diesel](https://diesel.rs), along with a number of smaller crates to
fill in any gaps. There are two primary entry points into the code,
`src/bin/main.rs` and `src/fedibook/lib.rs`. This is a pretty standard
setup for rust projects, in which the bulk of the code is in a
library-style crate (`src/fedibook/`) and the main runnable binary
is a smaller crate that sets up a few things and then calls into the
library crate. Keeping this separation is useful for adding other
binaries down the line, and it doesn't really cost us anything now.

## src/bin/main.rs

This is the file that, when compiled, implements the actual runnable
binary. It will be where we set up all the necessary data structures
that the webapp needs to run, gathers config from the user (right now
it's just using the standard `Rocket.toml` file that rocket.rs expects),
and sets up the various routes that are actually defined in the library
crate.

Currently we are mounting routes under 2 prefixes: `"/"` and
`"/api/v1"`, though there is nothing actually implemented under
`"/api/v1"`, other than a placeholder example route. All the URLs
mounted under `"/"` are routes that you would expect a user to request
in their browser. The routes that are currently written implement a
barebones user login/registration system, though there are still many
things to do to improve that system.

Routes under the `"/api/v1"` prefix are routes that should not be
directly browsed to, but will use OAuth for authentication, accept and
return JSON, and would primarily be used via XMLHttpRequests from the
browser webapp (or via cURL during development).

## src/fedibook/

This is where the real meat of the application is. The main entry point
for the library crate is `src/fedibook/lib.rs`, that has all the `mod`
statements and such. There are a few directories from here that should
be pretty self-explanatory. `controllers`, `forms`, `models`, and
`routes`. The `routes` module contain the definitions for the actual
endpoints, though I've tried to keep those small and move the bulk of
the logic into the controllers.

There is a special file, `src/fedibook/schema.rs`, that holds the
generated database schema. This is generated by:

1. Installing `diesel_cli`: `$ cargo install diesel_cli --no-default-features --features "postgres"`
2. Running `diesel print-schema --with-docs -s fedibook > src/fedibook/schema.rs`

The version of `diesel` that was used to develop this has a small bug in
which the `joinable!()` invocation at the bottom of the schema.rs file
had to be slightly changed, but the next version of `diesel` does not
have that bug and I am working on upgrading `fedibook` to that version.

## Migrations

In the `migrations/` directory, you will find all the database
migrations that `diesel` will need to properly set up your database.

## Templates

In the `templates/` directory, you will find the templates that `rocket`
uses to serve the login, logout, register, and home pages (barely more
than placeholders right now)

## In the browser

To go with the user registration/login system, I have a handful of
(very) barebones templates that can be used to register, login, and
logout in a browser. Assuming that rust, postgres, and diesel are
already installed, the workflow goes like this:

* start postgres. make sure you can successfully connect to it
* cd into the fedibook directory and set up the database (this is only
necessary the first time):
* make sure diesel can connect to your database:
`export DATABASE_URL="postgres://user:pass@host/dbname"`
* `diesel setup` should create the database
* `diesel migration run` should run all the migrations
* start the app: `cargo run` (or `cargo run --bin fedibook-server` if
you want to be explicit)
* When you see "Rocket has launched from http://...." you should be
able to connect to the running app
* Assuming you run the app on `localhost` and use the `7878` port, you
can browse to `http://localhost:7878/auth/sign_up` and register a
user account
* If the registration succeeds, it will redirect you to
`http://localhost:7878/auth/sign_in`. You won't actually be able to
sign in yet, you have to confirm your account.
* Go back to the command line, you should see a message that has a
path at the end with the token to confirm your account.
* Go to `http://localhost:7878<the path from the console>` in your
browser
* If the confirmation succeeded, it should redirect you back to the
`/auth/sign_in` page, where you should be able to log in now.
* If the log in succeeds, it should redirect you to `/web` and show
you a message, and a "logout" button

Going to `/web` without logging in should redirect you to the login page

59 changes: 59 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#![feature(plugin)]
#![feature(custom_derive)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate failure;
extern crate rocket_contrib;
extern crate serde;
extern crate r2d2;
extern crate r2d2_diesel;
extern crate ring;
extern crate diesel;

extern crate _fedibook as fedibook;

use ring::rand::SystemRandom;
use rocket::Rocket;
use rocket_contrib::Template;
use diesel::pg::PgConnection;
use r2d2_diesel::ConnectionManager;

type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;

fn db_pool(rocket: &Rocket) -> Pool {
let database_url = rocket.config().get_str("database_url").expect("Must set DATABASE_URL");
let config = r2d2::Config::default();
let manager = ConnectionManager::<PgConnection>::new(database_url);
r2d2::Pool::new(config, manager).expect("Could not get DB connection pool")
}

fn app() -> Rocket {
let r = rocket::ignite()
.mount("/api/v1", routes![
fedibook::routes::applications::register_application
])
.mount("/", routes![
fedibook::routes::auth::sign_up_form,
fedibook::routes::auth::sign_in_form,
fedibook::routes::auth::sign_up,
fedibook::routes::auth::sign_in,
fedibook::routes::auth::confirm,
fedibook::routes::auth::sign_out,

fedibook::routes::app::home,
fedibook::routes::app::home_redirect,
])
.attach(Template::fairing())
.manage(SystemRandom::new());

// we need an instance of the app to access the config values in Rocket.toml,
// so we pass it to the db_pool function, get the pool, and _then_ return the instance
let pool = db_pool(&r);
r.manage(pool)
}

fn main() {
app().launch();
}

File renamed without changes.
15 changes: 15 additions & 0 deletions src/fedibook/controllers/apps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use models::apps::{App, AppId, AppIdBuilder};

#[derive(Fail, Debug)]
#[fail(display = "Failed to create app.")]
pub(crate) struct CreateAppError;

pub(crate) fn create(app: App) -> Result<AppId, CreateAppError> {
// store the app somewhere
Ok(AppIdBuilder::default()
.id("foo")
.client_id("bar")
.client_secret("baz")
.build()
.unwrap())
}
Loading