This commit is contained in:
Nicole Tietz-Sokolskaya 2024-06-02 11:02:57 -04:00
parent c848037dcb
commit 3bf0f8de74
20 changed files with 117 additions and 139 deletions

View File

@ -2,8 +2,8 @@ use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use pique::db::establish_connection; use pique::db::establish_connection;
use pique::models::users::{self, NewUser}; use pique::models::users::{self, NewUser};
use rand::distributions::Alphanumeric; use rand::distributions::{Alphanumeric, DistString};
use rand::{distributions::DistString, thread_rng}; use rand::thread_rng;
#[tokio::main] #[tokio::main]
pub async fn main() -> Result<()> { pub async fn main() -> Result<()> {

View File

@ -1,5 +1,4 @@
use anyhow::Result; use anyhow::Result;
use pique::server; use pique::server;
#[tokio::main] #[tokio::main]

View File

@ -1,11 +1,8 @@
use diesel::prelude::*; use diesel::prelude::*;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::{ConnectionManager, Pool};
use diesel::r2d2::Pool;
use diesel::SqliteConnection; use diesel::SqliteConnection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use crate::{password, prelude::*};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
/// Establishes a connection to the database using the given URL. /// Establishes a connection to the database using the given URL.
@ -14,7 +11,8 @@ pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
/// * `url` - The database URL to connect to. /// * `url` - The database URL to connect to.
/// ///
/// # Panics /// # Panics
/// Panics if the database URL is not set or if the connection cannot be established. /// Panics if the database URL is not set or if the connection cannot be
/// established.
pub fn establish_connection(url: &str) -> SqliteConnection { pub fn establish_connection(url: &str) -> SqliteConnection {
SqliteConnection::establish(url).unwrap_or_else(|_| panic!("Error connecting to {}", url)) SqliteConnection::establish(url).unwrap_or_else(|_| panic!("Error connecting to {}", url))
} }
@ -45,53 +43,3 @@ pub fn build_connection_pool(url: &str) -> Pool<ConnectionManager<SqliteConnecti
pub fn migrate(conn: &mut SqliteConnection) { pub fn migrate(conn: &mut SqliteConnection) {
conn.run_pending_migrations(MIGRATIONS).unwrap(); conn.run_pending_migrations(MIGRATIONS).unwrap();
} }
pub struct NewUser {
pub full_name: String,
pub email: String,
pub username: String,
pub password: String,
}
impl NewUser {
pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
let mut validation_errors = vec![];
if self.full_name.len() > 100 {
validation_errors.push(ValidationError::on("full_name", "too long (max=100)"));
}
if self.email.len() > 100 {
validation_errors.push(ValidationError::on("email", "too long (max=100)"));
}
if self.username.len() > 100 {
validation_errors.push(ValidationError::on("username", "too long (max=32)"));
}
if validation_errors.is_empty() {
Ok(())
} else {
Err(validation_errors)
}
}
pub fn hash_password(&self) -> String {
password::hash(&self.password)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
impl ValidationError {
pub fn on(field: &str, message: &str) -> ValidationError {
ValidationError {
field: field.to_owned(),
message: message.to_owned(),
}
}
}

View File

@ -5,8 +5,7 @@ pub mod projects;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::Response; use axum::response::Response;
pub use login::login_page; pub use login::{login_page, login_submit};
pub use login::login_submit;
use tracing::error; use tracing::error;
pub fn internal_server_error() -> Response { pub fn internal_server_error() -> Response {

View File

@ -1,15 +1,15 @@
use axum::{extract::Path, http::StatusCode, response::Redirect, Form}; use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::Redirect;
use axum::Form;
use axum_login::AuthSession; use axum_login::AuthSession;
use crate::{ use crate::handler::internal_error;
handler::internal_error, use crate::models::documents::{self, NewDocument};
models::{ use crate::models::users::User;
documents::{self, NewDocument}, use crate::permissions::query::Permission;
users::User, use crate::permissions::{self};
}, use crate::prelude::*;
permissions::{self, query::Permission},
prelude::*,
};
pub async fn documents_page( pub async fn documents_page(
State(provider): State<Provider>, State(provider): State<Provider>,

View File

@ -2,8 +2,8 @@ use axum::response::Redirect;
use axum_login::AuthSession; use axum_login::AuthSession;
use crate::models::projects::Project; use crate::models::projects::Project;
use crate::permissions;
use {crate::permissions, crate::prelude::*}; use crate::prelude::*;
pub async fn home_page( pub async fn home_page(
State(provider): State<Provider>, State(provider): State<Provider>,

View File

@ -1,7 +1,10 @@
use axum::{response::Redirect, Form}; use axum::response::Redirect;
use axum::Form;
use axum_login::AuthSession; use axum_login::AuthSession;
use crate::{handler::internal_server_error, prelude::*, session::Credentials}; use crate::handler::internal_server_error;
use crate::prelude::*;
use crate::session::Credentials;
pub struct LoginTemplate { pub struct LoginTemplate {
pub username: String, pub username: String,

View File

@ -1,18 +1,15 @@
use axum::{http::StatusCode, response::Redirect, Form}; use axum::http::StatusCode;
use axum::response::Redirect;
use axum::Form;
use axum_login::AuthSession; use axum_login::AuthSession;
use crate::{
handler::internal_server_error,
models::{
project_memberships::{self, ProjectRole},
projects::{self, NewProject},
users::User,
},
permissions,
prelude::*,
};
use super::internal_error; use super::internal_error;
use crate::handler::internal_server_error;
use crate::models::project_memberships::{self, ProjectRole};
use crate::models::projects::{self, NewProject};
use crate::models::users::User;
use crate::permissions;
use crate::prelude::*;
pub async fn projects_page( pub async fn projects_page(
State(provider): State<Provider>, State(provider): State<Provider>,

View File

@ -12,3 +12,4 @@ pub mod serialize;
pub mod server; pub mod server;
pub mod session; pub mod session;
pub mod templates; pub mod templates;
pub mod validation;

View File

@ -1,10 +1,9 @@
use diesel::prelude::*; use diesel::prelude::*;
use serde::Serialize; use serde::Serialize;
use crate::schema::documents::dsl;
use uuid::Uuid; use uuid::Uuid;
use super::DbError; use super::DbError;
use crate::schema::documents::dsl;
#[derive(Queryable, Selectable, Debug, Clone, Serialize)] #[derive(Queryable, Selectable, Debug, Clone, Serialize)]
#[diesel(table_name = crate::schema::documents)] #[diesel(table_name = crate::schema::documents)]

View File

@ -1,6 +1,9 @@
use diesel::{expression::AsExpression, prelude::*, sql_types::Text};
use std::fmt; use std::fmt;
use diesel::expression::AsExpression;
use diesel::prelude::*;
use diesel::sql_types::Text;
#[derive(AsExpression, Debug, Clone)] #[derive(AsExpression, Debug, Clone)]
#[diesel(sql_type = Text)] #[diesel(sql_type = Text)]
pub enum ProjectRole { pub enum ProjectRole {
@ -63,9 +66,10 @@ pub struct NewProjectMembership {
} }
pub mod query { pub mod query {
use super::*;
use diesel::SqliteConnection; use diesel::SqliteConnection;
use super::*;
pub fn create( pub fn create(
db: &mut SqliteConnection, db: &mut SqliteConnection,
user_id: &str, user_id: &str,

View File

@ -1,10 +1,9 @@
use diesel::prelude::*; use diesel::prelude::*;
use serde::Serialize; use serde::Serialize;
use crate::schema::projects::dsl;
use uuid::Uuid; use uuid::Uuid;
use super::DbError; use super::DbError;
use crate::schema::projects::dsl;
#[derive(Queryable, Selectable, Debug, Clone, Serialize)] #[derive(Queryable, Selectable, Debug, Clone, Serialize)]
#[diesel(table_name = crate::schema::projects)] #[diesel(table_name = crate::schema::projects)]

View File

@ -1,12 +1,11 @@
use diesel::prelude::*; use diesel::prelude::*;
use serde::Serialize; use serde::Serialize;
use crate::schema::users::dsl;
use uuid::Uuid; use uuid::Uuid;
use crate::password;
use super::DbError; use super::DbError;
use crate::db::ValidationError;
use crate::password;
use crate::schema::users::dsl;
#[derive(Queryable, Selectable, Debug, Clone, Serialize)] #[derive(Queryable, Selectable, Debug, Clone, Serialize)]
#[diesel(table_name = crate::schema::users)] #[diesel(table_name = crate::schema::users)]
@ -40,6 +39,28 @@ impl NewUser {
password_hash, password_hash,
} }
} }
pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
let mut validation_errors = vec![];
if self.name.len() > 100 {
validation_errors.push(ValidationError::on("name", "too long (max=100)"));
}
if self.email.len() > 100 {
validation_errors.push(ValidationError::on("email", "too long (max=100)"));
}
if self.username.len() > 32 {
validation_errors.push(ValidationError::on("username", "too long (max=32)"));
}
if validation_errors.is_empty() {
Ok(())
} else {
Err(validation_errors)
}
}
} }
pub struct Query<'a> { pub struct Query<'a> {

View File

@ -1,9 +1,10 @@
pub mod query { pub mod query {
use diesel::prelude::*;
use diesel::SqliteConnection;
use crate::models::documents::Document; use crate::models::documents::Document;
use crate::models::project_memberships::ProjectRole; use crate::models::project_memberships::ProjectRole;
use crate::models::projects::Project; use crate::models::projects::Project;
use diesel::prelude::*;
use diesel::SqliteConnection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Permission { pub enum Permission {

View File

@ -1,7 +1,8 @@
pub use crate::provider::Provider;
pub use axum::extract::State; pub use axum::extract::State;
pub use axum::response::{Html, IntoResponse, Response}; pub use axum::response::{Html, IntoResponse, Response};
pub use minijinja::context; pub use minijinja::context;
pub use serde::{Deserialize, Serialize}; pub use serde::{Deserialize, Serialize};
pub use tracing::{debug, error, info, warn}; pub use tracing::{debug, error, info, warn};
pub use uuid::Uuid; pub use uuid::Uuid;
pub use crate::provider::Provider;

View File

@ -1,13 +1,12 @@
use diesel::{
r2d2::{ConnectionManager, Pool},
SqliteConnection,
};
use std::sync::Arc; use std::sync::Arc;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::SqliteConnection;
use minijinja_autoreload::AutoReloader;
use thiserror::Error; use thiserror::Error;
use minijinja_autoreload::AutoReloader; use crate::handler::internal_server_error;
use crate::prelude::*;
use crate::{handler::internal_server_error, prelude::*};
pub type ConnectionPool = Pool<ConnectionManager<SqliteConnection>>; pub type ConnectionPool = Pool<ConnectionManager<SqliteConnection>>;
pub type PooledConnection = diesel::r2d2::PooledConnection<ConnectionManager<SqliteConnection>>; pub type PooledConnection = diesel::r2d2::PooledConnection<ConnectionManager<SqliteConnection>>;
@ -38,7 +37,8 @@ impl Provider {
} }
pub fn render<T: Serialize>(&self, path: &str, data: T) -> anyhow::Result<String> { pub fn render<T: Serialize>(&self, path: &str, data: T) -> anyhow::Result<String> {
// TODO: more graceful handling of the potential errors here; this should not use anyhow // TODO: more graceful handling of the potential errors here; this should not
// use anyhow
let env = self.template_loader.acquire_env().unwrap(); let env = self.template_loader.acquire_env().unwrap();
let template = env.get_template(path)?; let template = env.get_template(path)?;
let rendered = template.render(data)?; let rendered = template.render(data)?;

View File

@ -1,38 +1,31 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use axum::{ use axum::routing::{get, post};
routing::{get, post}, use axum::Router;
Router,
};
use axum_login::AuthManagerLayerBuilder; use axum_login::AuthManagerLayerBuilder;
use clap::Parser; use clap::Parser;
use diesel_migrations::{embed_migrations, EmbeddedMigrations}; use diesel_migrations::{embed_migrations, EmbeddedMigrations};
use tower_http::{ use tower_http::services::ServeDir;
services::ServeDir, use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer},
};
use tower_sessions::SessionManagerLayer; use tower_sessions::SessionManagerLayer;
use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore}; use tower_sessions_sqlx_store::sqlx::SqlitePool;
use tower_sessions_sqlx_store::SqliteStore;
use tracing::Level; use tracing::Level;
use crate::{ use crate::config::CommandLineOptions;
config::CommandLineOptions, use crate::db;
provider::Provider, use crate::handler::documents::{
db,
handler::{
documents::{
create_document_page, create_document_submit, documents_page, edit_document_page, create_document_page, create_document_submit, documents_page, edit_document_page,
edit_document_submit, edit_document_submit,
},
home::home_page,
login::logout,
login_page, login_submit,
projects::{create_project_page, create_project_submit, projects_page},
},
logging::setup_logging,
templates::make_template_loader,
}; };
use crate::handler::home::home_page;
use crate::handler::login::logout;
use crate::handler::projects::{create_project_page, create_project_submit, projects_page};
use crate::handler::{login_page, login_submit};
use crate::logging::setup_logging;
use crate::provider::Provider;
use crate::templates::make_template_loader;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/"); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/");

View File

@ -1,11 +1,9 @@
use async_trait::async_trait; use async_trait::async_trait;
use axum_login::{AuthUser, AuthnBackend, UserId}; use axum_login::{AuthUser, AuthnBackend, UserId};
use crate::{ use crate::models::{self, users, DbError};
models::{self, users, DbError}, use crate::password;
password, use crate::prelude::*;
prelude::*,
};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Credentials { pub struct Credentials {

View File

@ -1,6 +1,5 @@
use free_icons::IconAttrs; use free_icons::IconAttrs;
use minijinja::{path_loader, Environment}; use minijinja::{path_loader, Environment, Error, ErrorKind};
use minijinja::{Error, ErrorKind};
use minijinja_autoreload::AutoReloader; use minijinja_autoreload::AutoReloader;
pub fn make_template_loader(auto_reload: bool) -> AutoReloader { pub fn make_template_loader(auto_reload: bool) -> AutoReloader {

16
src/validation.rs Normal file
View File

@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
impl ValidationError {
pub fn on(field: &str, message: &str) -> ValidationError {
ValidationError {
field: field.to_owned(),
message: message.to_owned(),
}
}
}