Basic user management
Signup and login (but no session handling yet).
This commit is contained in:
parent
1b4a9c14fa
commit
cb5bc07c7f
|
@ -2,4 +2,5 @@
|
|||
|
||||
/Cargo.lock
|
||||
/target
|
||||
/.env
|
||||
**/*.rs.bk
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,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();
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE users;
|
|
@ -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);
|
|
@ -0,0 +1,4 @@
|
|||
max_width = 78
|
||||
|
||||
reorder_imports = true
|
||||
report_fixme = "Always"
|
174
src/main.rs
174
src/main.rs
|
@ -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> {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// No models here yet.
|
|
@ -0,0 +1,8 @@
|
|||
table! {
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
username -> Varchar,
|
||||
realname -> Varchar,
|
||||
password -> Varchar,
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
})
|
Loading…
Reference in New Issue