refactor and implement user

This commit is contained in:
2023-08-29 21:34:00 +02:00
parent 57f97b0b7d
commit efcac1edc0
9 changed files with 1509 additions and 1344 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO users\n (id, username, fullname)\n VALUES\n (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "961ee325bfb6af3005bad00f0c5c2a78c8d2f362142ce15ca28ee93c47bfbb7f"
}

View File

@@ -1,8 +1,107 @@
use clap::Parser; use std::fmt;
use std::process::exit;
use clap::{Parser, Subcommand};
use packager::{models, sqlite, StartError};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
#[arg(long)] #[arg(long)]
database_url: String, database_url: String,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
#[command(subcommand)]
User(UserCommand),
}
#[derive(Subcommand, Debug)]
enum UserCommand {
Create(UserCreate),
}
#[derive(Parser, Debug)]
struct UserCreate {
#[arg(long)]
username: String,
#[arg(long)]
fullname: String,
}
#[derive(Debug)]
enum Error {
Generic { message: String },
UserExists { username: String },
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Generic { message } => write!(f, "{}", message),
Self::UserExists { username } => write!(f, "user \"{username}\" already exists"),
}
}
}
impl From<StartError> for Error {
fn from(starterror: StartError) -> Self {
Self::Generic {
message: starterror.to_string(),
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let database_pool = sqlite::init_database_pool(&args.database_url).await?;
match args.command {
Command::User(cmd) => match cmd {
UserCommand::Create(user) => {
let id = match models::user::create(
&database_pool,
models::user::NewUser {
username: &user.username,
fullname: &user.fullname,
},
)
.await
{
Ok(id) => id,
Err(error) => {
if let models::Error::Query(models::QueryError::Duplicate {
description: _,
}) = error
{
println!(
"Error: {}",
Error::UserExists {
username: user.username,
}
.to_string()
);
exit(1);
}
return Err(error.into());
}
};
println!(
"User \"{}\" created successfully (id {})",
user.username, id
)
}
},
}
Ok(())
} }

View File

@@ -47,6 +47,21 @@ pub enum StartError {
DatabaseMigrationError { message: String }, DatabaseMigrationError { message: String },
} }
impl fmt::Display for StartError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DatabaseInitError { message } => {
write!(f, "database initialization error: {message}")
}
Self::DatabaseMigrationError { message } => {
write!(f, "database migration error: {message}")
}
}
}
}
impl std::error::Error for StartError {}
impl From<sqlx::Error> for StartError { impl From<sqlx::Error> for StartError {
fn from(value: sqlx::Error) -> Self { fn from(value: sqlx::Error) -> Self {
Self::DatabaseInitError { Self::DatabaseInitError {

View File

@@ -1,5 +1,175 @@
mod error; use axum::{extract::State, http::header::HeaderValue, middleware::Next, response::IntoResponse};
mod models;
mod sqlite;
use error::{Error, RequestError, StartError}; use hyper::Request;
use uuid::Uuid;
use std::fmt;
pub mod error;
pub mod models;
pub mod routing;
pub mod sqlite;
mod html;
mod view;
pub use error::{Error, RequestError, StartError};
#[derive(Clone)]
pub enum AuthConfig {
Enabled,
Disabled { assume_user: String },
}
#[derive(Clone)]
pub struct AppState {
pub database_pool: sqlite::Pool<sqlite::Sqlite>,
pub client_state: ClientState,
pub auth_config: AuthConfig,
}
#[derive(Clone)]
pub struct Context {
user: models::user::User,
}
impl Context {
fn build(user: models::user::User) -> Self {
Self { user }
}
}
#[derive(Clone)]
pub struct ClientState {
pub active_category_id: Option<Uuid>,
pub edit_item: Option<Uuid>,
pub trip_edit_attribute: Option<models::trips::TripAttribute>,
pub trip_type_edit: Option<Uuid>,
}
impl ClientState {
pub fn new() -> Self {
ClientState {
active_category_id: None,
edit_item: None,
trip_edit_attribute: None,
trip_type_edit: None,
}
}
}
impl Default for ClientState {
fn default() -> Self {
Self::new()
}
}
struct UriPath(String);
impl fmt::Display for UriPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> Into<&'a str> for &'a UriPath {
fn into(self) -> &'a str {
self.0.as_str()
}
}
#[derive(PartialEq, Eq)]
pub enum TopLevelPage {
Inventory,
Trips,
}
impl TopLevelPage {
fn id(&self) -> &'static str {
match self {
Self::Inventory => "inventory",
Self::Trips => "trips",
}
}
fn path(&self) -> UriPath {
UriPath(
match self {
Self::Inventory => "/inventory/",
Self::Trips => "/trips/",
}
.to_string(),
)
}
fn name(&self) -> &'static str {
match self {
Self::Inventory => "Inventory",
Self::Trips => "Trips",
}
}
}
enum HtmxEvents {
TripItemEdited,
}
impl From<HtmxEvents> for HeaderValue {
fn from(val: HtmxEvents) -> Self {
HeaderValue::from_static(val.to_str())
}
}
impl HtmxEvents {
fn to_str(&self) -> &'static str {
match self {
Self::TripItemEdited => "TripItemEdited",
}
}
}
async fn authorize<B>(
State(state): State<AppState>,
mut request: Request<B>,
next: Next<B>,
) -> Result<impl IntoResponse, Error> {
let current_user = match state.auth_config {
AuthConfig::Disabled { assume_user } => {
match models::user::User::find_by_name(&state.database_pool, &assume_user).await? {
Some(user) => user,
None => {
return Err(Error::Request(RequestError::AuthenticationUserNotFound {
username: assume_user,
}))
}
}
}
AuthConfig::Enabled => {
let Some(username) = request.headers().get("x-auth-username") else {
return Err(Error::Request(RequestError::AuthenticationHeaderMissing));
};
let username = username
.to_str()
.map_err(|error| {
Error::Request(RequestError::AuthenticationHeaderInvalid {
message: error.to_string(),
})
})?
.to_string();
match models::user::User::find_by_name(&state.database_pool, &username).await? {
Some(user) => user,
None => {
return Err(Error::Request(RequestError::AuthenticationUserNotFound {
username,
}))
}
}
}
};
request.extensions_mut().insert(current_user);
Ok(next.run(request).await)
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,11 @@ pub struct User {
pub fullname: String, pub fullname: String,
} }
pub struct NewUser<'a> {
pub username: &'a str,
pub fullname: &'a str,
}
#[derive(Debug)] #[derive(Debug)]
pub struct DbUserRow { pub struct DbUserRow {
id: String, id: String,
@@ -43,3 +48,22 @@ impl User {
.transpose() .transpose()
} }
} }
pub async fn create(pool: &sqlx::Pool<sqlx::Sqlite>, user: NewUser<'_>) -> Result<Uuid, Error> {
let id = Uuid::new_v4();
let id_param = id.to_string();
sqlx::query!(
"INSERT INTO users
(id, username, fullname)
VALUES
(?, ?, ?)",
id_param,
user.username,
user.fullname
)
.execute(pool)
.await?;
Ok(id)
}

153
rust/src/routing/mod.rs Normal file
View File

@@ -0,0 +1,153 @@
use axum::{
http::header::{HeaderMap, HeaderName},
middleware,
routing::{get, post},
Router,
};
use crate::{authorize, AppState, Error, RequestError, TopLevelPage};
mod routes;
use routes::*;
enum HtmxResponseHeaders {
Trigger,
PushUrl,
}
impl From<HtmxResponseHeaders> for HeaderName {
fn from(val: HtmxResponseHeaders) -> Self {
match val {
HtmxResponseHeaders::Trigger => HeaderName::from_static("hx-trigger"),
HtmxResponseHeaders::PushUrl => HeaderName::from_static("hx-push-url"),
}
}
}
enum HtmxRequestHeaders {
HtmxRequest,
}
impl From<HtmxRequestHeaders> for HeaderName {
fn from(val: HtmxRequestHeaders) -> Self {
match val {
HtmxRequestHeaders::HtmxRequest => HeaderName::from_static("hx-request"),
}
}
}
fn is_htmx(headers: &HeaderMap) -> bool {
headers
.get::<HeaderName>(HtmxRequestHeaders::HtmxRequest.into())
.map(|value| value == "true")
.unwrap_or(false)
}
fn get_referer<'a>(headers: &'a HeaderMap) -> Result<&'a str, Error> {
headers
.get("referer")
.ok_or(Error::Request(RequestError::RefererNotFound))?
.to_str()
.map_err(|error| {
Error::Request(RequestError::RefererInvalid {
message: error.to_string(),
})
})
}
pub fn router(state: AppState) -> Router {
Router::new()
.route("/favicon.svg", get(icon))
.route("/assets/luggage.svg", get(icon))
.route(
"/notfound",
get(|| async {
Error::Request(RequestError::NotFound {
message: "hi".to_string(),
})
}),
)
.route("/debug", get(debug))
.merge(
// thse are routes that require authentication
Router::new()
.route("/", get(root))
.nest(
(&TopLevelPage::Trips.path()).into(),
Router::new()
.route("/", get(trips).post(trip_create))
.route("/types/", get(trips_types).post(trip_type_create))
.route("/types/:id/edit/name/submit", post(trips_types_edit_name))
.route("/:id/", get(trip))
.route("/:id/comment/submit", post(trip_comment_set))
.route("/:id/categories/:id/select", post(trip_category_select))
.route("/:id/packagelist/", get(trip_packagelist))
.route(
"/:id/packagelist/item/:id/pack",
post(trip_item_packagelist_set_pack_htmx),
)
.route(
"/:id/packagelist/item/:id/unpack",
post(trip_item_packagelist_set_unpack_htmx),
)
.route(
"/:id/packagelist/item/:id/ready",
post(trip_item_packagelist_set_ready_htmx),
)
.route(
"/:id/packagelist/item/:id/unready",
post(trip_item_packagelist_set_unready_htmx),
)
.route("/:id/state/:id", post(trip_state_set))
.route("/:id/total_weight", get(trip_total_weight_htmx))
.route("/:id/type/:id/add", get(trip_type_add))
.route("/:id/type/:id/remove", get(trip_type_remove))
.route("/:id/edit/:attribute/submit", post(trip_edit_attribute))
.route(
"/:id/items/:id/pick",
get(trip_item_set_pick).post(trip_item_set_pick_htmx),
)
.route(
"/:id/items/:id/unpick",
get(trip_item_set_unpick).post(trip_item_set_unpick_htmx),
)
.route(
"/:id/items/:id/pack",
get(trip_item_set_pack).post(trip_item_set_pack_htmx),
)
.route(
"/:id/items/:id/unpack",
get(trip_item_set_unpack).post(trip_item_set_unpack_htmx),
)
.route(
"/:id/items/:id/ready",
get(trip_item_set_ready).post(trip_item_set_ready_htmx),
)
.route(
"/:id/items/:id/unready",
get(trip_item_set_unready).post(trip_item_set_unready_htmx),
),
)
.nest(
(&TopLevelPage::Inventory.path()).into(),
Router::new()
.route("/", get(inventory_inactive))
.route("/categories/:id/select", post(inventory_category_select))
.route("/category/", post(inventory_category_create))
.route("/category/:id/", get(inventory_active))
.route("/item/", post(inventory_item_create))
.route("/item/:id/", get(inventory_item))
.route("/item/:id/cancel", get(inventory_item_cancel))
.route("/item/:id/delete", get(inventory_item_delete))
.route("/item/:id/edit", post(inventory_item_edit))
.route("/item/name/validate", post(inventory_item_validate_name)),
)
.layer(middleware::from_fn_with_state(state.clone(), authorize)),
)
.fallback(|| async {
Error::Request(RequestError::NotFound {
message: "no route found".to_string(),
})
})
.with_state(state)
}

1026
rust/src/routing/routes.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -98,7 +98,7 @@ impl Root {
."px-5" ."px-5"
."bg-gray-200" ."bg-gray-200"
."hover:bg-gray-300" ."hover:bg-gray-300"
href=(format!("/user/{}", context.user.username)) href=(format!("/user/{}", context.user.id))
{ {
span span
."m-auto" ."m-auto"
@@ -106,7 +106,7 @@ impl Root {
."mdi-account" ."mdi-account"
."text-3xl" ."text-3xl"
{} {}
p { (context.user.username)} p { (context.user.fullname)}
} }
} }
(body) (body)