User accounts, DAO trait, registration.

Not very thorough, but it does work.
This commit is contained in:
projectmoon 2020-12-05 13:07:33 +00:00
parent 5968f6458d
commit 10d5f30e5a
15 changed files with 377 additions and 104 deletions

61
Cargo.lock generated
View File

@ -79,6 +79,18 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -126,12 +138,29 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "blake2b_simd"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.7.3" version = "0.7.3"
@ -208,6 +237,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.11.3" version = "0.11.3"
@ -224,6 +259,17 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"lazy_static",
]
[[package]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.7.0" version = "0.7.0"
@ -1086,6 +1132,18 @@ dependencies = [
"unicode-xid 0.1.0", "unicode-xid 0.1.0",
] ]
[[package]]
name = "rust-argon2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [
"base64 0.13.0",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.18" version = "0.1.18"
@ -1246,8 +1304,11 @@ name = "tenebrous-sheets"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"diesel", "diesel",
"log 0.4.11",
"rand",
"rocket", "rocket",
"rocket_contrib", "rocket_contrib",
"rust-argon2",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",

View File

@ -12,6 +12,9 @@ serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
diesel = "1.4" diesel = "1.4"
thiserror = "1.0" thiserror = "1.0"
rust-argon2 = "0.8"
log = "0.4"
rand = "0.7"
rocket = { version= "0.4.6", features = ["private-cookies"] } rocket = { version= "0.4.6", features = ["private-cookies"] }
[dependencies.rocket_contrib] [dependencies.rocket_contrib]

View File

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

View File

@ -0,0 +1,5 @@
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
);

View File

@ -1,39 +1,74 @@
use crate::models::characters::{CharacterEntry, NewCharacter}; use crate::models::characters::{CharacterEntry, NewCharacter};
use crate::models::users::{NewUser, User};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::SqliteConnection;
#[database("tenebrous_db")] #[database("tenebrous_db")]
pub(crate) struct TenebrousDbConn(diesel::SqliteConnection); pub(crate) struct TenebrousDbConn(SqliteConnection);
pub(crate) fn load_character_list( pub(crate) trait Dao {
conn: TenebrousDbConn, fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
for_user_id: i32, fn load_user(&self, for_username: &str) -> QueryResult<Option<User>>;
) -> QueryResult<Vec<CharacterEntry>> {
use crate::schema::characters::dsl::*; fn insert_user(&self, new_user: &NewUser) -> QueryResult<User>;
characters.filter(user_id.eq(for_user_id)).load(&*conn)
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<CharacterEntry>>;
fn load_character(&self, character_id: i32) -> QueryResult<Option<CharacterEntry>>;
fn insert_character(&self, new_character: &NewCharacter) -> QueryResult<()>;
} }
pub(crate) fn load_character( impl Dao for TenebrousDbConn {
conn: TenebrousDbConn, fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
character_id: i32, use crate::schema::users::dsl::*;
) -> QueryResult<Option<CharacterEntry>> {
users.filter(id.eq(user_id)).first(&self.0).optional()
}
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>> {
use crate::schema::users::dsl::*;
users
.filter(username.eq(for_username))
.first(&self.0)
.optional()
}
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User> {
use crate::schema::users;
diesel::insert_into(users::table)
.values(new_user)
.execute(&self.0)?;
use crate::schema::users::dsl::*;
Ok(users
.filter(username.eq(new_user.username))
.first(&self.0)?)
}
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<CharacterEntry>> {
use crate::schema::characters::dsl::*;
characters.filter(user_id.eq(for_user_id)).load(&self.0)
}
fn load_character(&self, character_id: i32) -> QueryResult<Option<CharacterEntry>> {
use crate::schema::characters::dsl::*; use crate::schema::characters::dsl::*;
characters characters
.filter(id.eq(character_id)) .filter(id.eq(character_id))
.limit(1) .first(&self.0)
.first(&*conn)
.optional() .optional()
} }
pub(crate) fn insert_character( fn insert_character(&self, new_character: &NewCharacter) -> QueryResult<()> {
conn: TenebrousDbConn,
new_character: &NewCharacter,
) -> QueryResult<()> {
use crate::schema::characters; use crate::schema::characters;
diesel::insert_into(characters::table) diesel::insert_into(characters::table)
.values(new_character) .values(new_character)
.execute(&*conn)?; .execute(&self.0)?;
Ok(()) Ok(())
}
} }

View File

@ -21,6 +21,10 @@ pub enum Error {
QueryError(#[from] diesel::result::Error), QueryError(#[from] diesel::result::Error),
} }
#[derive(Error, Debug)]
#[error("internal eror")]
pub struct SensitiveError(Error);
impl Error { impl Error {
fn is_sensitive(&self) -> bool { fn is_sensitive(&self) -> bool {
use Error::*; use Error::*;
@ -33,6 +37,7 @@ impl Error {
impl<'r> Responder<'r> for Error { impl<'r> Responder<'r> for Error {
fn respond_to(self, req: &Request) -> response::Result<'r> { fn respond_to(self, req: &Request) -> response::Result<'r> {
log::error!("error: {0}", self.to_string());
//Hide sensitive error information //Hide sensitive error information
let message: String = if self.is_sensitive() { let message: String = if self.is_sensitive() {
"internal error".into() "internal error".into()
@ -53,3 +58,22 @@ impl<'r> Responder<'r> for Error {
} }
} }
} }
impl<'r> Responder<'r> for SensitiveError {
fn respond_to(self, req: &Request) -> response::Result<'r> {
log::error!("sensitive error: {0}", self.0.to_string());
let message: String = self.to_string();
let mut context = HashMap::new();
context.insert("message", message);
let resp = Template::render("error", context).respond_to(req)?;
use Error::*;
match self.0 {
NotFound => status::NotFound(resp).respond_to(req),
NotLoggedIn => status::Forbidden(Some(resp)).respond_to(req),
NoPermission => status::Forbidden(Some(resp)).respond_to(req),
_ => status::Custom(Status::InternalServerError, resp).respond_to(req),
}
}
}

View File

@ -1,44 +1,2 @@
use rocket::http::RawStr;
use rocket::outcome::IntoOutcome;
use rocket::request::{self, FromParam, FromRequest, Request};
use serde_derive::Serialize;
pub mod characters; pub mod characters;
pub mod users;
#[derive(Eq, PartialEq, Serialize, Debug)]
pub struct User {
pub id: i32,
pub username: String,
}
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = !;
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, !> {
request
.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.map(|id|
//TODO load from db
User {
id: id,
username: "somebody".to_string(),
})
.or_forward(())
}
}
impl<'r> FromParam<'r> for User {
type Error = &'r str;
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
let username: String = param.url_decode().or(Err("Invalid character ID"))?;
//TODO load from DB
Ok(User {
id: 1,
username: username,
})
}
}

56
src/models/users.rs Normal file
View File

@ -0,0 +1,56 @@
use crate::db::{Dao, TenebrousDbConn};
use crate::schema::users;
use argon2::{self, Config, Error as ArgonError};
use rand::Rng;
use rocket::outcome::IntoOutcome;
use rocket::request::{self, FromRequest, Request};
use serde_derive::Serialize;
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
let salt = rand::thread_rng().gen::<[u8; 16]>();
let config = Config::default();
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
}
#[derive(Eq, PartialEq, Serialize, Debug, Queryable)]
pub struct User {
pub id: i32,
pub username: String,
pub password: String,
}
impl User {
pub fn verify_password(&self, raw_password: &str) -> bool {
argon2::verify_encoded(&self.password, raw_password.as_bytes()).unwrap_or(false)
}
}
impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
type Error = !;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, !> {
let user: &Option<User> = request.local_cache(|| {
let attempt_load_user = |id| -> Option<User> {
TenebrousDbConn::from_request(request)
.map(|conn| conn.load_user_by_id(id).ok().flatten())
.succeeded()
.flatten()
};
request
.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.and_then(attempt_load_user)
});
user.as_ref().or_forward(())
}
}
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub username: &'a str,
pub password: &'a str,
}

View File

@ -1,4 +1,6 @@
use crate::models::User; use crate::db::{Dao, TenebrousDbConn};
use crate::models::users::{self, NewUser, User};
use log::error;
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, Cookies};
use rocket::request::{FlashMessage, Form}; use rocket::request::{FlashMessage, Form};
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
@ -6,7 +8,14 @@ use rocket_contrib::templates::Template;
use std::collections::HashMap; use std::collections::HashMap;
pub(crate) fn routes() -> Vec<rocket::Route> { pub(crate) fn routes() -> Vec<rocket::Route> {
routes![login, logout, logged_in_user, login_page] routes![
login,
logout,
logged_in_user,
login_page,
register_page,
register
]
} }
#[derive(FromForm)] #[derive(FromForm)]
@ -15,27 +24,58 @@ struct Login {
password: String, password: String,
} }
#[derive(FromForm)]
struct Registration {
username: String,
password: String,
}
fn add_login_cookie(cookies: &mut Cookies, user: &User) {
cookies.add_private(Cookie::new("user_id", user.id.to_string()));
}
fn remove_login_cookie(cookies: &mut Cookies) {
cookies.remove_private(Cookie::named("user_id"));
}
fn login_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
Flash::error(Redirect::to(uri!(login_page)), message.as_ref())
}
fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
Flash::error(Redirect::to(uri!(register_page)), message.as_ref())
}
#[post("/login", data = "<login>")] #[post("/login", data = "<login>")]
fn login(mut cookies: Cookies, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> { fn login(
if login.username == "test" && login.password == "test" { mut cookies: Cookies,
cookies.add_private(Cookie::new("user_id", 1.to_string())); login: Form<Login>,
conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> {
let user = conn
.load_user(&login.username)
.map_err(|e| {
error!("login - error loading user user: {}", e);
login_error_redirect("Internal error.")
})?
.ok_or_else(|| login_error_redirect("Invalid username or password."))?;
if user.verify_password(&login.password) {
add_login_cookie(&mut cookies, &user);
Ok(Redirect::to(uri!(super::root::index))) Ok(Redirect::to(uri!(super::root::index)))
} else { } else {
Err(Flash::error( Err(login_error_redirect("Invalid username or password."))
Redirect::to(uri!(login_page)),
"Invalid username orpassword.",
))
} }
} }
#[post("/logout")] #[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> { fn logout(mut cookies: Cookies) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id")); remove_login_cookie(&mut cookies);
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
} }
#[get("/login")] #[get("/login")]
fn logged_in_user(_user: User) -> Redirect { fn logged_in_user(_user: &User) -> Redirect {
Redirect::to(uri!(super::root::index)) Redirect::to(uri!(super::root::index))
} }
@ -48,3 +88,50 @@ fn login_page(flash: Option<FlashMessage>) -> Template {
Template::render("login", &context) Template::render("login", &context)
} }
#[get("/register")]
fn register_page(flash: Option<FlashMessage>) -> Template {
let mut context = HashMap::new();
if let Some(ref msg) = flash {
context.insert("flash", msg.msg());
}
Template::render("registration", &context)
}
#[post("/register", data = "<registration>")]
fn register(
mut cookies: Cookies,
registration: Form<Registration>,
conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> {
let existing_user = conn.load_user(&registration.username).map_err(|e| {
error!("registration - error loading existing user: {}", e);
registration_error_redirect("There was an error attempting to register.")
})?;
if existing_user.is_some() {
return Err(registration_error_redirect(format!(
"The username {} is already taken.",
registration.username
)));
}
let hashed_pw = users::hash_password(&registration.password).map_err(|e| {
error!("registration - password hashing error: {}", e);
registration_error_redirect("There was an error attempting to register.")
})?;
let user = NewUser {
username: &registration.username,
password: &hashed_pw,
};
let user = conn.insert_user(&user).map_err(|e| {
error!("registration - could not insert user: {}", e);
registration_error_redirect("There was an error completing registration.")
})?;
add_login_cookie(&mut cookies, &user);
Ok(Redirect::to(uri!(super::root::index)))
}

View File

@ -1,8 +1,8 @@
use crate::db::{self, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::{ use crate::models::{
characters::{CharacterEntry, NewCharacter}, characters::{CharacterEntry, NewCharacter},
User, users::User,
}; };
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
@ -21,20 +21,20 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
//TODO make private -- currently is referenced in homepage route. //TODO make private -- currently is referenced in homepage route.
//or move to common place. //or move to common place.
#[derive(Serialize)] #[derive(Serialize)]
pub struct TemplateContext { pub struct TemplateContext<'a> {
pub characters: Vec<CharacterEntry>, pub characters: Vec<CharacterEntry>,
pub user: User, pub user: &'a User,
} }
//TODO should return result based on whether or not character is publicly viewable. //TODO should return result based on whether or not character is publicly viewable.
#[get("/<user>/<character_id>")] #[get("/<username>/<character_id>")]
fn view_character( fn view_character(
character_id: i32, character_id: i32,
user: Option<User>, username: String,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let user = user.ok_or(Error::NotFound)?; let user = conn.load_user(&username)?.ok_or(Error::NotFound)?;
let character = db::load_character(conn, character_id)?.ok_or(Error::NotFound)?; let character = conn.load_character(character_id)?.ok_or(Error::NotFound)?;
let mut context = HashMap::new(); let mut context = HashMap::new();
context.insert("name", character.name); context.insert("name", character.name);
@ -43,7 +43,7 @@ fn view_character(
} }
#[get("/new")] #[get("/new")]
fn new_character(logged_in_user: User, conn: TenebrousDbConn) -> Result<Template, Error> { fn new_character(logged_in_user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
let context = HashMap::<String, String>::new(); let context = HashMap::<String, String>::new();
Ok(Template::render("new_character", context)) Ok(Template::render("new_character", context))
} }
@ -56,15 +56,15 @@ fn new_character_not_logged_in() -> Redirect {
#[get("/<owner>/<character_id>/edit")] #[get("/<owner>/<character_id>/edit")]
fn edit_character( fn edit_character(
character_id: i32, character_id: i32,
owner: Option<User>, owner: String,
logged_in_user: Option<User>, logged_in_user: Option<&User>,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let owner = owner.ok_or(Error::NotFound)?; let owner = conn.load_user(&owner)?.ok_or(Error::NotFound)?;
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?; let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character = db::load_character(conn, character_id)?.ok_or(Error::NotFound)?; let character = conn.load_character(character_id)?.ok_or(Error::NotFound)?;
if logged_in_user != owner { if logged_in_user != &owner {
return Err(Error::NoPermission); return Err(Error::NoPermission);
} }

View File

@ -1,7 +1,6 @@
use crate::db; use crate::db::{Dao, TenebrousDbConn};
use crate::db::TenebrousDbConn;
use crate::errors::Error; use crate::errors::Error;
use crate::models::{characters::CharacterEntry, User}; use crate::models::{characters::CharacterEntry, users::User};
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
@ -10,9 +9,9 @@ pub fn routes() -> Vec<rocket::Route> {
} }
#[get("/")] #[get("/")]
fn user_index(user: User, conn: TenebrousDbConn) -> Result<Template, Error> { fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
use crate::routes::characters::TemplateContext; use crate::routes::characters::TemplateContext;
let characters = db::load_character_list(conn, user.id)?; let characters = conn.load_character_list(user.id)?;
let context = TemplateContext { let context = TemplateContext {
characters: characters, characters: characters,

View File

@ -7,3 +7,16 @@ table! {
character_data -> Nullable<Binary>, character_data -> Nullable<Binary>,
} }
} }
table! {
users (id) {
id -> Integer,
username -> Text,
password -> Text,
}
}
allow_tables_to_appear_in_same_query!(
characters,
users,
);

View File

@ -13,5 +13,11 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<p>
<form action="/logout" method="post">
<input type="submit" value="Logout" />
</form>
</p>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -15,7 +15,7 @@
<input type="text" name="username" id="username" value="" /> <input type="text" name="username" id="username" value="" />
<label for="password">password</label> <label for="password">password</label>
<input type="password" name="password" id="password" value="" /> <input type="password" name="password" id="password" value="" />
<p><input type="submit" value="login"></p> <p><input type="submit" value="Login"></p>
</form> </form>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,25 @@
{% extends "base" %}
{% block content %}
<div>
<h1>Registration</h1>
{% if flash %}
<p>Error: {{ flash }}</p>
{% endif %}
<p>Please register with a username and password.</p>
<form action="/register" method="post" accept-charset="utf-8">
<div>
<label for="username">Username:</label>
<input id="username" name="username" type="text" value="" />
<label for="password">Password:</label>
<input id="password" name="password" type="password" value="" />
</div>
<div>
<input type="submit" value="Register" />
</div>
</form>
</div>
{% endblock content %}