refactor and implement user
This commit is contained in:
@@ -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)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(long)]
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -47,6 +47,21 @@ pub enum StartError {
|
||||
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 {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
Self::DatabaseInitError {
|
||||
|
||||
178
rust/src/lib.rs
178
rust/src/lib.rs
@@ -1,5 +1,175 @@
|
||||
mod error;
|
||||
mod models;
|
||||
mod sqlite;
|
||||
use axum::{extract::State, http::header::HeaderValue, middleware::Next, response::IntoResponse};
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
1340
rust/src/main.rs
1340
rust/src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,11 @@ pub struct User {
|
||||
pub fullname: String,
|
||||
}
|
||||
|
||||
pub struct NewUser<'a> {
|
||||
pub username: &'a str,
|
||||
pub fullname: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbUserRow {
|
||||
id: String,
|
||||
@@ -43,3 +48,22 @@ impl User {
|
||||
.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
153
rust/src/routing/mod.rs
Normal 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
1026
rust/src/routing/routes.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@ impl Root {
|
||||
."px-5"
|
||||
."bg-gray-200"
|
||||
."hover:bg-gray-300"
|
||||
href=(format!("/user/{}", context.user.username))
|
||||
href=(format!("/user/{}", context.user.id))
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
@@ -106,7 +106,7 @@ impl Root {
|
||||
."mdi-account"
|
||||
."text-3xl"
|
||||
{}
|
||||
p { (context.user.username)}
|
||||
p { (context.user.fullname)}
|
||||
}
|
||||
}
|
||||
(body)
|
||||
|
||||
Reference in New Issue
Block a user