Merge remote-tracking branch 'refs/remotes/origin/rear_auth' into rear_auth
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod models;
|
||||
pub mod user_admin_repository;
|
||||
pub mod utils;
|
||||
pub mod views;
|
||||
|
||||
+33
-15
@@ -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> {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
use rear::admin::domain::*;
|
||||
use rear::admin::state::AdminRegistry;
|
||||
use log::{debug, warn};
|
||||
use rear::depot::prelude::*;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::models::UserRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for UserRepository {
|
||||
impl DepotRepository for UserRepository {
|
||||
type Key = i64;
|
||||
|
||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||
@@ -26,6 +25,7 @@ impl AdminRepository for UserRepository {
|
||||
display_list: &["id", "username"],
|
||||
fields: &[
|
||||
"username",
|
||||
"password",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
@@ -35,28 +35,35 @@ impl AdminRepository for UserRepository {
|
||||
],
|
||||
// fields_readonly: &["last_login", "date_joined"]
|
||||
}
|
||||
.into()
|
||||
.build()
|
||||
.set_widget(
|
||||
"password",
|
||||
Widget::widget("/depot/widgets/password_change.jinja").as_password(),
|
||||
)
|
||||
.set_widget("is_staff", Widget::checkbox())
|
||||
.set_widget("is_active", Widget::checkbox())
|
||||
.set_widget("is_superuser", Widget::checkbox())
|
||||
}
|
||||
|
||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
||||
let id: i32 = *id as i32; // use try_into() instead.
|
||||
let get_user = entity::User::find_by_id(id).one(&self.connection).await;
|
||||
match get_user {
|
||||
Ok(get_user) => {
|
||||
if let Some(user) = get_user {
|
||||
let id = user.id.to_string();
|
||||
match serde_json::to_value(&user) {
|
||||
Ok(item) => {
|
||||
return Some(model.build_item(&*id, item));
|
||||
}
|
||||
Err(_) => return None,
|
||||
if let Ok(get_user) = get_user {
|
||||
if let Some(user) = get_user {
|
||||
let id = user.id.to_string();
|
||||
match serde_json::to_value(&user) {
|
||||
Ok(item) => {
|
||||
return Ok(RepositoryResponse::ItemOnly(model.build_item(&*id, item)));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(RepositoryError::UnknownError(
|
||||
"JSON Error creating value".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
None
|
||||
Ok(RepositoryResponse::NoItem)
|
||||
}
|
||||
|
||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||
@@ -80,17 +87,14 @@ impl AdminRepository for UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
|
||||
async fn create(&mut self, model: &RepositoryContext, data: Value) -> RepositoryResult {
|
||||
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 +106,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)),
|
||||
_ => (),
|
||||
}
|
||||
@@ -120,10 +119,11 @@ impl AdminRepository for UserRepository {
|
||||
|
||||
if let Ok(user) = user.insert(&self.connection).await {
|
||||
let id = user.id.to_string();
|
||||
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
||||
let item = model.build_item(&*id, serde_json::to_value(&user).unwrap());
|
||||
return Ok(RepositoryResponse::ItemOnly(item));
|
||||
}
|
||||
}
|
||||
None
|
||||
Ok(RepositoryResponse::NoItem)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
@@ -131,32 +131,57 @@ impl AdminRepository for UserRepository {
|
||||
model: &RepositoryContext,
|
||||
id: &Self::Key,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem> {
|
||||
) -> RepositoryResult {
|
||||
let id: i32 = *id as i32;
|
||||
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut user: entity::user::ActiveModel = user.unwrap().into();
|
||||
.map_err(|e| RepositoryError::DatabaseError(Box::new(e)))?;
|
||||
let mut user: entity::user::ActiveModel = user.ok_or(RepositoryError::ItemNotFound)?.into();
|
||||
|
||||
// change values
|
||||
// should we really allow username change?
|
||||
if let Some(value) = data.get("username") {
|
||||
if let Some(value) = value.as_str() {
|
||||
user.username = Set(value.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = data.get("password") {
|
||||
user.password = Set(value.as_str().unwrap().to_owned());
|
||||
let keys = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
];
|
||||
|
||||
for key in &keys {
|
||||
if let Some(value) = data.get(*key) {
|
||||
match *key {
|
||||
"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(true)),
|
||||
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update
|
||||
if let Ok(user) = user.update(&self.connection).await {
|
||||
let id = user.id.to_string();
|
||||
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
||||
match user.update(&self.connection).await {
|
||||
Ok(user) => {
|
||||
let id = user.id.to_string();
|
||||
return Ok(RepositoryResponse::ItemOnly(
|
||||
model.build_item(&*id, serde_json::to_value(&user).unwrap()),
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error updating user");
|
||||
return Err(RepositoryError::DatabaseError(Box::new(err)));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn replace(
|
||||
@@ -164,7 +189,7 @@ impl AdminRepository for UserRepository {
|
||||
model: &RepositoryContext,
|
||||
id: &Self::Key,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem> {
|
||||
) -> RepositoryResult {
|
||||
self.update(model, id, data).await
|
||||
}
|
||||
|
||||
@@ -176,6 +201,7 @@ impl AdminRepository for UserRepository {
|
||||
.unwrap();
|
||||
if let Some(user) = user {
|
||||
let delete_result = user.delete(&self.connection).await.unwrap();
|
||||
// .ok_or(RepositoryError::DatabaseError(Box::new(err)))?;
|
||||
debug!("deleted rows: {}", delete_result.rows_affected);
|
||||
}
|
||||
|
||||
@@ -183,16 +209,16 @@ impl AdminRepository for UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) {
|
||||
let app_key = registry.register_app("Auth");
|
||||
pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
|
||||
let section_key = registry.register_section("Auth");
|
||||
let repo = UserRepository::new(db);
|
||||
let model_config = AdminModelConfig {
|
||||
app_key: app_key,
|
||||
let model_config = DepotModelConfig {
|
||||
section_key: section_key,
|
||||
name: "User".to_owned(),
|
||||
};
|
||||
let model_result = registry.register_model(model_config, repo);
|
||||
let model_result = registry.register_model(model_config, repo.clone());
|
||||
match model_result {
|
||||
Err(err) => panic!("{}", err),
|
||||
_ => (),
|
||||
_ => repo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+16
-15
@@ -6,7 +6,6 @@ use axum::{
|
||||
Form, Router,
|
||||
};
|
||||
use axum_messages::{Message, Messages};
|
||||
use rear::service::templates::Templates;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{AuthSession, Credentials};
|
||||
@@ -24,11 +23,12 @@ pub struct NextUrl {
|
||||
next: Option<String>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Templates> {
|
||||
pub fn routes<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>() -> Router<S>
|
||||
where {
|
||||
Router::new()
|
||||
.route("/login", post(self::post::login))
|
||||
.route("/login", get(self::get::login))
|
||||
.route("/logout", get(self::get::logout))
|
||||
.route("/login", get(self::get::login::<S>))
|
||||
.route("/logout", post(self::post::logout))
|
||||
}
|
||||
|
||||
mod post {
|
||||
@@ -44,7 +44,7 @@ mod post {
|
||||
Ok(None) => {
|
||||
messages.error("Invalid credentials");
|
||||
|
||||
let mut login_url = "/login".to_string();
|
||||
let mut login_url = "/depot/login".to_string();
|
||||
if let Some(next) = creds.next {
|
||||
login_url = format!("{}?next={}", login_url, next);
|
||||
};
|
||||
@@ -67,29 +67,30 @@ mod post {
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||
match auth_session.logout().await {
|
||||
Ok(_) => Redirect::to("/depot/login").into_response(),
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod get {
|
||||
use super::*;
|
||||
use axum::extract::State;
|
||||
|
||||
pub async fn login(
|
||||
pub async fn login<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>(
|
||||
messages: Messages,
|
||||
templates: State<Templates>,
|
||||
admin: State<S>,
|
||||
Query(NextUrl { next }): Query<NextUrl>,
|
||||
) -> impl IntoResponse {
|
||||
let templates = admin.get_templates();
|
||||
let context = LoginTemplate {
|
||||
messages: messages.into_iter().collect(),
|
||||
next,
|
||||
};
|
||||
templates.render_html("login.html", context)
|
||||
}
|
||||
|
||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
||||
match auth_session.logout().await {
|
||||
Ok(_) => Redirect::to("/login").into_response(),
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
templates.render_html("depot/login.html", context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user