Initial commit after extraction
This commit is contained in:
commit
80ba54e4ef
43 changed files with 3865 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
2716
Cargo.lock
generated
Normal file
2716
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
33
Cargo.toml
Normal file
33
Cargo.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
[package]
|
||||||
|
name = "jelly"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = "0.5"
|
||||||
|
actix-web = { version = "0.6.15", features = ["alpn", "session"] }
|
||||||
|
actix-redis = { git = "https://github.com/ryanmcgrath/actix-redis.git", rev = "6c8801dc8caebe901257e8ea41e465e62db94c3f" }
|
||||||
|
sentry = "0.6.1"
|
||||||
|
sentry-actix = "0.1.0"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
chrono = "0.4.4"
|
||||||
|
diesel = { version = "<= 1.3.2", features = ["postgres", "chrono", "serde_json", "r2d2"] }
|
||||||
|
dotenv = "0.13.0"
|
||||||
|
askama = "0.7.0"
|
||||||
|
num_cpus = "1.0"
|
||||||
|
futures = "0.1.22"
|
||||||
|
log = "0.4.0"
|
||||||
|
env_logger = "0.5.10"
|
||||||
|
djangohashers = { version = "^0.3", features = ["fpbkdf2"] }
|
||||||
|
validator = { version = "0.7.0", features = [] }
|
||||||
|
validator_derive = "0.7.1"
|
||||||
|
uuid = { version = "0.6", features = ["v4"] }
|
||||||
|
zxcvbn = "1.0.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
askama = "0.7"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
12
LICENSE
Normal file
12
LICENSE
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
DO WHAT YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 1, July 2018
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed. Sub-projects... well, respect their licenses and
|
||||||
|
attribute accordingly.
|
||||||
|
|
||||||
|
DO WHAT YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT YOU WANT TO.
|
||||||
12
build.rs
Normal file
12
build.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// build.rs
|
||||||
|
//
|
||||||
|
// A build script. What, you wanna fight about it? Here's your paycheck.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
extern crate askama;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
askama::rerun_if_templates_changed();
|
||||||
|
}
|
||||||
5
diesel.toml
Normal file
5
diesel.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "src/schema.rs"
|
||||||
6
example.env
Normal file
6
example.env
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# SENTRY_DSN=""
|
||||||
|
# BIND_TO="127.0.0.1:17000"
|
||||||
|
# SECRET_KEY="generate w/ openssl rand -base64 32"
|
||||||
|
# DATABASE_URL=""
|
||||||
|
# REDIS="localhost:6379"
|
||||||
|
# POSTMARK=""
|
||||||
0
migrations/.gitkeep
Normal file
0
migrations/.gitkeep
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
6
migrations/00000000000000_diesel_initial_setup/down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||||
|
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||||
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
|
|
@ -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;
|
||||||
1
migrations/2018-07-17-173341_create_users/down.sql
Normal file
1
migrations/2018-07-17-173341_create_users/down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
drop table users;
|
||||||
21
migrations/2018-07-17-173341_create_users/up.sql
Normal file
21
migrations/2018-07-17-173341_create_users/up.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
create table if not exists users (
|
||||||
|
id serial primary key,
|
||||||
|
name text,
|
||||||
|
email text not null unique,
|
||||||
|
password text not null,
|
||||||
|
avatar text,
|
||||||
|
is_verified bool not null default false,
|
||||||
|
has_verified_email bool not null default false,
|
||||||
|
created timestamp with time zone not null default now(),
|
||||||
|
updated timestamp with time zone not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER user_updated BEFORE INSERT OR UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
|
||||||
64
readme.md
Normal file
64
readme.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Jelly
|
||||||
|
This is a sample repository showcasing a rather straightforward way to handle user sessions, signup, and authentication in an [actix-web](https://actix.rs) project. I extracted it from something I'm working on as I realized that it can be otherwise tricky to figure out at a glance how all of this fits together (actix-web is still fairly fast moving, and the docs can be... rough).
|
||||||
|
|
||||||
|
You might be interested in this project if:
|
||||||
|
|
||||||
|
- You want a sample Rust/actix-web project to kick off that has (mostly) sane defaults, and built-in user accounts.
|
||||||
|
- You're unsure about how to structure an actix-web project, and want an opinionated (not even necessarily correct) starter.
|
||||||
|
- You're not interested in putting a puzzle together for something as basic as user authentication, and just want it to work.
|
||||||
|
|
||||||
|
You might also not be interested in this, and that's cool too. It's licensed as a "do whatever you want" type deal, so... clone away and have fun. Some extra notes are below.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
- Clone the repo
|
||||||
|
- `mv example.env .env`, and fill in the values in there
|
||||||
|
- `diesel migration run` to create the user database table
|
||||||
|
- `cargo run` to... well, run it. Depending on whether you have `diesel_cli` installed you might need that too.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
This is probably still a bit rough around the edges, since I ripped it out of an existing project of mine, but the key things I wanted to solve were:
|
||||||
|
|
||||||
|
- User signup/login, with mostly secure cookie defaults
|
||||||
|
- An easy way to check the current active user/session on each request
|
||||||
|
- Figuring out how the hell to shove Redis in here - sessions are stored in there instead of the built-in `CookieSessionBackend` you'll find that ships with actix-web.
|
||||||
|
|
||||||
|
There's some "middleware" here (`src/users/middleware.rs`) that makes it easy to check the authentication status for the request, and load the associated `User` record. The first one, `request.is_authentication()`, simply checks the session to see if we have anything indicating a `User` is set. The second one, `request.user()`, returns a future that'll provide the actual `User` object.
|
||||||
|
|
||||||
|
`FutureResponse` and `future_redirect` are some wrappers around `actix-web` response formats to make the ergonomics of all of this more readable. You can take 'em or leave 'em.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
use users::middleware::UserAuthentication;
|
||||||
|
use utils::responses::{FutureHttpResponse, future_redirect};
|
||||||
|
|
||||||
|
fn view(request: HttpRequest) -> FutureResponse {
|
||||||
|
// Check the session is valid, without a database hit to load the user
|
||||||
|
if let Err(e) = request.is_authenticated() {
|
||||||
|
return future_redirect("http://www.mozilla.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call over to Postgres and get that there user
|
||||||
|
request.user().then(|a| match a {
|
||||||
|
Ok(user) => {
|
||||||
|
future_redirect("http://www.duckduckgo.com/")
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(_) => {
|
||||||
|
future_redirect("http://www.google.com/")
|
||||||
|
}
|
||||||
|
}).responder()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If I was the kind to use Rust nightly in a project, I'd be interested in a derive-esque macro to check auth, ala Django's `@login_required` decorator.
|
||||||
|
|
||||||
|
Also, as you read the code, you may notice a lot of this is influenced by Django. I think they got the user model stuff right at some point over the years. Thanks to the `djangohashers` package, this even matches the password hashing Django does.
|
||||||
|
|
||||||
|
Oh, and randomly, this includes a simple library for sending emails via [Postmark](https://postmarkapp.com/), since I enjoy their service.
|
||||||
|
|
||||||
|
## Questions, Comments, Etc?
|
||||||
|
- Email: [ryan@rymc.io](mailto:ryan@rymc.io)
|
||||||
|
- Twitter: [@ryanmcgrath](https://twitter.com/ryanmcgrath/)
|
||||||
|
- Web: [rymc.io](https://rymc.io/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
Do what you want. Read the license, it'll say the same.
|
||||||
9
src/emails/mod.rs
Normal file
9
src/emails/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// emails/mod.rs
|
||||||
|
//
|
||||||
|
// A module for dealing with emails - e.g, templates, etc.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
pub mod postmark;
|
||||||
|
pub use self::postmark::{Postmark, Email};
|
||||||
79
src/emails/postmark/mod.rs
Normal file
79
src/emails/postmark/mod.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// postmark.rs
|
||||||
|
//
|
||||||
|
// A basic API client library for utilizing Postmark (https://postmarkapp.com/),
|
||||||
|
// a great service for transactional emails.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use actix_web::client;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Email<'a> {
|
||||||
|
#[serde(rename = "Subject")]
|
||||||
|
pub subject: &'a str,
|
||||||
|
|
||||||
|
#[serde(rename = "TextBody")]
|
||||||
|
pub body: &'a str,
|
||||||
|
|
||||||
|
#[serde(rename = "HtmlBody")]
|
||||||
|
pub html_body: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "From")]
|
||||||
|
pub from: &'a str,
|
||||||
|
|
||||||
|
#[serde(rename = "To")]
|
||||||
|
pub to: &'a str,
|
||||||
|
|
||||||
|
#[serde(rename = "ReplyTo")]
|
||||||
|
pub reply_to: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "Cc")]
|
||||||
|
pub cc: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "Bcc")]
|
||||||
|
pub bcc: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "Tag")]
|
||||||
|
pub tag: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "Metadata")]
|
||||||
|
pub metadata: Option<HashMap<&'a str, &'a str>>,
|
||||||
|
|
||||||
|
#[serde(rename = "Headers")]
|
||||||
|
pub headers: Option<Vec<HashMap<&'a str, &'a str>>>,
|
||||||
|
|
||||||
|
#[serde(rename = "TrackOpens")]
|
||||||
|
pub track_opens: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename = "TrackLinks")]
|
||||||
|
pub track_links: Option<&'a str>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Postmark {
|
||||||
|
api_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Postmark {
|
||||||
|
pub fn new(api_token: &str) -> Self {
|
||||||
|
Postmark { api_token: api_token.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, subject: &str, body: &str, from: &str, to: &str) -> client::SendRequest {
|
||||||
|
let email = Email {
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
subject: subject,
|
||||||
|
body: body,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
client::post("https://api.postmarkapp.com/email")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-Postmark-Server-Token", self.api_token.as_str())
|
||||||
|
.json(email).unwrap().send()
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/main.rs
Normal file
88
src/main.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// main.rs
|
||||||
|
//
|
||||||
|
// Main linker for all the external crates and such. Server logic
|
||||||
|
// is handled over in app.rs for brevity's sake.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/02/2018
|
||||||
|
|
||||||
|
extern crate uuid;
|
||||||
|
extern crate futures;
|
||||||
|
extern crate dotenv;
|
||||||
|
extern crate sentry;
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate actix;
|
||||||
|
extern crate num_cpus;
|
||||||
|
extern crate actix_web;
|
||||||
|
extern crate validator;
|
||||||
|
extern crate env_logger;
|
||||||
|
extern crate actix_redis;
|
||||||
|
extern crate sentry_actix;
|
||||||
|
extern crate djangohashers;
|
||||||
|
#[macro_use] extern crate log;
|
||||||
|
#[macro_use] extern crate askama;
|
||||||
|
extern crate serde_json;
|
||||||
|
#[macro_use] extern crate serde_derive;
|
||||||
|
#[macro_use] extern crate validator_derive;
|
||||||
|
#[macro_use] extern crate diesel;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use actix::prelude::*;
|
||||||
|
use actix_web::{server, App, middleware};
|
||||||
|
use sentry_actix::SentryMiddleware;
|
||||||
|
|
||||||
|
pub mod schema;
|
||||||
|
pub mod users;
|
||||||
|
pub mod util;
|
||||||
|
pub mod pages;
|
||||||
|
pub mod emails;
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub db: Addr<Syn, util::database::Database>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dotenv().ok();
|
||||||
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
|
env::set_var("RUST_LOG", "actix_web=debug,info,warn");
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let _sentry;
|
||||||
|
if let Ok(dsn) = env::var("SENTRY_DSN") {
|
||||||
|
_sentry = sentry::init(dsn);
|
||||||
|
sentry::integrations::panic::register_panic_handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = env::var("BIND_TO").expect("BIND_TO not set!");
|
||||||
|
let sys = System::new("user-auth-demo");
|
||||||
|
/*actix::Arbiter::handle().spawn({
|
||||||
|
let postmark = emails::Postmark::new(env::var("POSTMARK").expect("No Postmark API key set!");
|
||||||
|
postmark.send("Testing", "123?", "ryan@rymc.io", "ryan@rymc.io").map_err(|e| {
|
||||||
|
println!("Error? {:?}", e);
|
||||||
|
}).and_then(|response| {
|
||||||
|
println!("Response: {:?}", response);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});*/
|
||||||
|
|
||||||
|
let pool = util::database::pool();
|
||||||
|
//let addr = SyncArbiter::start(num_cpus::get() * 3, move || database::Database(pool.clone()));
|
||||||
|
let addr = SyncArbiter::start(12, move || util::database::Database(pool.clone()));
|
||||||
|
|
||||||
|
server::new(move || {
|
||||||
|
let mut app = App::with_state(State {
|
||||||
|
db: addr.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
app = app.middleware(SentryMiddleware::new());
|
||||||
|
app = app.middleware(middleware::Logger::default());
|
||||||
|
|
||||||
|
app = users::configure(app);
|
||||||
|
app = pages::configure(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
}).backlog(8192).workers(4).bind(&address).unwrap().start();
|
||||||
|
|
||||||
|
let _ = sys.run();
|
||||||
|
}
|
||||||
76
src/pages/mod.rs
Normal file
76
src/pages/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// pages/mod.rs
|
||||||
|
//
|
||||||
|
// Basic pages that don't really belong anywhere else, that
|
||||||
|
// I want included but really don't want to deal with separate
|
||||||
|
// repos/etc.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use askama::Template;
|
||||||
|
use actix_web::{http::Method, App};
|
||||||
|
use actix_web::{AsyncResponder, HttpRequest};
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use users::middleware::UserAuthentication;
|
||||||
|
use users::models::User;
|
||||||
|
use util::responses::{FutureResponse, render, redirect};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
pub struct Homepage {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/terms.html")]
|
||||||
|
pub struct TermsOfService {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/privacy.html")]
|
||||||
|
pub struct PrivacyPolicy {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/cookies.html")]
|
||||||
|
pub struct CookiesPolicy {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/about.html")]
|
||||||
|
pub struct About {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/team.html")]
|
||||||
|
pub struct TeamBreakdown {}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "app/index.html")]
|
||||||
|
pub struct AppRoot {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_root(request: HttpRequest<State>) -> FutureResponse {
|
||||||
|
// If the session is blank, or has no UID for the current user, this will
|
||||||
|
// Err() immediately without a database hit.
|
||||||
|
request.user().then(|res| match res {
|
||||||
|
Ok(user) => Ok(render(&AppRoot {
|
||||||
|
user: user
|
||||||
|
})),
|
||||||
|
|
||||||
|
Err(_e) => {
|
||||||
|
Ok(render(&Homepage {}))
|
||||||
|
}
|
||||||
|
}).responder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(application: App<State>) -> App<State> {
|
||||||
|
application
|
||||||
|
.resource("/", |r| {
|
||||||
|
r.name("homepage");
|
||||||
|
r.method(Method::GET).f(render_root);
|
||||||
|
r.method(Method::POST).f(render_root)
|
||||||
|
})
|
||||||
|
.resource("/terms/", |r| r.method(Method::GET).f(|_req| render(&TermsOfService {})))
|
||||||
|
.resource("/privacy/", |r| r.method(Method::GET).f(|_req| render(&PrivacyPolicy {})))
|
||||||
|
.resource("/cookies/", |r| r.method(Method::GET).f(|_req| render(&CookiesPolicy {})))
|
||||||
|
.resource("/about/", |r| r.method(Method::GET).f(|_req| render(&About {})))
|
||||||
|
.resource("/team/", |r| r.method(Method::GET).f(|_req| render(&TeamBreakdown{})))
|
||||||
|
}
|
||||||
27
src/schema.rs
Normal file
27
src/schema.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
table! {
|
||||||
|
news (id) {
|
||||||
|
id -> Int4,
|
||||||
|
title -> Text,
|
||||||
|
url -> Text,
|
||||||
|
added -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
users (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Nullable<Text>,
|
||||||
|
email -> Text,
|
||||||
|
password -> Text,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
|
is_verified -> Bool,
|
||||||
|
has_verified_email -> Bool,
|
||||||
|
created -> Timestamptz,
|
||||||
|
updated -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
news,
|
||||||
|
users,
|
||||||
|
);
|
||||||
98
src/users/middleware.rs
Normal file
98
src/users/middleware.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// src/users/middleware.rs
|
||||||
|
//
|
||||||
|
// Middleware that handles loading current User for a given
|
||||||
|
// request, along with any Session data they may have. This
|
||||||
|
// specifically enables Anonymous users with Session data, ala
|
||||||
|
// Django's approach.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use std::io::{Error as IoError, ErrorKind};
|
||||||
|
use futures::Future;
|
||||||
|
use futures::future;
|
||||||
|
use actix_web::{HttpRequest, Error};
|
||||||
|
use actix_web::middleware::session::RequestSession;
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use users::models::{UserLookup, User};
|
||||||
|
|
||||||
|
pub type UserAuthenticationResult = Box<Future<Item = User, Error = Error>>;
|
||||||
|
|
||||||
|
/// UserAuthentication is kind of a request guard - it returns a Future which will resolve
|
||||||
|
/// with either the current authenticated user, or "error" out if the user has no session data
|
||||||
|
/// that'd tie them to a user profile, or if the session cache can't be read, or if the database
|
||||||
|
/// has issues, or... pick your poison I guess.
|
||||||
|
///
|
||||||
|
/// It enables your views to be as simple as:
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use users::middleware::UserAuthentication;
|
||||||
|
/// use utils::responses::{AsyncHttpResponse, async_redirect};
|
||||||
|
///
|
||||||
|
/// fn view(request: HttpRequest) -> AsyncHttpResponse {
|
||||||
|
/// request.check_authentication().then(move |a| match a {
|
||||||
|
/// Ok(user) => {
|
||||||
|
/// async_redirect("http://www.duckduckgo.com/")
|
||||||
|
/// },
|
||||||
|
///
|
||||||
|
/// Err(_) => {
|
||||||
|
/// async_redirect("http://www.google.com/")
|
||||||
|
/// }
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait UserAuthentication {
|
||||||
|
fn is_authenticated(&self) -> bool;
|
||||||
|
fn user(&self) -> UserAuthenticationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserAuthentication for HttpRequest<State> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn is_authenticated(&self) -> bool {
|
||||||
|
match self.session().get::<i32>("uid") {
|
||||||
|
Ok(session) => {
|
||||||
|
match session {
|
||||||
|
Some(_session_id) => true,
|
||||||
|
None => false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error'd when attempting to fetch session data: {:?}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user(&self) -> UserAuthenticationResult {
|
||||||
|
match self.session().get::<i32>("uid") {
|
||||||
|
Ok(session) => { match session {
|
||||||
|
Some(session_id) => {
|
||||||
|
Box::new(self.state().db.send(UserLookup {
|
||||||
|
id: session_id
|
||||||
|
}).from_err().and_then(|res| match res {
|
||||||
|
Ok(user) => Ok(user),
|
||||||
|
Err(err) => {
|
||||||
|
// Temporary because screw matching all these error types
|
||||||
|
let e = IoError::new(ErrorKind::NotFound, format!("{}", err));
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
let e = IoError::new(ErrorKind::NotFound, "User has no session data.");
|
||||||
|
Box::new(future::err(e.into()))
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error'd when attempting to fetch session data: {:?}", e);
|
||||||
|
Box::new(future::err(e.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/users/mod.rs
Normal file
41
src/users/mod.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// users/mod.rs
|
||||||
|
//
|
||||||
|
// URL dispatcher for user account related API endpoints.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/15/2018
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use actix_redis::{RedisSessionBackend, SameSite};
|
||||||
|
use actix_web::{http::Method, App, middleware::session::SessionStorage};
|
||||||
|
|
||||||
|
use State;
|
||||||
|
pub mod views;
|
||||||
|
pub mod models;
|
||||||
|
pub mod middleware;
|
||||||
|
|
||||||
|
pub fn configure(application: App<State>) -> App<State> {
|
||||||
|
let key = env::var("SECRET_KEY").expect("SECRET_KEY not set!");
|
||||||
|
|
||||||
|
application.middleware(SessionStorage::new(
|
||||||
|
RedisSessionBackend::new(env::var("REDIS").expect("REDIS not set!"), key.as_bytes())
|
||||||
|
.cookie_name("sessionid")
|
||||||
|
.cookie_secure(true)
|
||||||
|
//.cookie_domain("your domain here")
|
||||||
|
.cookie_path("/")
|
||||||
|
.cookie_same_site(SameSite::Lax)
|
||||||
|
)).scope("/users", |scope| {
|
||||||
|
scope.resource("/signup/", |r| {
|
||||||
|
r.method(Method::GET).with(views::Signup::get);
|
||||||
|
r.method(Method::POST).with(views::Signup::post)
|
||||||
|
}).resource("/login/", |r| {
|
||||||
|
r.method(Method::GET).with(views::Login::get);
|
||||||
|
r.method(Method::POST).with(views::Login::post)
|
||||||
|
}).resource("/logout/", |r| {
|
||||||
|
r.method(Method::POST).with(views::logout)
|
||||||
|
}).resource("/forgot_password/", |r| {
|
||||||
|
r.method(Method::GET).with(views::ResetPassword::get);
|
||||||
|
r.method(Method::POST).with(views::ResetPassword::post)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
93
src/users/models.rs
Normal file
93
src/users/models.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
// src/users/models.rs
|
||||||
|
//
|
||||||
|
// Implements a basic User model, with support for creating/updating/deleting
|
||||||
|
// users, along with welcome email and verification.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use chrono;
|
||||||
|
use diesel;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use actix::prelude::*;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use schema::users;
|
||||||
|
use util::database::Database;
|
||||||
|
|
||||||
|
#[derive(Queryable, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub is_verified: bool,
|
||||||
|
pub has_verified_email: bool,
|
||||||
|
pub created: chrono::NaiveDateTime,
|
||||||
|
pub updated: chrono::NaiveDateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Validate, Deserialize, Serialize, Debug)]
|
||||||
|
#[table_name="users"]
|
||||||
|
pub struct NewUser {
|
||||||
|
#[validate(email(message="Hmmm, invalid email provided."))]
|
||||||
|
pub email: String,
|
||||||
|
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for NewUser {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<NewUser> for Database {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: NewUser, _: &mut Self::Context) -> Self::Result {
|
||||||
|
use schema::users::dsl::*;
|
||||||
|
let conn = self.0.get().unwrap();
|
||||||
|
diesel::insert_into(users).values(&msg).get_result::<User>(&conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct UserLookup {
|
||||||
|
pub id: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for UserLookup {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<UserLookup> for Database {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: UserLookup, _: &mut Self::Context) -> Self::Result {
|
||||||
|
use schema::users::dsl::*;
|
||||||
|
let conn = self.0.get().unwrap();
|
||||||
|
users.filter(id.eq(msg.id)).get_result::<User>(&conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Validate, Serialize, Debug)]
|
||||||
|
pub struct UserLogin {
|
||||||
|
#[validate(email(message="Hmmm, invalid email provided."))]
|
||||||
|
pub email: String,
|
||||||
|
pub password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for UserLogin {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<UserLogin> for Database {
|
||||||
|
type Result = Result<User, diesel::result::Error>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: UserLogin, _: &mut Self::Context) -> Self::Result {
|
||||||
|
use schema::users::dsl::*;
|
||||||
|
let conn = self.0.get().unwrap();
|
||||||
|
users.filter(email.eq(msg.email)).get_result::<User>(&conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
85
src/users/views/login.rs
Normal file
85
src/users/views/login.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// src/users/views/login.rs
|
||||||
|
//
|
||||||
|
// Views for user authentication.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use validator::Validate;
|
||||||
|
use futures::future::Future;
|
||||||
|
use djangohashers::{check_password};
|
||||||
|
use actix_web::{Form, AsyncResponder, HttpRequest, HttpResponse};
|
||||||
|
use actix_web::middleware::session::RequestSession;
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use users::models::{UserLogin};
|
||||||
|
use users::middleware::UserAuthentication;
|
||||||
|
use util::forms::collect_validation_errors;
|
||||||
|
use util::responses::{FutureResponse, future_render, future_redirect, redirect, render};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "users/login.html")]
|
||||||
|
pub struct Login {
|
||||||
|
pub errors: Option<Vec<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
pub fn get(request: HttpRequest<State>) -> HttpResponse {
|
||||||
|
if request.is_authenticated() {
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
return redirect(url.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
render(&Self {
|
||||||
|
errors: None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post((request, login): (HttpRequest<State>, Form<UserLogin>)) -> FutureResponse {
|
||||||
|
if request.is_authenticated() {
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
return future_redirect(url.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// No sense in wasting a database call if they didn't pass in an actual email address.
|
||||||
|
// *shrug*
|
||||||
|
let login = login.into_inner();
|
||||||
|
if let Err(e) = login.validate() {
|
||||||
|
return future_render(&Self {
|
||||||
|
errors: Some(collect_validation_errors(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = login.password.clone();
|
||||||
|
request.state().db.send(login).from_err().and_then(move |res| match res {
|
||||||
|
Ok(user) => {
|
||||||
|
// Yo, so I know you're coming here and looking at this .unwrap(), and probably
|
||||||
|
// going "man what is this guy doing?". This should only ever give a Err() result
|
||||||
|
// if we're trying to use an unknown algorithm... which isn't the case here.
|
||||||
|
if check_password(&password, &user.password).unwrap() {
|
||||||
|
if let Err(e) = request.session().set("uid", user.id) {
|
||||||
|
error!("Could not set UID for user session! {:?}", e);
|
||||||
|
return Ok(render(&Self {
|
||||||
|
errors: Some(vec!["An internal error occurred while attempting to sign you in. Please try again in a bit.".into()])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
Ok(redirect(url.as_str()))
|
||||||
|
} else {
|
||||||
|
Ok(render(&Self {
|
||||||
|
errors: Some(vec!["Email or password is incorrect.".into()])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error locating user: {:?}", e);
|
||||||
|
Ok(render(&Self {
|
||||||
|
errors: Some(vec!["Email or password is incorrect.".into()])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}).responder()
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/users/views/logout.rs
Normal file
19
src/users/views/logout.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// src/users/views/signup.rs
|
||||||
|
//
|
||||||
|
// Endpoint for logging a user out. Nothing particularly special,
|
||||||
|
// just obliterates their session entry + cookie.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
|
use actix_web::middleware::session::RequestSession;
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use util::responses::redirect;
|
||||||
|
|
||||||
|
pub fn logout(request: HttpRequest<State>) -> HttpResponse {
|
||||||
|
request.session().clear();
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
redirect(url.as_str())
|
||||||
|
}
|
||||||
18
src/users/views/mod.rs
Normal file
18
src/users/views/mod.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// src/users/views/mod.rs
|
||||||
|
//
|
||||||
|
// View hoisting. *shrug*
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
pub mod signup;
|
||||||
|
pub use self::signup::Signup;
|
||||||
|
|
||||||
|
pub mod login;
|
||||||
|
pub use self::login::Login;
|
||||||
|
|
||||||
|
pub mod logout;
|
||||||
|
pub use self::logout::logout;
|
||||||
|
|
||||||
|
pub mod reset_password;
|
||||||
|
pub use self::reset_password::ResetPassword;
|
||||||
44
src/users/views/reset_password.rs
Normal file
44
src/users/views/reset_password.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// src/users/views/reset_password.rs
|
||||||
|
//
|
||||||
|
// Views for user registration.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use futures::future::Future;
|
||||||
|
use djangohashers::{make_password};
|
||||||
|
use actix_web::{Form, AsyncResponder, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use users::models::{NewUser};
|
||||||
|
use util::responses::{FutureResponse, render, redirect};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "users/reset_password.html")]
|
||||||
|
pub struct ResetPassword<'a> {
|
||||||
|
pub error: Option<&'a str>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ResetPassword<'a> {
|
||||||
|
pub fn get(_req: HttpRequest<State>) -> HttpResponse {
|
||||||
|
render(&ResetPassword {
|
||||||
|
error: None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post((req, item): (HttpRequest<State>, Form<NewUser>)) -> FutureResponse {
|
||||||
|
let mut item = item.into_inner();
|
||||||
|
item.password = make_password(&item.password);
|
||||||
|
|
||||||
|
req.state().db.send(item).from_err().and_then(|res| match res {
|
||||||
|
Ok(_) => Ok(redirect("/")),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error creating new user: {:?}", e);
|
||||||
|
Ok(render(&ResetPassword {
|
||||||
|
error: Some("An error occurred!")
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}).responder()
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/users/views/signup.rs
Normal file
81
src/users/views/signup.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
// src/users/views/signup.rs
|
||||||
|
//
|
||||||
|
// Views for user registration.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use validator::Validate;
|
||||||
|
use futures::future::Future;
|
||||||
|
use djangohashers::{make_password};
|
||||||
|
use actix_web::{Form, AsyncResponder, HttpRequest, HttpResponse};
|
||||||
|
use actix_web::middleware::session::RequestSession;
|
||||||
|
|
||||||
|
use State;
|
||||||
|
use users::middleware::UserAuthentication;
|
||||||
|
use users::models::{NewUser};
|
||||||
|
use util::forms::collect_validation_errors;
|
||||||
|
use util::responses::{FutureResponse, render, future_render, redirect, future_redirect};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "users/signup.html")]
|
||||||
|
pub struct Signup {
|
||||||
|
pub errors: Option<Vec<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signup {
|
||||||
|
pub fn get(request: HttpRequest<State>) -> HttpResponse {
|
||||||
|
if request.is_authenticated() {
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
return redirect(url.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
render(&Self {
|
||||||
|
errors: None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post((request, user): (HttpRequest<State>, Form<NewUser>)) -> FutureResponse {
|
||||||
|
if request.is_authenticated() {
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
return future_redirect(url.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut user = user.into_inner();
|
||||||
|
if let Err(e) = user.validate() {
|
||||||
|
return future_render(&Self {
|
||||||
|
errors: Some(collect_validation_errors(e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.password = make_password(&user.password);
|
||||||
|
request.state().db.send(user).from_err().and_then(move |res| match res {
|
||||||
|
Ok(user) => {
|
||||||
|
if let Err(e) = request.session().set("uid", user.id) {
|
||||||
|
error!("Could not set UID for user session! {:?}", e);
|
||||||
|
return Ok(render(&Self {
|
||||||
|
errors: Some(vec![
|
||||||
|
"Your account was created, but an internal error happened while \
|
||||||
|
attempting to sign you in. Try again in a bit!".into()
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = request.url_for("homepage", &[""; 0]).unwrap();
|
||||||
|
Ok(redirect(url.as_str()))
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating new user: {:?}", e);
|
||||||
|
Ok(render(&Self {
|
||||||
|
errors: Some(vec![
|
||||||
|
"An error occurred while trying to create your account. We've \
|
||||||
|
notified the engineering team and are looking into it - feel \
|
||||||
|
free to contact us for more information, or if you continue to \
|
||||||
|
see the issue after a short period.".into()
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}).responder()
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/util/database.rs
Normal file
26
src/util/database.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// database.rs
|
||||||
|
//
|
||||||
|
// Handles setting up database routines, state, and such
|
||||||
|
// to work within actix-web.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/16/2018
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use actix::prelude::*;
|
||||||
|
use diesel::pg::PgConnection;
|
||||||
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
|
|
||||||
|
pub fn pool() -> Pool<ConnectionManager<PgConnection>> {
|
||||||
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set!");
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
|
Pool::new(manager).expect("Error creating Postgres connection pool!")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Database(pub Pool<ConnectionManager<PgConnection>>);
|
||||||
|
|
||||||
|
unsafe impl Send for Database {}
|
||||||
|
|
||||||
|
impl Actor for Database {
|
||||||
|
type Context = SyncContext<Self>;
|
||||||
|
}
|
||||||
22
src/util/forms.rs
Normal file
22
src/util/forms.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// src/util/forms.rs
|
||||||
|
//
|
||||||
|
// Helper methods for dealing with certain form-related things, like... validation.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use validator::ValidationErrors;
|
||||||
|
|
||||||
|
/// Handles collecting validation errors from Form-ish structs.
|
||||||
|
/// This has to be done this way to work with Askama templates, which... well, I'm
|
||||||
|
/// not sold on, but it gets the job done for now.
|
||||||
|
///
|
||||||
|
/// @TODO: See about String -> &str?
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn collect_validation_errors(e: ValidationErrors) -> Vec<String> {
|
||||||
|
e.inner().into_iter().map(|(_k, v)| {
|
||||||
|
v.into_iter().map(|a| {
|
||||||
|
a.message.unwrap().to_string()
|
||||||
|
}).collect()
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
10
src/util/mod.rs
Normal file
10
src/util/mod.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// util/mod.rs
|
||||||
|
//
|
||||||
|
// Various utility methods and helper libs that persist throughout the project.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
pub mod forms;
|
||||||
|
pub mod responses;
|
||||||
|
pub mod database;
|
||||||
38
src/util/responses.rs
Normal file
38
src/util/responses.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// src/util/responses.rs
|
||||||
|
//
|
||||||
|
// Basic response objects that are commonly thrown about in API calls.
|
||||||
|
//
|
||||||
|
// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
// @created 06/18/2018
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use futures::future::{Future, result};
|
||||||
|
use actix_web::{HttpResponse, Error, AsyncResponder};
|
||||||
|
|
||||||
|
pub type FutureResponse = Box<Future<Item = HttpResponse, Error = Error>>;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct OperationResponse<'a> {
|
||||||
|
pub success: bool,
|
||||||
|
pub message: Option<&'a str>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn render(template: &Template) -> HttpResponse {
|
||||||
|
HttpResponse::Ok().content_type("text/html").body(template.render().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn future_render(template: &Template) -> FutureResponse {
|
||||||
|
result(Ok(HttpResponse::Ok().content_type("text/html").body(template.render().unwrap()))).responder()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn redirect(location: &str) -> HttpResponse {
|
||||||
|
HttpResponse::TemporaryRedirect().header("Location", location).finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn future_redirect(location: &str) -> FutureResponse {
|
||||||
|
result(Ok(HttpResponse::TemporaryRedirect().header("Location", location).finish())).responder()
|
||||||
|
}
|
||||||
1
templates/app/index.html
Normal file
1
templates/app/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
LOGGED IN!!!!
|
||||||
3
templates/index.html
Normal file
3
templates/index.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
19
templates/layout.html
Normal file
19
templates/layout.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if IE 8]><html class="lt-ie9"><![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--><html><!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0">
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="nav">
|
||||||
|
<a href="/users/signup/">Signup</a>
|
||||||
|
<a href="/users/login/">Login</a>
|
||||||
|
</div>
|
||||||
|
<div id="content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
templates/pages/about.html
Normal file
3
templates/pages/about.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
3
templates/pages/cookies.html
Normal file
3
templates/pages/cookies.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
3
templates/pages/privacy.html
Normal file
3
templates/pages/privacy.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
3
templates/pages/team.html
Normal file
3
templates/pages/team.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
3
templates/pages/terms.html
Normal file
3
templates/pages/terms.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}HELLO!{% endblock %}
|
||||||
13
templates/users/forgot_password.html
Normal file
13
templates/users/forgot_password.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Signup{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="/signup">
|
||||||
|
<input type="text" name="name">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<input type="submit" value="Go">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
8
templates/users/forgot_password_sent.html
Normal file
8
templates/users/forgot_password_sent.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Signup{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
13
templates/users/login.html
Normal file
13
templates/users/login.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Signup{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="/users/login/">
|
||||||
|
<input type="text" name="name">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<input type="submit" value="Go">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
1
templates/users/reset_password.html
Normal file
1
templates/users/reset_password.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1></h1>
|
||||||
23
templates/users/signup.html
Normal file
23
templates/users/signup.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Signup{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% match errors %}
|
||||||
|
{% when Some with (errs) %}
|
||||||
|
<ul>
|
||||||
|
{% for e in errs %}
|
||||||
|
<li>{{ e }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
|
||||||
|
<form method="POST" action="/users/signup/">
|
||||||
|
<input type="text" name="email">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<input type="submit" value="Go">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
Reference in a new issue