Bloody beans, there's finally a workig database
This commit is contained in:
parent
22761ee92e
commit
61e3a2cc31
18 changed files with 1943 additions and 106 deletions
|
@ -1,2 +1,3 @@
|
|||
[env]
|
||||
RUST_BACKTRACE = "1"
|
||||
RUSTFLAGS = "--cfg uuid_unstable"
|
||||
|
|
1723
Cargo.lock
generated
1723
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "1.73"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
5
src/entities/mod.rs
Normal file
5
src/entities/mod.rs
Normal 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
3
src/entities/prelude.rs
Normal 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
22
src/entities/user.rs
Normal 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 {}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...");
|
||||
}
|
||||
|
|
|
@ -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
12
src/migrator.rs
Normal 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)]
|
||||
}
|
||||
}
|
52
src/migrator/m20231114_143300_init.rs
Normal file
52
src/migrator/m20231114_143300_init.rs
Normal 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,
|
||||
}
|
|
@ -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>,
|
||||
}
|
116
src/router.rs
116
src/router.rs
|
@ -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(®ister.authenticity_token)?;
|
||||
State(s): State<Arc<state::State>>,
|
||||
Form(f): Form<Register>,
|
||||
) -> AppRes {
|
||||
csrf_verify(c, &f.authenticity_token)?;
|
||||
|
||||
let new_user: NewUser = (®ister).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(®ister.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(),
|
||||
),
|
||||
|
|
21
src/state.rs
21
src/state.rs
|
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
|
42
src/views.rs
42
src/views.rs
|
@ -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)
|
||||
")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue