Compare commits
8 Commits
cd4afbafe2
...
c99c7af369
Author | SHA1 | Date |
---|---|---|
jeff | c99c7af369 | |
jeff | 574ab543e5 | |
jeff | f572a9c493 | |
jeff | 717ab0677b | |
jeff | ed9b7c1a81 | |
jeff | b443f28a50 | |
jeff | 769512f357 | |
jeff | 7f4efb8122 |
3
.env
3
.env
|
@ -1 +1,2 @@
|
|||
DATABASE_URL="./tenebrous.sqlite"
|
||||
DATABASE_URL="sqlite://tenebrous.sqlite"
|
||||
SQLX_OFFLINE="true"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
todo.org
|
||||
*.sqlite
|
||||
*.sqlite*
|
||||
*.sqlite.*
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
|
@ -4,6 +4,15 @@ version = "0.1.0"
|
|||
authors = ["jeff <jeff@agnos.is>"]
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
default-run = "tenebrous"
|
||||
|
||||
[[bin]]
|
||||
name = "tenebrous-migrate"
|
||||
path = "src/migrate.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tenebrous"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.6"
|
||||
|
@ -14,14 +23,15 @@ serde = "1.0"
|
|||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
erased-serde = "0.3"
|
||||
diesel = "1.4"
|
||||
diesel-derive-enum = { version = "1", features = ["sqlite"] }
|
||||
thiserror = "1.0"
|
||||
rust-argon2 = "0.8"
|
||||
log = "0.4"
|
||||
rand = "0.7"
|
||||
futures = "0.3"
|
||||
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]
|
||||
git = "https://github.com/SergioBenitez/Rocket"
|
||||
|
@ -32,4 +42,4 @@ features = ["secrets"]
|
|||
git = "https://github.com/SergioBenitez/Rocket"
|
||||
branch = "master"
|
||||
default-features = false
|
||||
features = [ "tera_templates", "diesel_sqlite_pool", "serve" ]
|
||||
features = [ "tera_templates", "serve" ]
|
41
README.md
41
README.md
|
@ -7,9 +7,7 @@ Currently under heavy development.
|
|||
## Build Instructions
|
||||
|
||||
These are very basic build instructions. They assume you already have
|
||||
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].
|
||||
cargo set up and installed.
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
|
@ -23,18 +21,19 @@ Install dependencies. The exact method depends on your OS.
|
|||
|
||||
### Initial Setup
|
||||
|
||||
Follow these instructions from the root of the repository. Set up database:
|
||||
Follow these instructions from the root of the repository.
|
||||
|
||||
Set up database:
|
||||
|
||||
```
|
||||
cargo install diesel_cli --no-default-features --features sqlite
|
||||
diesel setup
|
||||
diesel migration run
|
||||
cargo install --version=0.2.0 sqlx-cli
|
||||
cargo run --bin tenebrous-migrate
|
||||
```
|
||||
|
||||
### Run Application
|
||||
|
||||
If you are using `rustup`, then it should automatically switch to the
|
||||
nightly version of Rust in this repository. This is because of the
|
||||
stable version of Rust in this repository. This is because of the
|
||||
`rust-toolchain` file.
|
||||
|
||||
Command line "instructions" to build and run the application:
|
||||
|
@ -43,9 +42,27 @@ Command line "instructions" to build and run the application:
|
|||
cargo run
|
||||
```
|
||||
|
||||
The sqlite database is currently always created in the same directory
|
||||
that `cargo run` was invoked from, so make sure you invoke it from the
|
||||
same place every time.
|
||||
The sqlite database is created in the directory `cargo run` was
|
||||
invoked from by default. You can also pass a path to a different
|
||||
location as a single argument to the program.
|
||||
|
||||
## 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
|
||||
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE characters;
|
|
@ -1,9 +0,0 @@
|
|||
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
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE users;
|
|
@ -1,5 +0,0 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
194
src/db.rs
194
src/db.rs
|
@ -1,120 +1,138 @@
|
|||
use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
|
||||
use crate::models::users::{NewUser, User};
|
||||
use crate::schema::characters;
|
||||
use diesel::prelude::*;
|
||||
use rocket_contrib::databases::diesel;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use sqlx::ConnectOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[database("tenebrous_db")]
|
||||
pub(crate) struct TenebrousDbConn(SqliteConnection);
|
||||
/// Type alias for the Rocket-managed singleton database connection.
|
||||
pub type TenebrousDbConn<'a> = rocket::State<'a, SqlitePool>;
|
||||
|
||||
/// 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]
|
||||
pub(crate) trait Dao {
|
||||
async fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
|
||||
async fn load_user_by_id(&self, id: i32) -> sqlx::Result<Option<User>>;
|
||||
|
||||
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>>;
|
||||
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>>;
|
||||
|
||||
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User>;
|
||||
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User>;
|
||||
|
||||
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
|
||||
async fn load_character_list(&self, for_user_id: i32) -> sqlx::Result<Vec<StrippedCharacter>>;
|
||||
|
||||
async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
|
||||
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>>;
|
||||
|
||||
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
|
||||
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()>;
|
||||
|
||||
async fn update_character_sheet(&self, character: Character) -> QueryResult<()>;
|
||||
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()>;
|
||||
}
|
||||
|
||||
type StrippedCharacterColumns = (
|
||||
characters::id,
|
||||
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,
|
||||
);
|
||||
|
||||
//TODO is:
|
||||
// - use compile time queries
|
||||
// - find replacement for diesel migrations
|
||||
#[rocket::async_trait]
|
||||
impl Dao for TenebrousDbConn {
|
||||
async fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
self.run(move |conn| users.filter(id.eq(user_id)).first(conn).optional())
|
||||
impl Dao for SqlitePool {
|
||||
async fn load_user_by_id(&self, user_id: i32) -> sqlx::Result<Option<User>> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
r#"SELECT id as "id: _", username, password FROM users WHERE id = ?"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(self)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_user(&self, for_username: &str) -> sqlx::Result<Option<User>> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
r#"SELECT id as "id: _", username, password FROM users WHERE username = ?"#,
|
||||
for_username
|
||||
)
|
||||
.fetch_optional(self)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn insert_user(&self, new_user: NewUser<'_>) -> sqlx::Result<User> {
|
||||
sqlx::query("INSERT INTO users (username, password) values (?, ?)")
|
||||
.bind(new_user.username)
|
||||
.bind(new_user.password)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
self.load_user(new_user.username)
|
||||
.await
|
||||
.and_then(|user| user.ok_or(sqlx::Error::RowNotFound))
|
||||
}
|
||||
|
||||
async fn load_user(&self, for_username: String) -> QueryResult<Option<User>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
|
||||
self.run(move |conn| {
|
||||
users
|
||||
.filter(username.eq(for_username))
|
||||
.first(conn)
|
||||
.optional()
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
async fn insert_user(&self, new_user: NewUser) -> QueryResult<User> {
|
||||
self.run(move |conn| {
|
||||
diesel::insert_into(users).values(&new_user).execute(conn)?;
|
||||
|
||||
use crate::schema::users::dsl::*;
|
||||
users.filter(username.eq(new_user.username)).first(conn)
|
||||
})
|
||||
async fn load_character(&self, character_id: i32) -> sqlx::Result<Option<Character>> {
|
||||
sqlx::query_as!(
|
||||
Character,
|
||||
r#"SELECT id as "id: _",
|
||||
user_id as "user_id: _",
|
||||
viewable, character_name, data,
|
||||
data_type as "data_type: _",
|
||||
data_version as "data_version: _"
|
||||
FROM characters WHERE id = ?"#,
|
||||
character_id
|
||||
)
|
||||
.fetch_optional(self)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
|
||||
use crate::schema::characters::dsl::*;
|
||||
|
||||
self.run(move |conn| {
|
||||
characters
|
||||
.filter(user_id.eq(for_user_id))
|
||||
.select(STRIPPED_CHARACTER_COLUMNS)
|
||||
.load(conn)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO characters
|
||||
(user_id, viewable, character_name, data_type, data_version, data)
|
||||
values (?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(new_character.user_id)
|
||||
.bind(new_character.viewable)
|
||||
.bind(new_character.character_name)
|
||||
.bind(new_character.data_type)
|
||||
.bind(new_character.data_version)
|
||||
.bind(new_character.data)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_character_sheet(&self, character: Character) -> QueryResult<()> {
|
||||
use crate::schema::characters::dsl::*;
|
||||
self.run(move |conn| {
|
||||
diesel::update(&character)
|
||||
.set(data.eq(&character.data))
|
||||
.execute(conn)
|
||||
})
|
||||
.await?;
|
||||
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()> {
|
||||
sqlx::query("UPDATE characters set data = ? where id = ?")
|
||||
.bind(&character.data)
|
||||
.bind(character.id)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ pub enum Error {
|
|||
#[error("invalid input")]
|
||||
InvalidInput,
|
||||
|
||||
#[error("query error: {0}")]
|
||||
QueryError(#[from] diesel::result::Error),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
SerializationError(#[from] prost::EncodeError),
|
||||
|
||||
|
@ -31,6 +28,12 @@ pub enum Error {
|
|||
|
||||
#[error("i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("query error: {0}")]
|
||||
QueryError(#[from] sqlx::Error),
|
||||
|
||||
#[error("rocket error: {0}")]
|
||||
RocketError(#[from] rocket::error::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -4,9 +4,6 @@ extern crate rocket;
|
|||
#[macro_use]
|
||||
extern crate rocket_contrib;
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
// Seemingly necessary to get serde::Serialize into scope for Prost
|
||||
// code generation.
|
||||
#[macro_use]
|
||||
|
@ -14,16 +11,28 @@ extern crate serde_derive;
|
|||
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
use std::env;
|
||||
|
||||
pub mod catchers;
|
||||
pub mod db;
|
||||
pub mod errors;
|
||||
pub mod migrator;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
|
||||
#[rocket::main]
|
||||
async fn main() -> Result<(), rocket::error::Error> {
|
||||
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);
|
||||
|
||||
migrator::migrate(db_path).await?;
|
||||
|
||||
let root_routes: Vec<rocket::Route> = {
|
||||
routes::root::routes()
|
||||
.into_iter()
|
||||
|
@ -37,7 +46,8 @@ async fn main() -> Result<(), rocket::error::Error> {
|
|||
|
||||
rocket::ignite()
|
||||
.attach(Template::fairing())
|
||||
.attach(db::TenebrousDbConn::fairing())
|
||||
//.attach(db::TenebrousDbConn::fairing())
|
||||
.manage(crate::db::create_pool(db_path).await?)
|
||||
.mount("/", root_routes)
|
||||
.mount("/characters", character_routes)
|
||||
.mount("/api", api_routes)
|
||||
|
@ -52,4 +62,5 @@ async fn main() -> Result<(), rocket::error::Error> {
|
|||
.register(catchers)
|
||||
.launch()
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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(())
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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>()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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>()
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
use refinery::include_migration_mods;
|
||||
include_migration_mods!("src/migrator/migrations");
|
|
@ -1,8 +1,6 @@
|
|||
use crate::errors::Error;
|
||||
use crate::models::proto::cofd::*;
|
||||
use crate::models::users::User;
|
||||
use crate::schema::characters;
|
||||
use diesel_derive_enum::DbEnum;
|
||||
use prost::bytes::BytesMut;
|
||||
use serde_derive::Serialize;
|
||||
use strum::{EnumIter, EnumString};
|
||||
|
@ -41,7 +39,8 @@ pub(crate) trait Visibility {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString)]
|
||||
#[derive(Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString, sqlx::Type)]
|
||||
#[sqlx(rename_all = "snake_case")]
|
||||
pub enum CharacterDataType {
|
||||
ChroniclesOfDarknessV1,
|
||||
ChangelingV1,
|
||||
|
@ -73,7 +72,7 @@ impl CharacterDataType {
|
|||
|
||||
/// An entry that appears in a user's character list. Properties are
|
||||
/// in order of table columns.
|
||||
#[derive(Serialize, Debug, Queryable, Identifiable, AsChangeset)]
|
||||
#[derive(Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct Character {
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
|
@ -145,7 +144,7 @@ impl Character {
|
|||
|
||||
/// Same as regular character type, but without the actual protobuf
|
||||
/// data loaded into memory.
|
||||
#[derive(Serialize, Debug, Queryable)]
|
||||
#[derive(Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct StrippedCharacter {
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
|
@ -167,13 +166,11 @@ impl Visibility for StrippedCharacter {
|
|||
|
||||
/// Represents insert of a new character into the database. Property
|
||||
/// names correspond to columns.
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "characters"]
|
||||
pub struct NewCharacter {
|
||||
pub struct NewCharacter<'a> {
|
||||
pub user_id: i32,
|
||||
pub viewable: bool,
|
||||
pub character_name: String,
|
||||
pub character_name: &'a str,
|
||||
pub data_type: CharacterDataType,
|
||||
pub data_version: i32,
|
||||
pub data: Vec<u8>,
|
||||
pub data: &'a [u8],
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::db::{Dao, TenebrousDbConn};
|
||||
use crate::schema::users;
|
||||
use argon2::{self, Config, Error as ArgonError};
|
||||
use rand::Rng;
|
||||
use rocket::outcome::IntoOutcome;
|
||||
|
@ -12,7 +11,7 @@ pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
|||
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Serialize, Debug, Queryable)]
|
||||
#[derive(Eq, PartialEq, Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
|
@ -25,10 +24,11 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn, id: i32) -> Option<User> {
|
||||
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn<'a>, id: i32) -> Option<User> {
|
||||
db.load_user_by_id(id).await.ok().flatten()
|
||||
}
|
||||
|
||||
/// Trait implementation to get the logged in user.
|
||||
#[rocket::async_trait]
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
|
||||
type Error = ();
|
||||
|
@ -53,9 +53,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "users"]
|
||||
pub struct NewUser {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub struct NewUser<'a> {
|
||||
pub username: &'a str,
|
||||
pub password: &'a str,
|
||||
}
|
||||
|
|
|
@ -24,13 +24,13 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
|
|||
/// logged in, the owner of the character is not found, or the logged
|
||||
/// in user does not have the permission to access this character.
|
||||
async fn load_character(
|
||||
conn: &TenebrousDbConn,
|
||||
conn: &TenebrousDbConn<'_>,
|
||||
logged_in_user: Option<&User>,
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
) -> Result<Character, Error> {
|
||||
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
|
||||
.load_character(character_id)
|
||||
.await?
|
||||
|
@ -71,7 +71,7 @@ mod cofd {
|
|||
owner: String,
|
||||
character_id: i32,
|
||||
attr_update: Proto<Attribute>,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
logged_in_user: Option<&User>,
|
||||
) -> Result<&'a str, Error> {
|
||||
let mut character = load_character(&conn, logged_in_user, owner, character_id).await?;
|
||||
|
@ -96,7 +96,7 @@ mod cofd {
|
|||
);
|
||||
|
||||
character.update_data(sheet)?;
|
||||
conn.update_character_sheet(character).await?;
|
||||
conn.update_character_sheet(&character).await?;
|
||||
Ok("lol")
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ mod cofd {
|
|||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<Skills>,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
|
|
|
@ -50,10 +50,10 @@ fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
|
|||
async fn login(
|
||||
cookies: &CookieJar<'_>,
|
||||
login: Form<Login>,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> Result<Redirect, Flash<Redirect>> {
|
||||
let user = conn
|
||||
.load_user(login.username.clone())
|
||||
.load_user(&login.username)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("login - error loading user user: {}", e);
|
||||
|
@ -104,15 +104,12 @@ fn register_page(flash: Option<FlashMessage>) -> Template {
|
|||
async fn register(
|
||||
mut cookies: &CookieJar<'_>,
|
||||
registration: Form<Registration>,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> Result<Redirect, Flash<Redirect>> {
|
||||
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.")
|
||||
})?;
|
||||
let existing_user = conn.load_user(®istration.username).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!(
|
||||
|
@ -127,8 +124,8 @@ async fn register(
|
|||
})?;
|
||||
|
||||
let user = NewUser {
|
||||
username: registration.username.clone(),
|
||||
password: hashed_pw,
|
||||
username: ®istration.username,
|
||||
password: &hashed_pw,
|
||||
};
|
||||
|
||||
let user = conn.insert_user(user).await.map_err(|e| {
|
||||
|
|
|
@ -50,10 +50,10 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
|
|||
async fn view_character(
|
||||
character_id: i32,
|
||||
username: String,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
logged_in_user: Option<&User>,
|
||||
) -> 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
|
||||
.load_character(character_id)
|
||||
|
|
|
@ -25,9 +25,9 @@ pub(super) async fn edit_character_page(
|
|||
character_id: i32,
|
||||
owner: String,
|
||||
logged_in_user: Option<&User>,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> 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 character = conn
|
||||
|
|
|
@ -59,7 +59,7 @@ impl NewCharacterContext {
|
|||
async fn create_new_character(
|
||||
form: &Form<NewCharacterForm>,
|
||||
user_id: i32,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> Result<(), Error> {
|
||||
let system: CharacterDataType = *form.system.as_ref().unwrap();
|
||||
let sheet: Vec<u8> = system.create_data()?.to_vec();
|
||||
|
@ -67,10 +67,10 @@ async fn create_new_character(
|
|||
let insert = NewCharacter {
|
||||
user_id: user_id,
|
||||
viewable: true,
|
||||
character_name: form.name.clone(),
|
||||
character_name: &form.name,
|
||||
data_type: system,
|
||||
data_version: 1,
|
||||
data: sheet,
|
||||
data: &sheet,
|
||||
};
|
||||
|
||||
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(
|
||||
form: Form<NewCharacterForm>,
|
||||
logged_in_user: &User,
|
||||
conn: TenebrousDbConn,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
) -> Result<Redirect, Template> {
|
||||
if let Err(e) = &form.system {
|
||||
return Err(render_error(&form, e.to_string().clone()));
|
||||
|
|
|
@ -18,7 +18,7 @@ pub struct UserHomeContext<'a> {
|
|||
}
|
||||
|
||||
#[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
|
||||
.load_character_list(user.id)
|
||||
.await?
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
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,
|
||||
);
|
Loading…
Reference in New Issue