wip: login, auth working, still need refactoring

This commit is contained in:
Gabor Körber 2024-07-16 07:27:26 +02:00
parent c78e386645
commit 402585f968
16 changed files with 127 additions and 51 deletions

1
Cargo.lock generated
View File

@ -2478,6 +2478,7 @@ dependencies = [
"axum-login",
"axum-messages",
"blake3",
"chrono",
"entity",
"log",
"password-auth",

View File

@ -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",
]
features = ["runtime-tokio-native-tls", "sqlx-postgres"]

View File

@ -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<E>(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;
}

View File

@ -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),
}
}

View File

@ -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),
..<Self as ActiveModelTrait>::default()
}
}
}
impl Related<Group> for Entity {
// The final relation is User -> UserGroup -> Group

View File

@ -2,7 +2,3 @@ pub struct User {
pub id: i32,
pub username: String,
}
pub struct NewUser<'a> {
pub username: &'a str,
}

View File

@ -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"

View File

@ -1,3 +1,4 @@
pub mod models;
pub mod user_admin_repository;
pub mod utils;
pub mod views;

View File

@ -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())),
..<entity::user::ActiveModel as ActiveModelTrait>::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<Self>) -> Result<Option<Self::User>, Self::Error> {

View File

@ -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<RepositoryItem> {
@ -83,14 +85,11 @@ impl AdminRepository for UserRepository {
async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
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)),
_ => (),
}

8
rear_auth/src/utils.rs Normal file
View File

@ -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()
}

View File

@ -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(),
}
}

View File

@ -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<entity::user::Model, DbErr> {
let user = entity::user::ActiveModel {
username: Set(username.to_owned()),
..Default::default()
};
pub async fn create_user(
conn: &DbConn,
username: &str,
password: &str,
) -> Result<entity::user::Model, DbErr> {
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);

View File

@ -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");
}

View File

@ -4,7 +4,7 @@
<div class="ui container">
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
{% 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" %}
</div>
{% endblock content %}

View File

@ -24,11 +24,11 @@
<legend>User login</legend>
<p>
<label for="username">Username</label>
<input name="username" id="username" value="ferris" />
<input name="username" id="username" value="admin" />
</p>
<p>
<label for="password">Password</label>
<input name="password" id="password" type="password" value="hunter42" />
<input name="password" id="password" type="password" value="admin" />
</p>
</fieldset>