diff --git a/Cargo.lock b/Cargo.lock index 6ce0af2..9eb335e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2478,6 +2478,7 @@ dependencies = [ "axum-login", "axum-messages", "blake3", + "chrono", "entity", "log", "password-auth", diff --git a/entity/Cargo.toml b/entity/Cargo.toml index 650861f..675493e 100644 --- a/entity/Cargo.toml +++ b/entity/Cargo.toml @@ -3,18 +3,17 @@ name = "entity" version = "0.1.0" edition = "2021" publish = false +default-run = "entity" [lib] name = "entity" path = "src/lib.rs" + [dependencies] serde = { version = "1", features = ["derive"] } tokio = { version = "1.32.0", features = ["full"] } [dependencies.sea-orm] -version = "0.12.10" # sea-orm version -features = [ - "runtime-tokio-native-tls", - "sqlx-postgres", -] \ No newline at end of file +version = "0.12.10" # sea-orm version +features = ["runtime-tokio-native-tls", "sqlx-postgres"] diff --git a/entity/src/bin/drop_tables.rs b/entity/src/bin/drop_tables.rs new file mode 100644 index 0000000..1a5fae7 --- /dev/null +++ b/entity/src/bin/drop_tables.rs @@ -0,0 +1,43 @@ +use sea_orm::{ + ConnectionTrait, Database, DatabaseConnection, DbConn, EntityTrait, Schema, Statement, +}; + +use entity::{group, group_permission, permission, user, user_group, user_permission}; + +async fn drop_table(db: &DatabaseConnection, entity: E) +where + E: EntityTrait, +{ + let table_name = entity.table_name(); + let sql = format!("DROP TABLE IF EXISTS {} CASCADE;", table_name.to_string()); + + println!("{}", sql); + + let stmt = Statement::from_string(db.get_database_backend(), sql); + + match db.execute(stmt).await { + Ok(_) => println!("Dropped table {}", table_name), + Err(e) => println!("Error dropping table {}: {}", table_name, e), + } +} + +pub async fn drop_tables(db: &DbConn) { + drop_table(db, user::Entity).await; + drop_table(db, permission::Entity).await; + drop_table(db, group::Entity).await; + drop_table(db, user_permission::Entity).await; + drop_table(db, group_permission::Entity).await; + drop_table(db, user_group::Entity).await; +} + +#[tokio::main] +async fn main() { + // Running Entities manually creates the tables from the entities in their latest incarnation. + println!("Connecting to database..."); + let db: DatabaseConnection = + Database::connect("postgresql://miniweb:miniweb@localhost:54321/miniweb") + .await + .unwrap(); + println!("Dropping tables for entities..."); + drop_tables(&db).await; +} diff --git a/entity/src/main.rs b/entity/src/main.rs index af7ce01..b6f2833 100644 --- a/entity/src/main.rs +++ b/entity/src/main.rs @@ -21,7 +21,7 @@ where let stmt = backend.build(&*table_create_statement); match db.execute(stmt).await { - Ok(_) => println!("Migrated {}", entity.table_name()), + Ok(_) => println!("Created {}", entity.table_name()), Err(e) => println!("Error: {}", e), } } diff --git a/entity/src/user.rs b/entity/src/user.rs index 825a743..fa05746 100644 --- a/entity/src/user.rs +++ b/entity/src/user.rs @@ -1,4 +1,5 @@ use sea_orm::entity::prelude::*; +use sea_orm::ActiveValue::{NotSet, Set}; use serde::{Deserialize, Serialize}; use super::group::Entity as Group; @@ -12,7 +13,7 @@ pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] pub id: i64, - #[sea_orm(index = "user_usernames")] + #[sea_orm(index = "user_usernames", unique)] pub username: String, pub password: String, @@ -32,7 +33,16 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} -impl ActiveModelBehavior for ActiveModel {} +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + is_active: Set(true), + is_staff: Set(false), + is_superuser: Set(false), + ..::default() + } + } +} impl Related for Entity { // The final relation is User -> UserGroup -> Group diff --git a/rear/src/auth/models.rs b/rear/src/auth/models.rs index 4a7aa74..058f20a 100644 --- a/rear/src/auth/models.rs +++ b/rear/src/auth/models.rs @@ -2,7 +2,3 @@ pub struct User { pub id: i32, pub username: String, } - -pub struct NewUser<'a> { - pub username: &'a str, -} diff --git a/rear_auth/Cargo.toml b/rear_auth/Cargo.toml index bdf4ff1..dc19078 100644 --- a/rear_auth/Cargo.toml +++ b/rear_auth/Cargo.toml @@ -28,3 +28,4 @@ async-trait = "0.1.80" password-auth = "1.0.0" thiserror = "1.0.61" blake3 = "1.5.1" +chrono = "0.4" diff --git a/rear_auth/src/lib.rs b/rear_auth/src/lib.rs index 63c6f4b..5f8d432 100644 --- a/rear_auth/src/lib.rs +++ b/rear_auth/src/lib.rs @@ -1,3 +1,4 @@ pub mod models; pub mod user_admin_repository; +pub mod utils; pub mod views; diff --git a/rear_auth/src/models.rs b/rear_auth/src/models.rs index 0509cc7..e52aebf 100644 --- a/rear_auth/src/models.rs +++ b/rear_auth/src/models.rs @@ -5,11 +5,16 @@ use axum_login::{AuthUser, AuthzBackend}; use axum_login::{AuthnBackend, UserId}; use log::debug; use password_auth::{generate_hash, is_hash_obsolete, verify_password}; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter}; +use sea_orm::{ + ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, + QueryFilter, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::task; +use crate::utils::get_current_timestamp; + #[derive(Debug, Clone)] pub struct UserRepository { pub(crate) connection: DatabaseConnection, @@ -35,6 +40,17 @@ impl UserRepository { // As checking for obsoleteness errored out, we assume this a raw password. generate_hash(password) } + + pub fn new_user(username: &str, password: &str) -> entity::user::ActiveModel { + entity::user::ActiveModel { + username: sea_orm::ActiveValue::Set(username.to_owned()), + password: sea_orm::ActiveValue::Set(UserRepository::encode_password( + password.to_owned(), + )), + date_joined: sea_orm::ActiveValue::Set(Some(get_current_timestamp())), + ..::default() + } + } } #[derive(Clone, Serialize, Deserialize)] @@ -146,21 +162,23 @@ impl AuthnBackend for UserRepository { .one(&self.connection) .await?; - let user = if let Some(user) = user_found { - let rear_user: AuthenticatedUser = user.into(); - Some(rear_user) - } else { - None - }; + if let Some(user) = user_found { + let given_password = creds.password.clone(); + let user_password = user.password.clone(); + let verified = + task::spawn_blocking(move || verify_password(&given_password, &user_password)) + .await?; - // Verifying the password is blocking and potentially slow, so we'll do so via - // `spawn_blocking`. - task::spawn_blocking(|| { - // We're using password-based authentication--this works by comparing our form - // input with an argon2 password hash. - Ok(user.filter(|user| verify_password(creds.password, &user.password).is_ok())) - }) - .await? + if verified.is_ok() { + let mut db_user: entity::user::ActiveModel = user.into(); + db_user.last_login = ActiveValue::Set(Some(get_current_timestamp())); + let user = db_user.update(&self.connection).await?; + let rear_user: AuthenticatedUser = user.into(); + return Ok(Some(rear_user)); + } + } + + Ok(None) // No user found or verification failed } async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { diff --git a/rear_auth/src/user_admin_repository.rs b/rear_auth/src/user_admin_repository.rs index b242c18..c3add71 100644 --- a/rear_auth/src/user_admin_repository.rs +++ b/rear_auth/src/user_admin_repository.rs @@ -26,6 +26,7 @@ impl AdminRepository for UserRepository { display_list: &["id", "username"], fields: &[ "username", + "password", "first_name", "last_name", "email", @@ -35,7 +36,8 @@ impl AdminRepository for UserRepository { ], // fields_readonly: &["last_login", "date_joined"] } - .into() + .build() + .set_widget("password", Widget::default().as_password()) } async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { @@ -83,14 +85,11 @@ impl AdminRepository for UserRepository { async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option { if let Value::Object(data) = data { let username = data.get("username").unwrap().as_str().unwrap(); + let password = data.get("password").unwrap().as_str().unwrap(); - let mut user = entity::user::ActiveModel { - username: Set(username.to_owned()), - ..Default::default() - }; + let mut user = UserRepository::new_user(username, password); let keys = [ - "password", "first_name", "last_name", "email", @@ -102,16 +101,11 @@ impl AdminRepository for UserRepository { for key in &keys { if let Some(value) = data.get(*key) { match *key { - "password" => { - user.password = Set(UserRepository::encode_password( - value.as_str().unwrap().to_owned(), - )) - } "first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())), "last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())), "email" => user.email = Set(value.as_str().map(|s| s.to_owned())), "is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)), - "is_active" => user.is_active = Set(value.as_bool().unwrap_or(false)), + "is_active" => user.is_active = Set(value.as_bool().unwrap_or(true)), "is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)), _ => (), } diff --git a/rear_auth/src/utils.rs b/rear_auth/src/utils.rs new file mode 100644 index 0000000..8643a97 --- /dev/null +++ b/rear_auth/src/utils.rs @@ -0,0 +1,8 @@ +use sea_orm::prelude::DateTimeWithTimeZone; + +pub fn get_current_timestamp() -> DateTimeWithTimeZone { + let utc_now = chrono::Utc::now(); + // Apply the timezone offset to the UTC time: FixedOffset::east(offset_hours * 3600); + let local_time = utc_now; + local_time.into() +} diff --git a/rear_auth/src/views.rs b/rear_auth/src/views.rs index fc72774..ff4a56b 100644 --- a/rear_auth/src/views.rs +++ b/rear_auth/src/views.rs @@ -70,7 +70,7 @@ mod post { pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse { match auth_session.logout().await { - Ok(_) => Redirect::to("/login").into_response(), + Ok(_) => Redirect::to("/admin/login").into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/src/bin/create_user.rs b/src/bin/create_user.rs index d43c279..dda831f 100644 --- a/src/bin/create_user.rs +++ b/src/bin/create_user.rs @@ -1,5 +1,5 @@ use dotenvy::dotenv; -use entity; +use rear_auth::models::UserRepository; use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set}; use std::env; use std::io::stdin; @@ -11,11 +11,12 @@ pub async fn establish_connection() -> DatabaseConnection { db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } -pub async fn create_user(conn: &DbConn, username: &str) -> Result { - let user = entity::user::ActiveModel { - username: Set(username.to_owned()), - ..Default::default() - }; +pub async fn create_user( + conn: &DbConn, + username: &str, + password: &str, +) -> Result { + let user = UserRepository::new_user(username, password); let user = user.insert(conn).await?; Ok(user) } @@ -25,12 +26,16 @@ async fn main() { let connection = establish_connection().await; let mut username = String::new(); + let mut password = String::new(); println!("Enter username for new user:"); stdin().read_line(&mut username).unwrap(); let username = username.trim_end(); // Remove the trailing newline + println!("Enter password for new user:"); + stdin().read_line(&mut password).unwrap(); + let password = password.trim_end(); // Remove the trailing newline - let user = create_user(&connection, username) + let user = create_user(&connection, username, password) .await .expect("Error creating user"); println!("\nSaved user {} with id {}", username, user.id); diff --git a/src/bin/list_users.rs b/src/bin/list_users.rs index c5989e3..f9df0d7 100644 --- a/src/bin/list_users.rs +++ b/src/bin/list_users.rs @@ -21,7 +21,7 @@ async fn main() { println!("Displaying {} users", results.len()); for user in results { - println!("{}", user.username); + println!("{} - {}", user.username, user.password); } println!("-----------\n"); } diff --git a/templates/admin/items/item_change.jinja b/templates/admin/items/item_change.jinja index d48f730..570f47c 100644 --- a/templates/admin/items/item_change.jinja +++ b/templates/admin/items/item_change.jinja @@ -4,7 +4,7 @@

Update {{item_model.name}} in {{item_info.name}}

{% set fields = item_info.fields %} - {% set form = { 'action': item.change_url } %} + {% set form = { 'action': item.change_url, 'method': 'PATCH' } %} {% include "/admin/items/item_change_form.jinja" %}
{% endblock content %} \ No newline at end of file diff --git a/templates/admin/login.html b/templates/admin/login.html index 0867306..d897c96 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -24,11 +24,11 @@ User login

- +

- +