Bloody beans, there's finally a workig database

This commit is contained in:
Daniel Flanagan 2023-11-14 16:05:27 -06:00
parent 22761ee92e
commit 61e3a2cc31
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
18 changed files with 1943 additions and 106 deletions

View file

@ -1,2 +1,3 @@
[env]
RUST_BACKTRACE = "1"
RUSTFLAGS = "--cfg uuid_unstable"

1723
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,8 @@ thiserror = "1.0.50"
axum-macros = "0.3.8"
color-eyre = "0.6.2"
# irust
# bacon
# sqlx (sea orm?)
# poem-openapi?
# db
sea-orm = { version = "0.12.6", features = ["sqlx-sqlite", "macros", "runtime-tokio-rustls"] }
sea-orm-migration = { version = "0.12.6", features = ["sqlx-sqlite"] }
uuid = { version = "1.5.0", features = ["v7", "atomic", "fast-rng", "macro-diagnostics"] }
password-hash = "0.5.0"

View file

@ -53,12 +53,19 @@
rust-analyzer
nodePackages_latest.vscode-langservers-extracted
# to install sea-orm-cli
pkg-config
openssl
hurl
];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
shellHook = ''
export DATABASE_URL="sqlite://./data/lyrs.sqlitedb";
export MIGRATION_DIR="src/migrator"
export DATABASE_URL="sqlite://./data/lyrs.sqlitedb?mode=rwc";
export COOKIE_KEY="2z49_8yfKUkoTOo0cjzzjwufCfhKvfOIc1CGleuTXC5zRqY4U0Xhkd34ipREQN5iHRH62tt5O7y6U5mmFBH3MA"
export RUST_BACKTRACE="1"
export RUSTFLAGS="--cfg uuid_unstable"
'';
};
});

View file

@ -1,2 +1,2 @@
[toolchain]
channel = "nightly"
channel = "1.73"

View file

@ -1 +0,0 @@

5
src/entities/mod.rs Normal file
View file

@ -0,0 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
pub mod prelude;
pub mod user;

3
src/entities/prelude.rs Normal file
View file

@ -0,0 +1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
pub use super::user::Entity as User;

22
src/entities/user.rs Normal file
View file

@ -0,0 +1,22 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(
primary_key,
auto_increment = false,
column_type = "Binary(BlobSize::Blob(None))"
)]
pub id: Vec<u8>,
pub name: Option<String>,
pub username: String,
pub password_digest: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,13 +1,14 @@
use core::fmt;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use core::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
Database(#[from] sea_orm::error::DbErr),
PasswordHash(#[from] password_hash::Error),
InvalidCsrf(#[from] axum_csrf::CsrfError),
Other(#[from] anyhow::Error),
}
@ -17,6 +18,8 @@ impl AppError {
match self {
AppError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::InvalidCsrf(_) => StatusCode::BAD_REQUEST,
AppError::PasswordHash(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
@ -24,6 +27,8 @@ impl AppError {
match self {
AppError::Other(e) => format!("something went wrong: {}", e),
AppError::InvalidCsrf(e) => format!("unable to verify csrf: {}", e),
AppError::PasswordHash(e) => format!("failed to hash password: {}", e),
AppError::Database(e) => format!("database error: {}", e),
}
}
}

View file

@ -1,9 +1,11 @@
use tracing::{instrument, trace};
use tracing::{info, instrument};
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
#[instrument]
pub fn init() {
color_eyre::install().expect("Failed to install color_eyre");
setup_trace_logger();
info!("Instrumentation initialized.");
}
#[instrument]
@ -13,6 +15,4 @@ pub fn setup_trace_logger() {
.parse_lossy("info,lyrs=trace");
tracing_subscriber::fmt().with_env_filter(filter).init();
trace!("Starting...");
}

View file

@ -4,11 +4,11 @@
// TODO: Implement authn
mod app;
mod database;
mod entities;
mod error;
mod feather_icons;
mod instrumentation;
mod models;
mod migrator;
mod partials;
mod router;
mod server;

12
src/migrator.rs Normal file
View file

@ -0,0 +1,12 @@
use sea_orm_migration::prelude::*;
pub struct Migrator;
mod m20231114_143300_init;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20231114_143300_init::Migration)]
}
}

View file

@ -0,0 +1,52 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20231114_143300_init.rs"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.col(
ColumnDef::new(User::Id)
.binary_len(16)
.not_null()
.primary_key(),
)
.col(ColumnDef::new(User::Name).string())
.col(
ColumnDef::new(User::Username)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(User::PasswordDigest).text().not_null())
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum User {
Table,
Id,
Name,
Username,
PasswordDigest,
}

View file

@ -1,12 +0,0 @@
pub struct User {
pub id: Vec<u8>,
pub username: String,
pub name: Option<String>,
pub password_digest: Vec<u8>,
}
pub struct NewUser<'a> {
pub username: &'a str,
pub name: Option<&'a str>,
pub password_digest: Vec<u8>,
}

View file

@ -1,26 +1,22 @@
use std::{env, path::Path};
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHasher,
};
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
routing::get,
Form, Router,
};
use crate::entities::{prelude::*, *};
use crate::state;
use crate::{error::AppError, views};
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHasher};
use axum::extract::State;
use axum::{http::StatusCode, response::Html, routing::get, Form, Router};
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken};
use base64::prelude::*;
use maud::html;
use notify::Watcher;
use sea_orm::*;
use serde::Deserialize;
use std::sync::Arc;
use std::{env, path::Path};
use tower_http::services::ServeDir;
use tower_livereload::LiveReloadLayer;
use tracing::instrument;
use base64::prelude::*;
use crate::{error::AppError, models::NewUser, state::State, views};
use tracing::{info, instrument};
#[instrument]
pub async fn new() -> Result<Router, anyhow::Error> {
@ -29,7 +25,7 @@ pub async fn new() -> Result<Router, anyhow::Error> {
.route("/hello-world-text", get(views::greet_world_text));
let assets_dir = ServeDir::new("./assets");
let state = State::new().await?;
let state = state::State::new().await?;
let live_reload_layer = LiveReloadLayer::new();
let reloader = live_reload_layer.reloader();
@ -58,6 +54,14 @@ pub async fn new() -> Result<Router, anyhow::Error> {
Ok(router)
}
fn csrf_verify(c: CsrfToken, t: &str) -> Result<(), AppError> {
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
c.verify(t)?;
Ok(())
}
type AppRes = Result<(StatusCode, Html<String>), AppError>;
#[derive(Deserialize)]
struct Register {
authenticity_token: String,
@ -65,40 +69,52 @@ struct Register {
password: String,
}
impl<'a> TryInto<NewUser<'a>> for &'a Register {
type Error = anyhow::Error;
impl TryInto<user::ActiveModel> for Register {
type Error = AppError;
fn try_into(self: &'a Register) -> Result<NewUser<'a>, Self::Error> {
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,
fn try_into(self) -> Result<user::ActiveModel, Self::Error> {
Ok(user::ActiveModel {
id: ActiveValue::Set(uuid::Uuid::now_v7().into()),
name: ActiveValue::Set(None),
username: ActiveValue::Set(self.username),
password_digest: ActiveValue::Set(password_digest(self.password)?),
})
}
}
fn password_digest<S>(s: S) -> Result<String, password_hash::Error>
where
S: AsRef<str>,
{
Ok(Argon2::default()
.hash_password(s.as_ref().as_bytes(), &SaltString::generate(&mut OsRng))?
.serialize()
.to_string())
}
impl user::ActiveModel {}
async fn register(
c: CsrfToken,
Form(register): Form<Register>,
) -> Result<impl IntoResponse, AppError> {
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
c.verify(&register.authenticity_token)?;
State(s): State<Arc<state::State>>,
Form(f): Form<Register>,
) -> AppRes {
csrf_verify(c, &f.authenticity_token)?;
let new_user: NewUser = (&register).try_into()?;
// TODO: handle duplicate username
let new: user::ActiveModel = f.try_into()?;
let res = User::insert(new).exec(&s.db).await;
info!("insert new user result: {:?}", res);
res.map_err(anyhow::Error::from)?;
Ok((
StatusCode::CREATED,
Html(
html! {
h1 { (new_user.username) }
// h1 { (new_user.username) }
}
.into_string(),
),
@ -112,27 +128,15 @@ struct Login {
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);
println!("{:?} {:?}", register.authenticity_token, v);
if v.is_err() {
return Ok((
StatusCode::BAD_REQUEST,
Html(html! { "invalid request" }.into_string()),
));
}
async fn login(c: CsrfToken, Form(f): Form<Login>) -> AppRes {
csrf_verify(c, &f.authenticity_token)?;
Ok((
StatusCode::CREATED,
StatusCode::OK,
Html(
html! {
h1 { (register.username) }
h1 { (register.password) }
h1 { (f.username) }
h1 { (f.password) }
}
.into_string(),
),

View file

@ -1,12 +1,21 @@
use std::env;
use std::{env, sync::Arc};
#[derive(Clone)]
pub struct State {}
use sea_orm::{Database, DatabaseConnection};
use sea_orm_migration::MigratorTrait;
use crate::migrator::Migrator;
pub struct State {
pub db: DatabaseConnection,
}
impl State {
pub async fn new() -> Result<Self, anyhow::Error> {
let _database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
pub async fn new() -> Result<Arc<Self>, anyhow::Error> {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
Ok(State {})
let db = Database::connect(database_url).await?;
Migrator::refresh(&db).await?;
Ok(Arc::new(State { db }))
}
}

View file

@ -1,17 +1,20 @@
use crate::entities::{prelude::*, *};
use crate::{
error::AppError,
partials::{footer, header},
state,
};
use axum::{
extract::State,
http::StatusCode,
response::{Html, IntoResponse},
};
use axum_csrf::CsrfToken;
use maud::html;
use sea_orm::EntityTrait;
use std::sync::Arc;
use tracing::instrument;
use crate::{
error::AppError,
models::User,
partials::{footer, header},
};
pub async fn csrf<F>(csrf: CsrfToken, cb: F) -> impl IntoResponse
where
F: Fn(&str) -> Html<String>,
@ -20,6 +23,8 @@ where
(csrf, cb(&token))
}
type AppRes = Result<(StatusCode, Html<String>), AppError>;
#[instrument]
pub async fn index() -> Html<String> {
Html(
@ -100,11 +105,8 @@ pub async fn login(t: CsrfToken) -> impl IntoResponse {
.await
}
#[allow(unreachable_code)]
pub async fn all_users(
State(_state): State<crate::state::State>,
) -> Result<Html<String>, AppError> {
let all_users: Vec<User> = vec![];
pub async fn all_users(State(s): State<Arc<state::State>>) -> Result<Html<String>, AppError> {
let users: Vec<user::Model> = User::find().all(&s.db).await?;
// @if let Some(name) = u.name {
// name
@ -117,13 +119,17 @@ pub async fn all_users(
main class="prose" {
h1 { "Users" }
ul {
@for u in all_users {
li {
(u.username)
@if let Some(name) = u.name {
" ("
(name)
")"
@if users.len() < 1 {
li { "It looks like there are no users yet!" }
} else {
@for u in users {
li {
(u.username)
@if let Some(name) = u.name {
" ("
(name)
")"
}
}
}
}