wip: login, auth working, still need refactoring
This commit is contained in:
parent
c78e386645
commit
402585f968
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2478,6 +2478,7 @@ dependencies = [
|
|||||||
"axum-login",
|
"axum-login",
|
||||||
"axum-messages",
|
"axum-messages",
|
||||||
"blake3",
|
"blake3",
|
||||||
|
"chrono",
|
||||||
"entity",
|
"entity",
|
||||||
"log",
|
"log",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
|
@ -3,18 +3,17 @@ name = "entity"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
default-run = "entity"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "entity"
|
name = "entity"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12.10" # sea-orm version
|
version = "0.12.10" # sea-orm version
|
||||||
features = [
|
features = ["runtime-tokio-native-tls", "sqlx-postgres"]
|
||||||
"runtime-tokio-native-tls",
|
|
||||||
"sqlx-postgres",
|
|
||||||
]
|
|
||||||
|
43
entity/src/bin/drop_tables.rs
Normal file
43
entity/src/bin/drop_tables.rs
Normal 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;
|
||||||
|
}
|
@ -21,7 +21,7 @@ where
|
|||||||
let stmt = backend.build(&*table_create_statement);
|
let stmt = backend.build(&*table_create_statement);
|
||||||
|
|
||||||
match db.execute(stmt).await {
|
match db.execute(stmt).await {
|
||||||
Ok(_) => println!("Migrated {}", entity.table_name()),
|
Ok(_) => println!("Created {}", entity.table_name()),
|
||||||
Err(e) => println!("Error: {}", e),
|
Err(e) => println!("Error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::ActiveValue::{NotSet, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::group::Entity as Group;
|
use super::group::Entity as Group;
|
||||||
@ -12,7 +13,7 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[sea_orm(index = "user_usernames")]
|
#[sea_orm(index = "user_usernames", unique)]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
@ -32,7 +33,16 @@ pub struct Model {
|
|||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
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 {
|
impl Related<Group> for Entity {
|
||||||
// The final relation is User -> UserGroup -> Group
|
// The final relation is User -> UserGroup -> Group
|
||||||
|
@ -2,7 +2,3 @@ pub struct User {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NewUser<'a> {
|
|
||||||
pub username: &'a str,
|
|
||||||
}
|
|
||||||
|
@ -28,3 +28,4 @@ async-trait = "0.1.80"
|
|||||||
password-auth = "1.0.0"
|
password-auth = "1.0.0"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
blake3 = "1.5.1"
|
blake3 = "1.5.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod user_admin_repository;
|
pub mod user_admin_repository;
|
||||||
|
pub mod utils;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
@ -5,11 +5,16 @@ use axum_login::{AuthUser, AuthzBackend};
|
|||||||
use axum_login::{AuthnBackend, UserId};
|
use axum_login::{AuthnBackend, UserId};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use password_auth::{generate_hash, is_hash_obsolete, verify_password};
|
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 serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::utils::get_current_timestamp;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UserRepository {
|
pub struct UserRepository {
|
||||||
pub(crate) connection: DatabaseConnection,
|
pub(crate) connection: DatabaseConnection,
|
||||||
@ -35,6 +40,17 @@ impl UserRepository {
|
|||||||
// As checking for obsoleteness errored out, we assume this a raw password.
|
// As checking for obsoleteness errored out, we assume this a raw password.
|
||||||
generate_hash(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)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@ -146,21 +162,23 @@ impl AuthnBackend for UserRepository {
|
|||||||
.one(&self.connection)
|
.one(&self.connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user = if let Some(user) = user_found {
|
if let Some(user) = user_found {
|
||||||
let rear_user: AuthenticatedUser = user.into();
|
let given_password = creds.password.clone();
|
||||||
Some(rear_user)
|
let user_password = user.password.clone();
|
||||||
} else {
|
let verified =
|
||||||
None
|
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
|
if verified.is_ok() {
|
||||||
// `spawn_blocking`.
|
let mut db_user: entity::user::ActiveModel = user.into();
|
||||||
task::spawn_blocking(|| {
|
db_user.last_login = ActiveValue::Set(Some(get_current_timestamp()));
|
||||||
// We're using password-based authentication--this works by comparing our form
|
let user = db_user.update(&self.connection).await?;
|
||||||
// input with an argon2 password hash.
|
let rear_user: AuthenticatedUser = user.into();
|
||||||
Ok(user.filter(|user| verify_password(creds.password, &user.password).is_ok()))
|
return Ok(Some(rear_user));
|
||||||
})
|
}
|
||||||
.await?
|
}
|
||||||
|
|
||||||
|
Ok(None) // No user found or verification failed
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||||
|
@ -26,6 +26,7 @@ impl AdminRepository for UserRepository {
|
|||||||
display_list: &["id", "username"],
|
display_list: &["id", "username"],
|
||||||
fields: &[
|
fields: &[
|
||||||
"username",
|
"username",
|
||||||
|
"password",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
@ -35,7 +36,8 @@ impl AdminRepository for UserRepository {
|
|||||||
],
|
],
|
||||||
// fields_readonly: &["last_login", "date_joined"]
|
// 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> {
|
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> {
|
async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
|
||||||
if let Value::Object(data) = data {
|
if let Value::Object(data) = data {
|
||||||
let username = data.get("username").unwrap().as_str().unwrap();
|
let username = data.get("username").unwrap().as_str().unwrap();
|
||||||
|
let password = data.get("password").unwrap().as_str().unwrap();
|
||||||
|
|
||||||
let mut user = entity::user::ActiveModel {
|
let mut user = UserRepository::new_user(username, password);
|
||||||
username: Set(username.to_owned()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = [
|
let keys = [
|
||||||
"password",
|
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
@ -102,16 +101,11 @@ impl AdminRepository for UserRepository {
|
|||||||
for key in &keys {
|
for key in &keys {
|
||||||
if let Some(value) = data.get(*key) {
|
if let Some(value) = data.get(*key) {
|
||||||
match *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())),
|
"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())),
|
"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())),
|
"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_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)),
|
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
8
rear_auth/src/utils.rs
Normal file
8
rear_auth/src/utils.rs
Normal 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()
|
||||||
|
}
|
@ -70,7 +70,7 @@ mod post {
|
|||||||
|
|
||||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||||
match auth_session.logout().await {
|
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(),
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use entity;
|
use rear_auth::models::UserRepository;
|
||||||
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::stdin;
|
use std::io::stdin;
|
||||||
@ -11,11 +11,12 @@ pub async fn establish_connection() -> DatabaseConnection {
|
|||||||
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(conn: &DbConn, username: &str) -> Result<entity::user::Model, DbErr> {
|
pub async fn create_user(
|
||||||
let user = entity::user::ActiveModel {
|
conn: &DbConn,
|
||||||
username: Set(username.to_owned()),
|
username: &str,
|
||||||
..Default::default()
|
password: &str,
|
||||||
};
|
) -> Result<entity::user::Model, DbErr> {
|
||||||
|
let user = UserRepository::new_user(username, password);
|
||||||
let user = user.insert(conn).await?;
|
let user = user.insert(conn).await?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
@ -25,12 +26,16 @@ async fn main() {
|
|||||||
let connection = establish_connection().await;
|
let connection = establish_connection().await;
|
||||||
|
|
||||||
let mut username = String::new();
|
let mut username = String::new();
|
||||||
|
let mut password = String::new();
|
||||||
|
|
||||||
println!("Enter username for new user:");
|
println!("Enter username for new user:");
|
||||||
stdin().read_line(&mut username).unwrap();
|
stdin().read_line(&mut username).unwrap();
|
||||||
let username = username.trim_end(); // Remove the trailing newline
|
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
|
.await
|
||||||
.expect("Error creating user");
|
.expect("Error creating user");
|
||||||
println!("\nSaved user {} with id {}", username, user.id);
|
println!("\nSaved user {} with id {}", username, user.id);
|
||||||
|
@ -21,7 +21,7 @@ async fn main() {
|
|||||||
|
|
||||||
println!("Displaying {} users", results.len());
|
println!("Displaying {} users", results.len());
|
||||||
for user in results {
|
for user in results {
|
||||||
println!("{}", user.username);
|
println!("{} - {}", user.username, user.password);
|
||||||
}
|
}
|
||||||
println!("-----------\n");
|
println!("-----------\n");
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
||||||
{% set fields = item_info.fields %}
|
{% 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" %}
|
{% include "/admin/items/item_change_form.jinja" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -24,11 +24,11 @@
|
|||||||
<legend>User login</legend>
|
<legend>User login</legend>
|
||||||
<p>
|
<p>
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input name="username" id="username" value="ferris" />
|
<input name="username" id="username" value="admin" />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input name="password" id="password" type="password" value="hunter42" />
|
<input name="password" id="password" type="password" value="admin" />
|
||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user