Ready for a database

This commit is contained in:
Daniel Flanagan 2023-11-14 11:26:03 -06:00
parent bfc5a6f90d
commit 0782e82d6e
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
15 changed files with 75 additions and 368 deletions

214
Cargo.lock generated
View file

@ -358,49 +358,6 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "deadpool"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
dependencies = [
"async-trait",
"deadpool-runtime",
"num_cpus",
"serde",
"tokio",
]
[[package]]
name = "deadpool-diesel"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8404d25ddc6cb0676d4a863bbd007613ee3fffb54db23e0e6341e1fe61c3e"
dependencies = [
"deadpool",
"deadpool-sync",
"diesel",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49"
dependencies = [
"tokio",
]
[[package]]
name = "deadpool-sync"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8db70494c13cae4ce67b4b4dafdaf828cf0df7237ab5b9e2fcabee4965d0a0a"
dependencies = [
"deadpool-runtime",
"tracing",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.9" version = "0.3.9"
@ -410,50 +367,6 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "diesel"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2268a214a6f118fce1838edba3d1561cf0e78d8de785475957a580a7f8c69d33"
dependencies = [
"diesel_derives",
"libsqlite3-sys",
"time",
"uuid",
]
[[package]]
name = "diesel_derives"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
dependencies = [
"diesel_table_macro_syntax",
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "diesel_migrations"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
dependencies = [
"diesel",
"migrations_internals",
"migrations_macros",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
dependencies = [
"syn 2.0.39",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -465,12 +378,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "eyre" name = "eyre"
version = "0.6.8" version = "0.6.8"
@ -593,12 +500,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "hashbrown"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
[[package]] [[package]]
name = "headers" name = "headers"
version = "0.3.9" version = "0.3.9"
@ -713,16 +614,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.9.6"
@ -790,16 +681,6 @@ version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.11" version = "0.4.11"
@ -828,10 +709,6 @@ dependencies = [
"base64", "base64",
"color-eyre", "color-eyre",
"cookie", "cookie",
"deadpool",
"deadpool-diesel",
"diesel",
"diesel_migrations",
"maud", "maud",
"notify", "notify",
"serde", "serde",
@ -886,27 +763,6 @@ version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "migrations_internals"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "migrations_macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -1088,12 +944,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]] [[package]]
name = "polyval" name = "polyval"
version = "0.6.1" version = "0.6.1"
@ -1326,15 +1176,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1549,40 +1390,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.4.13" version = "0.4.13"
@ -1759,24 +1566,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "uuid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -1904,12 +1699,3 @@ name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
dependencies = [
"memchr",
]

View file

@ -20,12 +20,6 @@ tower-http = { version = "0.4.4", features = ["fs"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
# database
deadpool = "0.10.0"
deadpool-diesel = { version = "0.5.0", features = ["rt_tokio_1", "sqlite", "tracing", "serde"] }
diesel = { version = "2.1.3", features = ["sqlite", "uuid"] }
diesel_migrations = { version = "2.1.0", features = ["sqlite"] }
# fancy during-development stuff # fancy during-development stuff
notify = "6.1.1" notify = "6.1.1"
tower-livereload = "0.8.2" tower-livereload = "0.8.2"
@ -34,7 +28,6 @@ thiserror = "1.0.50"
axum-macros = "0.3.8" axum-macros = "0.3.8"
color-eyre = "0.6.2" color-eyre = "0.6.2"
# color-eyre
# irust # irust
# bacon # bacon
# sqlx (sea orm?) # sqlx (sea orm?)

View file

@ -1,6 +0,0 @@
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

View file

@ -50,11 +50,9 @@
rustfmt rustfmt
rustPackages.clippy rustPackages.clippy
rustPackages.bacon
rust-analyzer rust-analyzer
nodePackages_latest.vscode-langservers-extracted nodePackages_latest.vscode-langservers-extracted
diesel-cli
hurl hurl
]; ];
RUST_SRC_PATH = rustPlatform.rustLibSrc; RUST_SRC_PATH = rustPlatform.rustLibSrc;

View file

@ -1 +0,0 @@
drop table users

View file

@ -1,7 +0,0 @@
create table users (
id text primary key,
username text
name text,
password_digest binary
);
create unique index users_username on users(username);

View file

@ -1,42 +1 @@
use deadpool_diesel::sqlite::{Manager, Pool, Runtime};
use diesel::sqlite::Sqlite;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use tracing::info;
#[derive(Clone)]
pub struct Database {
pub pool: Pool,
}
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
impl Database {
// TODO: database seeding?
fn run_migrations(
connection: &mut impl MigrationHarness<Sqlite>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// This will run the necessary migrations.
//
// See the documentation for `MigrationHarness` for
// all available methods.
connection.run_pending_migrations(MIGRATIONS)?;
Ok(())
}
// TODO: make an actual error type
pub async fn new<T: AsRef<str>>(database_url: T) -> Result<Self, anyhow::Error> {
let manager = Manager::new(database_url.as_ref(), Runtime::Tokio1);
let pool = Pool::builder(manager).max_size(8).build().unwrap();
let conn = pool.get().await?;
let _ = conn
.interact(|c| Self::run_migrations(c))
.await
.expect("Failed to run migrations");
info!("Migrations completed!");
return Ok(Database { pool });
}
}

View file

@ -1,13 +1,9 @@
use std::fmt::Display;
use axum::{ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use thiserror::Error;
#[derive(Error, Debug)] pub struct AppError(anyhow::Error);
pub struct AppError(#[from] anyhow::Error);
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
@ -19,8 +15,11 @@ impl IntoResponse for AppError {
} }
} }
impl Display for AppError { impl<E> From<E> for AppError
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { where
f.write_fmt(format!("app error")) E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
} }
} }

View file

@ -2,7 +2,7 @@ use tracing::{instrument, trace};
use tracing_subscriber::{filter::LevelFilter, EnvFilter}; use tracing_subscriber::{filter::LevelFilter, EnvFilter};
pub fn init() { pub fn init() {
color_eyre::install(); color_eyre::install().expect("Failed to install color_eyre");
setup_trace_logger(); setup_trace_logger();
} }

View file

@ -11,7 +11,6 @@ mod instrumentation;
mod models; mod models;
mod partials; mod partials;
mod router; mod router;
mod schema;
mod server; mod server;
mod state; mod state;
mod views; mod views;

View file

@ -1,23 +1,12 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User { pub struct User {
// pub id: Text,
// pub username: sql_types::Text,
// pub name: sql_types::Text,
// pub password_digest: sql_types::Binary,
pub id: Vec<u8>, pub id: Vec<u8>,
pub username: String, pub username: String,
pub name: Option<String>, pub name: Option<String>,
pub password_digest: Vec<u8>, pub password_digest: Vec<u8>,
} }
#[derive(Insertable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser<'a> { pub struct NewUser<'a> {
pub username: &'a str, pub username: &'a str,
pub name: Option<&'a str>, pub name: Option<&'a str>,
pub password_digest: &'a [u8], pub password_digest: Vec<u8>,
} }

View file

@ -1,4 +1,4 @@
use std::{env, path::Path, sync::Arc}; use std::{env, path::Path};
use argon2::{ use argon2::{
password_hash::{rand_core::OsRng, SaltString}, password_hash::{rand_core::OsRng, SaltString},
@ -29,7 +29,7 @@ pub async fn new() -> Result<Router, anyhow::Error> {
.route("/hello-world-text", get(views::greet_world_text)); .route("/hello-world-text", get(views::greet_world_text));
let assets_dir = ServeDir::new("./assets"); let assets_dir = ServeDir::new("./assets");
let state = Arc::new(State::new().await?); let state = State::new().await?;
let live_reload_layer = LiveReloadLayer::new(); let live_reload_layer = LiveReloadLayer::new();
let reloader = live_reload_layer.reloader(); let reloader = live_reload_layer.reloader();
@ -48,6 +48,7 @@ pub async fn new() -> Result<Router, anyhow::Error> {
.nest("/app", app_router) .nest("/app", app_router)
.nest_service("/assets", assets_dir) .nest_service("/assets", assets_dir)
.route("/", get(views::index)) .route("/", get(views::index))
.route("/login", get(views::login).post(login))
.route("/register", get(views::register).post(register)) .route("/register", get(views::register).post(register))
.route("/all_users", get(views::all_users)) .route("/all_users", get(views::all_users))
.with_state(state) .with_state(state)
@ -64,11 +65,23 @@ struct Register {
password: String, password: String,
} }
impl<'a> TryInto<NewUser<'a>> for Register { impl<'a> TryInto<NewUser<'a>> for &'a Register {
type Error = (); type Error = argon2::password_hash::Error;
fn try_into(self) -> Result<NewUser<'a>, Self::Error> { fn try_into(self: &'a Register) -> Result<NewUser<'a>, Self::Error> {
todo!() let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_digest: Vec<u8> = argon2
.hash_password(self.password.as_bytes(), &salt)?
.hash
.expect("no password hash")
.as_bytes()
.into();
Ok(NewUser {
username: &self.username,
name: None,
password_digest,
})
} }
} }
@ -78,6 +91,40 @@ async fn register(
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf // TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
let v = csrf_token.verify(&register.authenticity_token);
if v.is_err() {
return Ok((
StatusCode::BAD_REQUEST,
Html(html! { "invalid request" }.into_string()),
));
}
let new_user: NewUser = (&register).try_into()?;
Ok((
StatusCode::CREATED,
Html(
html! {
h1 { (new_user.username) }
}
.into_string(),
),
))
}
#[derive(Deserialize)]
struct Login {
authenticity_token: String,
username: String,
password: String,
}
async fn login(
csrf_token: CsrfToken,
Form(register): Form<Login>,
) -> Result<impl IntoResponse, AppError> {
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
let v = csrf_token.verify(&register.authenticity_token); let v = csrf_token.verify(&register.authenticity_token);
println!("{:?} {:?}", register.authenticity_token, v); println!("{:?} {:?}", register.authenticity_token, v);
if v.is_err() { if v.is_err() {
@ -87,16 +134,6 @@ async fn register(
)); ));
} }
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_digest = argon2.hash_password(register.password.as_bytes(), &salt)?;
let _new_user = NewUser {
username: &register.username,
name: None,
password_digest: password_digest.to_string().as_bytes(),
};
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
Html( Html(

View file

@ -1,10 +0,0 @@
// @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Binary,
username -> Text,
name -> Nullable<Text>,
password_digest -> Binary,
}
}

View file

@ -1,17 +1,12 @@
use std::env; use std::env;
use crate::database;
#[derive(Clone)] #[derive(Clone)]
pub struct State { pub struct State {}
pub database: database::Database,
}
impl State { impl State {
pub async fn new() -> Result<Self, anyhow::Error> { pub async fn new() -> Result<Self, anyhow::Error> {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let _database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let database = database::Database::new(database_url).await?;
Ok(State { database }) Ok(State {})
} }
} }

View file

@ -1,15 +1,9 @@
use std::{error::Error, sync::Arc};
use axum::{ use axum::{
extract::State, extract::State,
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use axum_csrf::CsrfToken; use axum_csrf::CsrfToken;
use axum_macros::debug_handler;
use deadpool_diesel::InteractError;
use diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
use maud::html; use maud::html;
use thiserror::Error;
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
@ -102,32 +96,11 @@ pub async fn login(csrf: CsrfToken) -> impl IntoResponse {
.into_response() .into_response()
} }
#[derive(Error, Debug)] #[allow(unreachable_code)]
enum AllUsersError {
#[error("other application error")]
App(#[from] AppError),
#[error("failed to retrieve users")]
DB(#[from] InteractError),
}
#[debug_handler]
pub async fn all_users( pub async fn all_users(
State(state): State<crate::state::State>, State(_state): State<crate::state::State>,
) -> Result<Html<String>, AllUsersError> { ) -> Result<Html<String>, AppError> {
use crate::schema::users::dsl::*; let all_users: Vec<User> = vec![];
let conn = state.database.pool.get().await?;
let cc = |c| -> Vec<User> {
users
.select(User::as_select())
.load(c)
.expect("error loading users")
};
let all_users: Vec<User> = match conn.interact(cc).await {
Ok(u) => u,
Err(e) => return Err(("failed to retrieve users")),
};
// @if let Some(name) = u.name { // @if let Some(name) = u.name {
// name // name
@ -142,13 +115,16 @@ pub async fn all_users(
ul { ul {
@for u in all_users { @for u in all_users {
li { li {
(u) (u.username)
"(" @if let Some(name) = u.name {
" ("
(name)
")" ")"
} }
} }
} }
} }
}
(footer()) (footer())
} }
.into_string(), .into_string(),