Compare commits

..

No commits in common. "c99c7af369135dff689dcfaaee510782d5d0dcb4" and "cd4afbafe2e8d344ab3ef06a2ce7af4e86c52bdf" have entirely different histories.

28 changed files with 272 additions and 1083 deletions

3
.env
View File

@ -1,2 +1 @@
DATABASE_URL="sqlite://tenebrous.sqlite" DATABASE_URL="./tenebrous.sqlite"
SQLX_OFFLINE="true"

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
/target /target
todo.org todo.org
*.sqlite* *.sqlite
*.sqlite.*

699
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,15 +4,6 @@ version = "0.1.0"
authors = ["jeff <jeff@agnos.is>"] authors = ["jeff <jeff@agnos.is>"]
edition = "2018" edition = "2018"
build = "build.rs" build = "build.rs"
default-run = "tenebrous"
[[bin]]
name = "tenebrous-migrate"
path = "src/migrate.rs"
[[bin]]
name = "tenebrous"
path = "src/main.rs"
[build-dependencies] [build-dependencies]
prost-build = "0.6" prost-build = "0.6"
@ -23,15 +14,14 @@ serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
erased-serde = "0.3" erased-serde = "0.3"
diesel = "1.4"
diesel-derive-enum = { version = "1", features = ["sqlite"] }
thiserror = "1.0" thiserror = "1.0"
rust-argon2 = "0.8" rust-argon2 = "0.8"
log = "0.4" log = "0.4"
rand = "0.7" rand = "0.7"
futures = "0.3" futures = "0.3"
strum = { version = "0.20", features = ["derive"] } strum = { version = "0.20", features = ["derive"] }
sqlx = { version = "0.4.2", features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] }
refinery = { version = "0.3", features = ["rusqlite"]}
barrel = { version = "0.6", features = ["sqlite3"] }
[dependencies.rocket] [dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"
@ -42,4 +32,4 @@ features = ["secrets"]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"
branch = "master" branch = "master"
default-features = false default-features = false
features = [ "tera_templates", "serve" ] features = [ "tera_templates", "diesel_sqlite_pool", "serve" ]

View File

@ -7,7 +7,9 @@ Currently under heavy development.
## Build Instructions ## Build Instructions
These are very basic build instructions. They assume you already have These are very basic build instructions. They assume you already have
cargo set up and installed. cargo set up and installed. Building the application requires **Rust
Nightly!** See [rustup documentation][rustup] for more, particularly
the part about using [Rust Nightly][nightly].
### Install Dependencies ### Install Dependencies
@ -21,19 +23,18 @@ Install dependencies. The exact method depends on your OS.
### Initial Setup ### Initial Setup
Follow these instructions from the root of the repository. Follow these instructions from the root of the repository. Set up database:
Set up database:
``` ```
cargo install --version=0.2.0 sqlx-cli cargo install diesel_cli --no-default-features --features sqlite
cargo run --bin tenebrous-migrate diesel setup
diesel migration run
``` ```
### Run Application ### Run Application
If you are using `rustup`, then it should automatically switch to the If you are using `rustup`, then it should automatically switch to the
stable version of Rust in this repository. This is because of the nightly version of Rust in this repository. This is because of the
`rust-toolchain` file. `rust-toolchain` file.
Command line "instructions" to build and run the application: Command line "instructions" to build and run the application:
@ -42,27 +43,9 @@ Command line "instructions" to build and run the application:
cargo run cargo run
``` ```
The sqlite database is created in the directory `cargo run` was The sqlite database is currently always created in the same directory
invoked from by default. You can also pass a path to a different that `cargo run` was invoked from, so make sure you invoke it from the
location as a single argument to the program. same place every time.
## Development
Development instructions.
To set up a local database, or run migrations, run:
```
cargo run --bin tenebrous-migrate
```
### Database Queries and Migrations
When adding/updating a compile-checked query or a migration, you need
to update the SQLx data JSON file:
```
cargo sqlx prepare -- --bin tenebrous
```
[rustup]: https://rust-lang.github.io/rustup/index.html [rustup]: https://rust-lang.github.io/rustup/index.html
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust

0
migrations/.gitkeep Normal file
View File

View File

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

View File

@ -0,0 +1,9 @@
CREATE TABLE characters(
id INTEGER NOT NULL PRIMARY KEY,
user_id INTEGER NOT NULL,
viewable BOOLEAN NOT NULL,
character_name TEXT NOT NULL,
data_type TEXT CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1')) NOT NULL,
data_version INTEGER NOT NULL,
data BLOB NOT NULL
);

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,165 +0,0 @@
{
"db": "SQLite",
"492e1e087edc6eff4004033227e3f3510f1165c3ec7626e89b695c760bc113d2": {
"query": "SELECT id as \"id: _\",\n user_id as \"user_id: _\",\n data_type as \"data_type: _\",\n data_version as \"data_version: _\",\n viewable, character_name\n FROM characters WHERE user_id = ?",
"describe": {
"columns": [
{
"name": "id: _",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "user_id: _",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "data_type: _",
"ordinal": 2,
"type_info": "Null"
},
{
"name": "data_version: _",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "viewable",
"ordinal": 4,
"type_info": "Bool"
},
{
"name": "character_name",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"7a55ee917cf1ec732b10b2844dd5a3d0089c177b0eb183932fd73e20387a1610": {
"query": "SELECT id as \"id: _\", username, password FROM users WHERE username = ?",
"describe": {
"columns": [
{
"name": "id: _",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "password",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"d7d1fc9ceff3b7659c8f04fd5b574104d1866cb1ea67d9534b6f3f4064699fb6": {
"query": "SELECT id as \"id: _\", username, password FROM users WHERE id = ?",
"describe": {
"columns": [
{
"name": "id: _",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "password",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"e3c4b224ce8ce70a4d7709af7abe82a6f53bff834460fa7d32bb0b6daea94565": {
"query": "SELECT id as \"id: _\",\n user_id as \"user_id: _\",\n viewable, character_name, data,\n data_type as \"data_type: _\",\n data_version as \"data_version: _\"\n FROM characters WHERE id = ?",
"describe": {
"columns": [
{
"name": "id: _",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "user_id: _",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "viewable",
"ordinal": 2,
"type_info": "Bool"
},
{
"name": "character_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "data",
"ordinal": 4,
"type_info": "Blob"
},
{
"name": "data_type: _",
"ordinal": 5,
"type_info": "Null"
},
{
"name": "data_version: _",
"ordinal": 6,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
}
}
}

184
src/db.rs
View File

@ -1,137 +1,119 @@
use crate::models::characters::{Character, NewCharacter, StrippedCharacter}; use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
use crate::models::users::{NewUser, User}; use crate::models::users::{NewUser, User};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use crate::schema::characters;
use sqlx::ConnectOptions; use diesel::prelude::*;
use std::str::FromStr; use rocket_contrib::databases::diesel;
/// Type alias for the Rocket-managed singleton database connection. #[database("tenebrous_db")]
pub type TenebrousDbConn<'a> = rocket::State<'a, SqlitePool>; pub(crate) struct TenebrousDbConn(SqliteConnection);
/// Create a connection pool to the database.
pub(crate) async fn create_pool(db_path: &str) -> Result<SqlitePool, crate::errors::Error> {
//Create database if missing.
let conn = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))?
.create_if_missing(true)
.connect()
.await?;
drop(conn);
//Return actual conncetion pool.
SqlitePoolOptions::new()
.max_connections(5)
.connect(db_path)
.await
.map_err(|e| e.into())
}
#[rocket::async_trait] #[rocket::async_trait]
pub(crate) trait Dao { pub(crate) trait Dao {
async fn load_user_by_id(&self, id: i32) -> sqlx::Result<Option<User>>; async fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>>; async fn load_user(&self, for_username: String) -> QueryResult<Option<User>>;
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User>; async fn insert_user(&self, new_user: NewUser) -> QueryResult<User>;
async fn load_character_list(&self, for_user_id: i32) -> sqlx::Result<Vec<StrippedCharacter>>; async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>>; async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()>; async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()>; async fn update_character_sheet(&self, character: Character) -> QueryResult<()>;
} }
//TODO is: type StrippedCharacterColumns = (
// - use compile time queries characters::id,
// - find replacement for diesel migrations characters::user_id,
characters::viewable,
characters::character_name,
characters::data_type,
characters::data_version,
);
const STRIPPED_CHARACTER_COLUMNS: StrippedCharacterColumns = (
characters::id,
characters::user_id,
characters::viewable,
characters::character_name,
characters::data_type,
characters::data_version,
);
#[rocket::async_trait] #[rocket::async_trait]
impl Dao for SqlitePool { impl Dao for TenebrousDbConn {
async fn load_user_by_id(&self, user_id: i32) -> sqlx::Result<Option<User>> { async fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
sqlx::query_as!( use crate::schema::users::dsl::*;
User, self.run(move |conn| users.filter(id.eq(user_id)).first(conn).optional())
r#"SELECT id as "id: _", username, password FROM users WHERE id = ?"#,
user_id
)
.fetch_optional(self)
.await .await
} }
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>> { async fn load_user(&self, for_username: String) -> QueryResult<Option<User>> {
sqlx::query_as!( use crate::schema::users::dsl::*;
User,
r#"SELECT id as "id: _", username, password FROM users WHERE username = ?"#, self.run(move |conn| {
for_username users
) .filter(username.eq(for_username))
.fetch_optional(self) .first(conn)
.optional()
})
.await .await
} }
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User> { async fn insert_user(&self, new_user: NewUser) -> QueryResult<User> {
sqlx::query("INSERT INTO users (username, password) values (?, ?)") self.run(move |conn| {
.bind(new_user.username) diesel::insert_into(users).values(&new_user).execute(conn)?;
.bind(new_user.password)
.execute(self)
.await?;
self.load_user(new_user.username) use crate::schema::users::dsl::*;
.await users.filter(username.eq(new_user.username)).first(conn)
.and_then(|user| user.ok_or(sqlx::Error::RowNotFound)) })
}
async fn load_character_list(&self, for_user_id: i32) -> sqlx::Result<Vec<StrippedCharacter>> {
sqlx::query_as!(
StrippedCharacter,
r#"SELECT id as "id: _",
user_id as "user_id: _",
data_type as "data_type: _",
data_version as "data_version: _",
viewable, character_name
FROM characters WHERE user_id = ?"#,
for_user_id
)
.fetch_all(self)
.await .await
} }
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>> { async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
sqlx::query_as!( use crate::schema::characters::dsl::*;
Character,
r#"SELECT id as "id: _", self.run(move |conn| {
user_id as "user_id: _", characters
viewable, character_name, data, .filter(user_id.eq(for_user_id))
data_type as "data_type: _", .select(STRIPPED_CHARACTER_COLUMNS)
data_version as "data_version: _" .load(conn)
FROM characters WHERE id = ?"#, })
character_id
)
.fetch_optional(self)
.await .await
} }
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()> { async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
sqlx::query( use crate::schema::characters::dsl::*;
"INSERT INTO characters
(user_id, viewable, character_name, data_type, data_version, data) self.run(move |conn| {
values (?, ?, ?, ?, ?, ?)", characters
) .filter(id.eq(character_id))
.bind(new_character.user_id) .first(conn)
.bind(new_character.viewable) .optional()
.bind(new_character.character_name) })
.bind(new_character.data_type) .await
.bind(new_character.data_version) }
.bind(new_character.data)
.execute(self) async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> {
self.run(|conn| {
diesel::insert_into(characters::table)
.values(new_character)
.execute(conn)
})
.await?; .await?;
Ok(()) Ok(())
} }
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()> { async fn update_character_sheet(&self, character: Character) -> QueryResult<()> {
sqlx::query("UPDATE characters set data = ? where id = ?") use crate::schema::characters::dsl::*;
.bind(&character.data) self.run(move |conn| {
.bind(character.id) diesel::update(&character)
.execute(self) .set(data.eq(&character.data))
.execute(conn)
})
.await?; .await?;
Ok(()) Ok(())

View File

@ -20,6 +20,9 @@ pub enum Error {
#[error("invalid input")] #[error("invalid input")]
InvalidInput, InvalidInput,
#[error("query error: {0}")]
QueryError(#[from] diesel::result::Error),
#[error("serialization error: {0}")] #[error("serialization error: {0}")]
SerializationError(#[from] prost::EncodeError), SerializationError(#[from] prost::EncodeError),
@ -28,12 +31,6 @@ pub enum Error {
#[error("i/o error: {0}")] #[error("i/o error: {0}")]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("query error: {0}")]
QueryError(#[from] sqlx::Error),
#[error("rocket error: {0}")]
RocketError(#[from] rocket::error::Error),
} }
impl Error { impl Error {

View File

@ -4,6 +4,9 @@ extern crate rocket;
#[macro_use] #[macro_use]
extern crate rocket_contrib; extern crate rocket_contrib;
#[macro_use]
extern crate diesel;
// Seemingly necessary to get serde::Serialize into scope for Prost // Seemingly necessary to get serde::Serialize into scope for Prost
// code generation. // code generation.
#[macro_use] #[macro_use]
@ -11,28 +14,16 @@ extern crate serde_derive;
use rocket_contrib::serve::StaticFiles; use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::env;
pub mod catchers; pub mod catchers;
pub mod db; pub mod db;
pub mod errors; pub mod errors;
pub mod migrator;
pub mod models; pub mod models;
pub mod routes; pub mod routes;
pub mod schema;
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), rocket::error::Error> {
let args: Vec<String> = env::args().collect();
let db_path: &str = match &args[..] {
[_, path] => path.as_ref(),
[_, _, ..] => panic!("Expected exactly 0 or 1 argument"),
_ => "tenebrous.sqlite",
};
println!("Using database: {}", db_path);
migrator::migrate(db_path).await?;
let root_routes: Vec<rocket::Route> = { let root_routes: Vec<rocket::Route> = {
routes::root::routes() routes::root::routes()
.into_iter() .into_iter()
@ -46,8 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
rocket::ignite() rocket::ignite()
.attach(Template::fairing()) .attach(Template::fairing())
//.attach(db::TenebrousDbConn::fairing()) .attach(db::TenebrousDbConn::fairing())
.manage(crate::db::create_pool(db_path).await?)
.mount("/", root_routes) .mount("/", root_routes)
.mount("/characters", character_routes) .mount("/characters", character_routes)
.mount("/api", api_routes) .mount("/api", api_routes)
@ -62,5 +52,4 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.register(catchers) .register(catchers)
.launch() .launch()
.await .await
.map_err(|e| e.into())
} }

View File

@ -1,17 +0,0 @@
use std::env;
pub mod migrator;
#[rocket::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
let db_path: &str = match &args[..] {
[_, path] => path.as_ref(),
[_, _, ..] => panic!("Expected exactly 0 or 1 argument"),
_ => "tenebrous.sqlite",
};
println!("Using database: {}", db_path);
crate::migrator::migrate(db_path).await
}

View File

@ -1,22 +0,0 @@
use refinery::config::{Config, ConfigDbType};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::ConnectOptions;
use std::str::FromStr;
pub mod migrations;
/// Run database migrations against the sqlite database.
pub(crate) async fn migrate(db_path: &str) -> Result<(), Box<dyn std::error::Error>> {
//Create database if missing.
let conn = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))?
.create_if_missing(true)
.connect()
.await?;
drop(conn);
let mut conn = Config::new(ConfigDbType::Sqlite).set_db_path(db_path);
println!("Running migrations");
migrations::runner().run(&mut conn)?;
Ok(())
}

View File

@ -1,15 +0,0 @@
use barrel::backend::Sqlite;
use barrel::{types, Migration};
pub fn migration() -> String {
let mut m = Migration::new();
println!("Applying migration: {}", file!());
m.create_table("users", |t| {
t.add_column("id", types::primary());
t.add_column("username", types::text());
t.add_column("password", types::text());
});
m.make::<Sqlite>()
}

View File

@ -1,20 +0,0 @@
use barrel::backend::Sqlite;
use barrel::{types, Migration};
pub fn migration() -> String {
let mut m = Migration::new();
println!("Applying migration: {}", file!());
m.create_table("characters", move |t| {
let db_enum = r#"CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1'))"#;
t.add_column("id", types::primary());
t.add_column("user_id", types::integer());
t.add_column("viewable", types::boolean());
t.add_column("character_name", types::text());
t.add_column("data_type", types::custom(db_enum));
t.add_column("data_version", types::integer());
t.add_column("data", types::custom("BLOB"));
});
m.make::<Sqlite>()
}

View File

@ -1,2 +0,0 @@
use refinery::include_migration_mods;
include_migration_mods!("src/migrator/migrations");

View File

@ -1,6 +1,8 @@
use crate::errors::Error; use crate::errors::Error;
use crate::models::proto::cofd::*; use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use crate::schema::characters;
use diesel_derive_enum::DbEnum;
use prost::bytes::BytesMut; use prost::bytes::BytesMut;
use serde_derive::Serialize; use serde_derive::Serialize;
use strum::{EnumIter, EnumString}; use strum::{EnumIter, EnumString};
@ -39,8 +41,7 @@ pub(crate) trait Visibility {
} }
} }
#[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)] #[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString)]
#[sqlx(rename_all = "snake_case")]
pub enum CharacterDataType { pub enum CharacterDataType {
ChroniclesOfDarknessV1, ChroniclesOfDarknessV1,
ChangelingV1, ChangelingV1,
@ -72,7 +73,7 @@ impl CharacterDataType {
/// An entry that appears in a user's character list. Properties are /// An entry that appears in a user's character list. Properties are
/// in order of table columns. /// in order of table columns.
#[derive(Serialize, Debug, sqlx::FromRow)] #[derive(Serialize, Debug, Queryable, Identifiable, AsChangeset)]
pub struct Character { pub struct Character {
pub id: i32, pub id: i32,
pub user_id: i32, pub user_id: i32,
@ -144,7 +145,7 @@ impl Character {
/// Same as regular character type, but without the actual protobuf /// Same as regular character type, but without the actual protobuf
/// data loaded into memory. /// data loaded into memory.
#[derive(Serialize, Debug, sqlx::FromRow)] #[derive(Serialize, Debug, Queryable)]
pub struct StrippedCharacter { pub struct StrippedCharacter {
pub id: i32, pub id: i32,
pub user_id: i32, pub user_id: i32,
@ -166,11 +167,13 @@ impl Visibility for StrippedCharacter {
/// Represents insert of a new character into the database. Property /// Represents insert of a new character into the database. Property
/// names correspond to columns. /// names correspond to columns.
pub struct NewCharacter<'a> { #[derive(Insertable)]
#[table_name = "characters"]
pub struct NewCharacter {
pub user_id: i32, pub user_id: i32,
pub viewable: bool, pub viewable: bool,
pub character_name: &'a str, pub character_name: String,
pub data_type: CharacterDataType, pub data_type: CharacterDataType,
pub data_version: i32, pub data_version: i32,
pub data: &'a [u8], pub data: Vec<u8>,
} }

View File

@ -1,4 +1,5 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::schema::users;
use argon2::{self, Config, Error as ArgonError}; use argon2::{self, Config, Error as ArgonError};
use rand::Rng; use rand::Rng;
use rocket::outcome::IntoOutcome; use rocket::outcome::IntoOutcome;
@ -11,7 +12,7 @@ pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config) argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
} }
#[derive(Eq, PartialEq, Serialize, Debug, sqlx::FromRow)] #[derive(Eq, PartialEq, Serialize, Debug, Queryable)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub username: String, pub username: String,
@ -24,11 +25,10 @@ impl User {
} }
} }
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn<'a>, id: i32) -> Option<User> { async fn attempt_load_user<'a>(db: &'a TenebrousDbConn, id: i32) -> Option<User> {
db.load_user_by_id(id).await.ok().flatten() db.load_user_by_id(id).await.ok().flatten()
} }
/// Trait implementation to get the logged in user.
#[rocket::async_trait] #[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for &'a User { impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
type Error = (); type Error = ();
@ -53,7 +53,9 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
} }
} }
pub struct NewUser<'a> { #[derive(Insertable)]
pub username: &'a str, #[table_name = "users"]
pub password: &'a str, pub struct NewUser {
pub username: String,
pub password: String,
} }

View File

@ -24,13 +24,13 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
/// logged in, the owner of the character is not found, or the logged /// logged in, the owner of the character is not found, or the logged
/// in user does not have the permission to access this character. /// in user does not have the permission to access this character.
async fn load_character( async fn load_character(
conn: &TenebrousDbConn<'_>, conn: &TenebrousDbConn,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
owner: String, owner: String,
character_id: i32, character_id: i32,
) -> Result<Character, Error> { ) -> Result<Character, Error> {
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?; let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let owner = conn.load_user(&owner).await?.ok_or(Error::NotFound)?; let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
let character: Character = conn let character: Character = conn
.load_character(character_id) .load_character(character_id)
.await? .await?
@ -71,7 +71,7 @@ mod cofd {
owner: String, owner: String,
character_id: i32, character_id: i32,
attr_update: Proto<Attribute>, attr_update: Proto<Attribute>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<&'a str, Error> { ) -> Result<&'a str, Error> {
let mut character = load_character(&conn, logged_in_user, owner, character_id).await?; let mut character = load_character(&conn, logged_in_user, owner, character_id).await?;
@ -96,7 +96,7 @@ mod cofd {
); );
character.update_data(sheet)?; character.update_data(sheet)?;
conn.update_character_sheet(&character).await?; conn.update_character_sheet(character).await?;
Ok("lol") Ok("lol")
} }
@ -105,7 +105,7 @@ mod cofd {
owner: String, owner: String,
character_id: i32, character_id: i32,
info: Proto<Skills>, info: Proto<Skills>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> &'a str { ) -> &'a str {
"lol" "lol"
} }

View File

@ -50,10 +50,10 @@ fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
async fn login( async fn login(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
login: Form<Login>, login: Form<Login>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> { ) -> Result<Redirect, Flash<Redirect>> {
let user = conn let user = conn
.load_user(&login.username) .load_user(login.username.clone())
.await .await
.map_err(|e| { .map_err(|e| {
error!("login - error loading user user: {}", e); error!("login - error loading user user: {}", e);
@ -104,9 +104,12 @@ fn register_page(flash: Option<FlashMessage>) -> Template {
async fn register( async fn register(
mut cookies: &CookieJar<'_>, mut cookies: &CookieJar<'_>,
registration: Form<Registration>, registration: Form<Registration>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> { ) -> Result<Redirect, Flash<Redirect>> {
let existing_user = conn.load_user(&registration.username).await.map_err(|e| { let existing_user = conn
.load_user(registration.username.clone())
.await
.map_err(|e| {
error!("registration - error loading existing user: {}", e); error!("registration - error loading existing user: {}", e);
registration_error_redirect("There was an error attempting to register.") registration_error_redirect("There was an error attempting to register.")
})?; })?;
@ -124,8 +127,8 @@ async fn register(
})?; })?;
let user = NewUser { let user = NewUser {
username: &registration.username, username: registration.username.clone(),
password: &hashed_pw, password: hashed_pw,
}; };
let user = conn.insert_user(user).await.map_err(|e| { let user = conn.insert_user(user).await.map_err(|e| {

View File

@ -50,10 +50,10 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
async fn view_character( async fn view_character(
character_id: i32, character_id: i32,
username: String, username: String,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let user = &conn.load_user(&username).await?.ok_or(Error::NotFound)?; let user = &conn.load_user(username).await?.ok_or(Error::NotFound)?;
let character = conn let character = conn
.load_character(character_id) .load_character(character_id)

View File

@ -25,9 +25,9 @@ pub(super) async fn edit_character_page(
character_id: i32, character_id: i32,
owner: String, owner: String,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let owner = conn.load_user(&owner).await?.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 logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character = conn let character = conn

View File

@ -59,7 +59,7 @@ impl NewCharacterContext {
async fn create_new_character( async fn create_new_character(
form: &Form<NewCharacterForm>, form: &Form<NewCharacterForm>,
user_id: i32, user_id: i32,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> Result<(), Error> { ) -> Result<(), Error> {
let system: CharacterDataType = *form.system.as_ref().unwrap(); let system: CharacterDataType = *form.system.as_ref().unwrap();
let sheet: Vec<u8> = system.create_data()?.to_vec(); let sheet: Vec<u8> = system.create_data()?.to_vec();
@ -67,10 +67,10 @@ async fn create_new_character(
let insert = NewCharacter { let insert = NewCharacter {
user_id: user_id, user_id: user_id,
viewable: true, viewable: true,
character_name: &form.name, character_name: form.name.clone(),
data_type: system, data_type: system,
data_version: 1, data_version: 1,
data: &sheet, data: sheet,
}; };
conn.insert_character(insert).await?; conn.insert_character(insert).await?;
@ -97,7 +97,7 @@ pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Err
pub(super) async fn new_character_submit( pub(super) async fn new_character_submit(
form: Form<NewCharacterForm>, form: Form<NewCharacterForm>,
logged_in_user: &User, logged_in_user: &User,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn,
) -> Result<Redirect, Template> { ) -> Result<Redirect, Template> {
if let Err(e) = &form.system { if let Err(e) = &form.system {
return Err(render_error(&form, e.to_string().clone())); return Err(render_error(&form, e.to_string().clone()));

View File

@ -18,7 +18,7 @@ pub struct UserHomeContext<'a> {
} }
#[get("/")] #[get("/")]
async 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 let characters: Vec<StrippedCharacter> = conn
.load_character_list(user.id) .load_character_list(user.id)
.await? .await?

30
src/schema.rs Normal file
View File

@ -0,0 +1,30 @@
table! {
use diesel::sql_types::*;
use crate::models::characters::*;
characters (id) {
id -> Integer,
user_id -> Integer,
viewable -> Bool,
character_name -> Text,
data_type -> CharacterDataTypeMapping,
data_version -> Integer,
data -> Binary,
}
}
table! {
use diesel::sql_types::*;
use crate::models::characters::*;
users (id) {
id -> Integer,
username -> Text,
password -> Text,
}
}
allow_tables_to_appear_in_same_query!(
characters,
users,
);