Migrate to Rocket 0.5.

Rocket 0.5 is a major uprade, rewriting most of Rocket to be async.
Required many changes through the code, especially the database layer.
The new Rocket async database calls require Futures with 'static
lifetimes.

General:
 - Move to stable rust.
 - Most of codebase is now async.
 - Rocket migrations (e.g. Cookies to CookieJar).

Database:
 - Switched to owned data (&str -> String) for inserts because of the
   'static lifetime requirement on Rocket's DB future.
 - All database methods now asynchronous.

Pages:
 - Changed various routes to async.
 - Needed to add clone calls to some places because we need to use
   owned data multiple times (registration).
This commit is contained in:
projectmoon 2020-12-13 13:57:50 +00:00
parent 4024f32621
commit 8c8ed4f6ef
13 changed files with 1086 additions and 534 deletions

1348
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,10 +20,16 @@ thiserror = "1.0"
rust-argon2 = "0.8"
log = "0.4"
rand = "0.7"
rocket = { version= "0.4.6", features = ["private-cookies"] }
futures = "0.3"
strum = { version = "0.20", features = ["derive"] }
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
branch = "master"
features = ["secrets"]
[dependencies.rocket_contrib]
version = "0.4.6"
git = "https://github.com/SergioBenitez/Rocket"
branch = "master"
default-features = false
features = [ "tera_templates", "diesel_sqlite_pool", "serve" ]

View File

@ -7,5 +7,5 @@ write_timeout = 5
log = "normal"
limits = { forms = 32768 }
[development.databases]
[global.databases]
tenebrous_db = { url = "./tenebrous.sqlite" }

View File

@ -1 +1 @@
nightly
stable

View File

@ -2,23 +2,27 @@ use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
use crate::models::users::{NewUser, User};
use crate::schema::characters;
use diesel::prelude::*;
use diesel::SqliteConnection;
use rocket_contrib::databases::diesel;
#[database("tenebrous_db")]
pub(crate) struct TenebrousDbConn(SqliteConnection);
#[rocket::async_trait]
pub(crate) trait Dao {
fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
async fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>>;
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>>;
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User>;
//async fn insert_user<'a>(&self, new_user: &'a NewUser<'a>) -> QueryResult<User>;
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User>;
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
//async fn insert_character<'a>(&self, new_character: NewCharacter<'a>) -> QueryResult<()>;
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
}
type StrippedCharacterColumns = (
@ -39,55 +43,67 @@ const STRIPPED_CHARACTER_COLUMNS: StrippedCharacterColumns = (
characters::data_version,
);
#[rocket::async_trait]
impl Dao for TenebrousDbConn {
fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
async fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
use crate::schema::users::dsl::*;
users.filter(id.eq(user_id)).first(&self.0).optional()
self.run(move |conn| users.filter(id.eq(user_id)).first(conn).optional())
.await
}
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>> {
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>> {
use crate::schema::users::dsl::*;
users
.filter(username.eq(for_username))
.first(&self.0)
.optional()
self.run(move |conn| {
users
.filter(username.eq(for_username))
.first(conn)
.optional()
})
.await
}
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User> {
use crate::schema::users;
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User> {
self.run(move |conn| {
diesel::insert_into(users).values(&new_user).execute(conn)?;
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)?)
use crate::schema::users::dsl::*;
users.filter(username.eq(new_user.username)).first(conn)
})
.await
}
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
use crate::schema::characters::dsl::*;
characters
.filter(user_id.eq(for_user_id))
.select(STRIPPED_CHARACTER_COLUMNS)
.load(&self.0)
}
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
use crate::schema::characters::dsl::*;
characters
.filter(id.eq(character_id))
.first(&self.0)
.optional()
self.run(move |conn| {
characters
.filter(user_id.eq(for_user_id))
.select(STRIPPED_CHARACTER_COLUMNS)
.load(conn)
})
.await
}
fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> {
diesel::insert_into(characters::table)
.values(new_character)
.execute(&self.0)?;
async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
use crate::schema::characters::dsl::*;
self.run(move |conn| {
characters
.filter(id.eq(character_id))
.first(conn)
.optional()
})
.await
}
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> {
self.run(|conn| {
diesel::insert_into(characters::table)
.values(new_character)
.execute(conn)
})
.await?;
Ok(())
}

View File

@ -27,10 +27,6 @@ pub enum Error {
DeserializationError(#[from] prost::DecodeError),
}
#[derive(Error, Debug)]
#[error("internal eror")]
pub struct SensitiveError(Error);
impl Error {
fn is_sensitive(&self) -> bool {
use Error::*;
@ -41,8 +37,9 @@ impl Error {
}
}
impl<'r> Responder<'r> for Error {
fn respond_to(self, req: &Request) -> response::Result<'r> {
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, req: &Request) -> response::Result<'static> {
log::error!("error: {0}", self.to_string());
//Hide sensitive error information
let message: String = if self.is_sensitive() {
@ -64,22 +61,3 @@ 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,5 +1,3 @@
#![feature(proc_macro_hygiene, decl_macro, never_type)]
#[macro_use]
extern crate rocket;
@ -24,7 +22,8 @@ pub mod models;
pub mod routes;
pub mod schema;
fn main() {
#[rocket::main]
async fn main() -> Result<(), rocket::error::Error> {
let root_routes: Vec<rocket::Route> = {
routes::root::routes()
.into_iter()
@ -45,5 +44,6 @@ fn main() {
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
)
.register(catchers)
.launch();
.launch()
.await
}

View File

@ -155,11 +155,11 @@ impl Visibility for StrippedCharacter {
/// names correspond to columns.
#[derive(Insertable)]
#[table_name = "characters"]
pub struct NewCharacter<'a> {
pub struct NewCharacter {
pub user_id: i32,
pub viewable: bool,
pub character_name: &'a str,
pub character_name: String,
pub data_type: CharacterDataType,
pub data_version: i32,
pub data: &'a [u8],
pub data: Vec<u8>,
}

View File

@ -25,24 +25,29 @@ impl User {
}
}
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn, id: i32) -> Option<User> {
db.load_user_by_id(id).await.ok().flatten()
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
type Error = !;
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()
};
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let db = try_outcome!(request.guard::<TenebrousDbConn>().await);
let user_id: Option<i32> = request
.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok());
request
.cookies()
.get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.and_then(attempt_load_user)
});
let user: &Option<User> = request
.local_cache_async(async {
match user_id {
Some(id) => attempt_load_user(&db, id).await,
_ => None,
}
})
.await;
user.as_ref().or_forward(())
}
@ -50,7 +55,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub username: &'a str,
pub password: &'a str,
pub struct NewUser {
pub username: String,
pub password: String,
}

View File

@ -1,7 +1,7 @@
use crate::db::{Dao, TenebrousDbConn};
use crate::models::users::{self, NewUser, User};
use log::error;
use rocket::http::{Cookie, Cookies};
use rocket::http::{Cookie, CookieJar};
use rocket::request::{FlashMessage, Form};
use rocket::response::{Flash, Redirect};
use rocket_contrib::templates::Template;
@ -30,11 +30,11 @@ struct Registration {
password: String,
}
fn add_login_cookie(cookies: &mut Cookies, user: &User) {
fn add_login_cookie(cookies: &CookieJar<'_>, user: &User) {
cookies.add_private(Cookie::new("user_id", user.id.to_string()));
}
fn remove_login_cookie(cookies: &mut Cookies) {
fn remove_login_cookie(cookies: &CookieJar<'_>) {
cookies.remove_private(Cookie::named("user_id"));
}
@ -47,13 +47,14 @@ fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
}
#[post("/login", data = "<login>")]
fn login(
mut cookies: Cookies,
async fn login(
cookies: &CookieJar<'_>,
login: Form<Login>,
conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> {
let user = conn
.load_user(&login.username)
.load_user(login.username.clone())
.await
.map_err(|e| {
error!("login - error loading user user: {}", e);
login_error_redirect("Internal error.")
@ -61,7 +62,7 @@ fn login(
.ok_or_else(|| login_error_redirect("Invalid username or password."))?;
if user.verify_password(&login.password) {
add_login_cookie(&mut cookies, &user);
add_login_cookie(cookies, &user);
Ok(Redirect::to(uri!(super::root::index)))
} else {
Err(login_error_redirect("Invalid username or password."))
@ -69,8 +70,8 @@ fn login(
}
#[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> {
remove_login_cookie(&mut cookies);
fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
remove_login_cookie(cookies);
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
}
@ -100,15 +101,18 @@ fn register_page(flash: Option<FlashMessage>) -> Template {
}
#[post("/register", data = "<registration>")]
fn register(
mut cookies: Cookies,
async fn register(
mut cookies: &CookieJar<'_>,
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.")
})?;
let existing_user = conn
.load_user(registration.username.clone())
.await
.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!(
@ -123,11 +127,11 @@ fn register(
})?;
let user = NewUser {
username: &registration.username,
password: &hashed_pw,
username: registration.username.clone(),
password: hashed_pw,
};
let user = conn.insert_user(&user).map_err(|e| {
let user = conn.insert_user(user).await.map_err(|e| {
error!("registration - could not insert user: {}", e);
registration_error_redirect("There was an error completing registration.")
})?;

View File

@ -45,16 +45,17 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
}
#[get("/<username>/<character_id>")]
fn view_character(
async fn view_character(
character_id: i32,
username: String,
conn: TenebrousDbConn,
logged_in_user: Option<&User>,
) -> Result<Template, Error> {
let user = &conn.load_user(&username)?.ok_or(Error::NotFound)?;
let user = &conn.load_user(username).await?.ok_or(Error::NotFound)?;
let character = conn
.load_character(character_id)?
.load_character(character_id)
.await?
.and_then(|c| c.as_visible_for(logged_in_user))
.ok_or(Error::NotFound)?;
@ -63,15 +64,19 @@ fn view_character(
}
#[get("/<owner>/<character_id>/edit")]
fn edit_character(
async fn edit_character(
character_id: i32,
owner: String,
logged_in_user: Option<&User>,
conn: TenebrousDbConn,
) -> Result<Template, Error> {
let owner = conn.load_user(&owner)?.ok_or(Error::NotFound)?;
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character = conn.load_character(character_id)?.ok_or(Error::NotFound)?;
let character = conn
.load_character(character_id)
.await?
.ok_or(Error::NotFound)?;
if logged_in_user != &owner {
return Err(Error::NoPermission);

View File

@ -5,7 +5,6 @@ use crate::models::{
convert::ValidationError,
users::User,
};
use prost::bytes::BytesMut;
use rocket::{request::Form, response::Redirect};
use rocket_contrib::templates::Template;
use strum::IntoEnumIterator;
@ -57,24 +56,24 @@ impl NewCharacterContext {
/// Create and insert a new character into the database. The form is
/// assumed to be successfully validated.
fn create_new_character(
async fn create_new_character(
form: &Form<NewCharacterForm>,
user_id: i32,
conn: TenebrousDbConn,
) -> Result<(), Error> {
let system: CharacterDataType = *form.system.as_ref().unwrap();
let sheet: BytesMut = system.create_data()?;
let sheet: Vec<u8> = system.create_data()?.to_vec();
let insert = NewCharacter {
user_id: user_id,
viewable: true,
character_name: &form.name,
character_name: form.name.clone(),
data_type: system,
data_version: 1,
data: &sheet,
data: sheet,
};
conn.insert_character(insert)?;
conn.insert_character(insert).await?;
Ok(())
}
@ -95,7 +94,7 @@ pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Err
}
#[post("/new", data = "<form>")]
pub(super) fn new_character_submit(
pub(super) async fn new_character_submit(
form: Form<NewCharacterForm>,
logged_in_user: &User,
conn: TenebrousDbConn,
@ -104,7 +103,7 @@ pub(super) fn new_character_submit(
return Err(render_error(&form, e.to_string().clone()));
}
match create_new_character(&form, logged_in_user.id, conn) {
match create_new_character(&form, logged_in_user.id, conn).await {
Ok(_) => Ok(crate::routes::common::redirect_to_index()),
Err(e) => Err(render_error(&form, e.to_string().clone())),
}

View File

@ -18,9 +18,10 @@ pub struct UserHomeContext<'a> {
}
#[get("/")]
fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
async fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
let characters: Vec<StrippedCharacter> = conn
.load_character_list(user.id)?
.load_character_list(user.id)
.await?
.into_iter()
.map(|c| c.as_visible_for(Some(user)))
.filter_map(|c| c)