Basic user management

Signup and login (but no session handling yet).
This commit is contained in:
Rasmus Kaj 2018-08-18 16:40:05 +02:00
parent 1b4a9c14fa
commit cb5bc07c7f
15 changed files with 262 additions and 38 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
/Cargo.lock
/target
/.env
**/*.rs.bk

View File

@ -1,5 +1,5 @@
[package]
name = "warp-ructe"
name = "warp-db-session"
version = "0.1.0"
authors = ["Rasmus Kaj <kaj@kth.se>"]
@ -12,3 +12,9 @@ ructe = { version = "0.4.0", features = ["sass", "mime03"] }
warp = "0.1.2"
mime = "0.3.0"
env_logger = "0.5.0"
log = "*"
diesel = { version = "1.0.0", features = ["r2d2", "postgres"] }
dotenv = "0.13.0"
serde = "1.0.0"
serde_derive = "1.0.0"
bcrypt = "0.2.0"

5
diesel.toml Normal file
View 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"

0
migrations/.gitkeep Normal file
View File

View 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();

View 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;

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,8 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR UNIQUE NOT NULL,
realname VARCHAR NOT NULL,
password VARCHAR UNIQUE NOT NULL
);
CREATE UNIQUE INDEX users_username_idx ON users (username);

4
rustfmt.toml Normal file
View File

@ -0,0 +1,4 @@
max_width = 78
reorder_imports = true
report_fixme = "Always"

View File

@ -1,52 +1,194 @@
//! An example web service using ructe with the warp framework.
#![deny(warnings)]
extern crate bcrypt;
#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate env_logger;
#[macro_use]
extern crate log;
extern crate mime;
#[macro_use]
extern crate serde_derive;
extern crate warp;
mod models;
mod render_ructe;
mod schema;
use diesel::insert_into;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use dotenv::dotenv;
use render_ructe::RenderRucte;
use std::env;
use std::io::{self, Write};
use std::time::{Duration, SystemTime};
use templates::statics::StaticFile;
use warp::http::{Response, StatusCode};
use warp::{path, reject, Filter, Rejection, Reply};
use warp::{reject, Filter, Rejection, Reply};
/// Main program: Set up routes and start server.
fn main() {
dotenv().ok();
env_logger::init();
let login_routes =
path("login").and(warp::index())
.and(
warp::get2().and_then(login_form)
.or(warp::post2().and_then(do_login))
);
let routes = warp::get2()
let pool = pg_pool();
// setup the the connection pool to get a connection on each request
let pg =
warp::any()
.map(move || pool.clone())
.and_then(|pool: PgPool| match pool.get() {
Ok(conn) => Ok(conn),
Err(_) => Err(reject::server_error()),
});
use warp::{body, get2 as get, index, path, post2 as post};
let login_routes = path("login").and(index()).and(
get()
.and_then(login_form)
.or(post().and(pg.clone()).and(body::form()).and_then(do_login)),
);
let signup_routes = path("signup").and(index()).and(
get()
.and_then(signup_form)
.or(post().and(pg).and(body::form()).and_then(do_signup)),
);
let routes = get()
.and(
warp::index()
.and_then(home_page)
.or(path("static").and(path::param()).and_then(static_file))
.or(path("bad").and_then(bad_handler)),
).or(login_routes)
.or(signup_routes)
.recover(customize_error);
warp::serve(routes).run(([127, 0, 0, 1], 3030));
}
type PgPool = Pool<ConnectionManager<PgConnection>>;
type PooledPg = PooledConnection<ConnectionManager<PgConnection>>;
fn pg_pool() -> PgPool {
let database_url =
env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::new(manager).expect("Postgres connection pool could not be created")
}
/// Render a login form.
fn login_form() -> Result<impl Reply, Rejection> {
Response::builder().html(|o| {
templates::login(o, None, None)
})
Response::builder().html(|o| templates::login(o, None, None))
}
fn do_login() -> Result<impl Reply, Rejection> {
Response::builder().html(|o| {
templates::login(o, None, None)
})
/// Verify a login attempt.
/// If the credentials in the LoginForm are correct, redirect to the home page.
/// Otherwise, show the login form again, but with a message.
fn do_login(db: PooledPg, form: LoginForm) -> Result<impl Reply, Rejection> {
use schema::users::dsl::*;
let authenticated = users
.filter(username.eq(&form.user))
.select(password)
.first::<String>(&db)
.map_err(|e| {
error!("Failed to load hash for {:?}: {:?}", form.user, e);
()
}).and_then(|hash: String| {
bcrypt::verify(&form.password, &hash).map_err(|e| {
error!("Failed to verify hash for {:?}: {:?}", form.user, e)
})
}).unwrap_or(false);
eprintln!("Database result: {:?}", authenticated);
if authenticated {
Response::builder()
.status(StatusCode::FOUND)
.header("location", "/")
// TODO: Set a session cookie?
.body(b"".to_vec())
.map_err(|_| reject::server_error()) // TODO This seems ugly?
} else {
Response::builder().html(|o| {
templates::login(o, None, Some("Authentication failed"))
})
}
}
/// The data submitted by the login form.
/// This does not derive Debug or Serialize, as the password is plain text.
#[derive(Deserialize)]
struct LoginForm {
user: String,
password: String,
}
/// Render a login form.
fn signup_form() -> Result<impl Reply, Rejection> {
Response::builder().html(|o| templates::signup(o, None))
}
/// Verify a login attempt.
/// If the credentials in the LoginForm are correct, redirect to the home page.
/// Otherwise, show the login form again, but with a message.
fn do_signup(
db: PooledPg,
form: SignupForm,
) -> Result<impl Reply, Rejection> {
let result = form
.validate()
.map_err(|e| e.to_string())
.and_then(|form| {
let hash = bcrypt::hash(&form.password, bcrypt::DEFAULT_COST)
.map_err(|e| format!("Hash failed: {}", e))?;
Ok((form, hash))
}).and_then(|(form, hash)| {
use schema::users::dsl::*;
insert_into(users)
.values((
username.eq(form.user),
realname.eq(form.realname),
password.eq(&hash),
)).execute(&db)
.map_err(|e| format!("Oops: {}", e))
});
match result {
Ok(_) => {
Response::builder()
.status(StatusCode::FOUND)
.header("location", "/")
// TODO: Set a session cookie?
.body(b"".to_vec())
.map_err(|_| reject::server_error()) // TODO This seems ugly?
}
Err(msg) => {
Response::builder().html(|o| templates::signup(o, Some(&msg)))
}
}
}
/// The data submitted by the login form.
/// This does not derive Debug or Serialize, as the password is plain text.
#[derive(Deserialize)]
struct SignupForm {
user: String,
realname: String,
password: String,
}
impl SignupForm {
fn validate(self) -> Result<Self, &'static str> {
if self.user.len() < 2 {
Err("Username must be at least two characters")
} else if self.realname.is_empty() {
Err("A real name (or pseudonym) must be given")
} else if self.password.len() < 3 {
Err("Please use a better password")
} else {
Ok(self)
}
}
}
/// Home page handler; just render a template with some arguments.
fn home_page() -> Result<impl Reply, Rejection> {

1
src/models.rs Normal file
View File

@ -0,0 +1 @@
// No models here yet.

8
src/schema.rs Normal file
View File

@ -0,0 +1,8 @@
table! {
users (id) {
id -> Int4,
username -> Varchar,
realname -> Varchar,
password -> Varchar,
}
}

View File

@ -1,22 +1,10 @@
@use templates::statics::*;
@use footer;
@use templates::page_base;
@(paras: &[(&str, usize)])
<!doctype html>
<html lang="sv">
<head>
<title>Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" href="/static/@style_css.name"/>
</head>
<body>
<main>
<h1>Example</h1>
@:page_base("Example", {
<p>This is a simple sample page,
to be served with gotham.
It contains an image of a squirrel and not much more.</p>
<p>This is a simple sample page.</p>
@for (order, n) in paras {
<p>This is a @order paragraph, with @n repeats.
@ -24,7 +12,4 @@
This is a @order paragraph.
}
}
</main>
@:footer()
</body>
</html>
})

View File

@ -12,10 +12,13 @@
</head>
<body>
<header>
<span>Example app</span>
@*
@if let Some(u) = MySession::get_user(state) {<span class="user">@u (<a href="/logout">log out</a>)</span>}
else {<span class="user">(<a href="/login@if let Some(p) = Uri::try_borrow_from(state) {?next=@p}">log in</a>)</span>}
*@
else { *@
<span class="user">(<a href="/login">log in</a> or
<a href="/signup">sign up</a>)</span>
@* } *@
</header>
<main>

18
templates/signup.rs.html Normal file
View File

@ -0,0 +1,18 @@
@use templates::page_base;
@(message: Option<&str>)
@:page_base("login", {
<form action="" method="post">
@if let Some(message) = message {<p>@message</p>}
<p><label for="user">User:</label>
<input id="user" name="user"></p>
<p><label for="realname">Name:</label>
<input id="realname" name="realname"></p>
<p><label for="password">Password:</label>
<input id="password" name="password" type="password"></p>
<p><span>[<a href="/">Cancel</a>]</span>
<input type="submit" value="Log in">
</p>
</form>
})