wip: implementing auth further
This commit is contained in:
parent
e19d5db7c6
commit
b029b4b975
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -143,6 +143,12 @@ dependencies = [
|
|||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@ -490,6 +496,23 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-messages"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0d06050ddcb05eaaffe44c5f73443ea936eadae6a8332dd47b8c8c8cb1af98a"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core 0.4.3",
|
||||||
|
"http 1.0.0",
|
||||||
|
"parking_lot",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tower",
|
||||||
|
"tower-sessions-core",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
@ -576,6 +599,19 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake3"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"arrayvec",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"constant_time_eq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -772,6 +808,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@ -2431,6 +2473,8 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum 0.7.4",
|
"axum 0.7.4",
|
||||||
"axum-login",
|
"axum-login",
|
||||||
|
"axum-messages",
|
||||||
|
"blake3",
|
||||||
"entity",
|
"entity",
|
||||||
"log",
|
"log",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
|
@ -23,6 +23,8 @@ sea-orm = { version = "0.12.10", features = [
|
|||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
] }
|
] }
|
||||||
axum-login = "0.15.3"
|
axum-login = "0.15.3"
|
||||||
|
axum-messages = "0.6.1"
|
||||||
async-trait = "0.1.80"
|
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"
|
||||||
|
2
rear_auth/roadmap.md
Normal file
2
rear_auth/roadmap.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Atm. it is not clear to me if rear_auth will really be it's own package, as it needs to interact a lot with rear, or if I split up rear into modules instead.
|
||||||
|
|
@ -1 +1,3 @@
|
|||||||
pub mod users;
|
pub mod models;
|
||||||
|
pub mod user_admin_repository;
|
||||||
|
pub mod views;
|
||||||
|
@ -1,3 +1 @@
|
|||||||
mod users;
|
|
||||||
|
|
||||||
pub fn main() {}
|
pub fn main() {}
|
||||||
|
230
rear_auth/src/models.rs
Normal file
230
rear_auth/src/models.rs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
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 serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserRepository {
|
||||||
|
pub(crate) connection: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserRepository {
|
||||||
|
pub fn new(connection: DatabaseConnection) -> Self {
|
||||||
|
UserRepository {
|
||||||
|
connection: connection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode_password(password: String) -> String {
|
||||||
|
// This function will try to avoid re-encoding an encoded password.
|
||||||
|
// This is why it is not public outside of the crate.
|
||||||
|
if let Ok(_is_obsolete) = is_hash_obsolete(password.as_str()) {
|
||||||
|
// Not sure what to do if it is obsolete.
|
||||||
|
if _is_obsolete {
|
||||||
|
debug!("UserRepository::encode_password: found obsolete password hash.");
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
// As checking for obsoleteness errored out, we assume this a raw password.
|
||||||
|
generate_hash(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AuthenticatedUser {
|
||||||
|
id: i64,
|
||||||
|
pub username: String,
|
||||||
|
password: String,
|
||||||
|
session_token: Vec<u8>, // Stores the hash
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthenticatedUser {
|
||||||
|
fn new(id: i64, username: String, password: String) -> Self {
|
||||||
|
AuthenticatedUser {
|
||||||
|
id: id,
|
||||||
|
username: username,
|
||||||
|
session_token: blake3::hash(&password.as_bytes()).as_bytes().to_vec(),
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_password(&mut self, new_password: String) {
|
||||||
|
self.password = new_password;
|
||||||
|
self.session_token = blake3::hash(self.password.as_bytes()).as_bytes().to_vec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we've implemented `Debug` manually to avoid accidentally logging the
|
||||||
|
// password hash.
|
||||||
|
impl std::fmt::Debug for AuthenticatedUser {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("User")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("username", &self.username)
|
||||||
|
.field("password", &"[redacted]")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthUser for AuthenticatedUser {
|
||||||
|
type Id = i64;
|
||||||
|
|
||||||
|
fn id(&self) -> Self::Id {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_auth_hash(&self) -> &[u8] {
|
||||||
|
&self.session_token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<entity::user::Model> for AuthenticatedUser {
|
||||||
|
fn from(value: entity::user::Model) -> Self {
|
||||||
|
AuthenticatedUser::new(value.id, value.username, value.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This allows us to extract the authentication fields from forms. We use this
|
||||||
|
// to authenticate requests with the backend.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Credentials {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub struct NotFoundError {
|
||||||
|
pub details: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotFoundError {
|
||||||
|
pub fn new(details: &str) -> Self {
|
||||||
|
NotFoundError {
|
||||||
|
details: details.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for NotFoundError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Not Found... Error: {}", self.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
DbErr(#[from] sea_orm::DbErr),
|
||||||
|
|
||||||
|
#[error("Not Found: {0}")]
|
||||||
|
NotFound(#[from] NotFoundError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
TaskJoin(#[from] task::JoinError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthnBackend for UserRepository {
|
||||||
|
type User = AuthenticatedUser;
|
||||||
|
type Credentials = Credentials;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
async fn authenticate(
|
||||||
|
&self,
|
||||||
|
creds: Self::Credentials,
|
||||||
|
) -> Result<Option<Self::User>, Self::Error> {
|
||||||
|
let user_found = entity::User::find()
|
||||||
|
.filter(entity::user::Column::Username.eq(creds.username))
|
||||||
|
.one(&self.connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let user = if let Some(user) = user_found {
|
||||||
|
let rear_user: AuthenticatedUser = user.into();
|
||||||
|
Some(rear_user)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||||
|
let user_found = entity::User::find_by_id(*user_id)
|
||||||
|
.one(&self.connection)
|
||||||
|
.await?;
|
||||||
|
if let Some(user) = user_found {
|
||||||
|
let rear_user: AuthenticatedUser = user.into();
|
||||||
|
Ok(Some(rear_user))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct Permission {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Permission {
|
||||||
|
fn from(name: &str) -> Self {
|
||||||
|
Permission {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<entity::permission::Model> for Permission {
|
||||||
|
fn from(model: entity::permission::Model) -> Self {
|
||||||
|
Permission {
|
||||||
|
name: model.codename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthzBackend for UserRepository {
|
||||||
|
type Permission = Permission;
|
||||||
|
|
||||||
|
async fn get_group_permissions(
|
||||||
|
&self,
|
||||||
|
user: &Self::User,
|
||||||
|
) -> Result<HashSet<Self::Permission>, Self::Error> {
|
||||||
|
let user = entity::User::find_by_id(user.id)
|
||||||
|
.one(&self.connection)
|
||||||
|
.await?;
|
||||||
|
if let Some(user) = user {
|
||||||
|
let permissions = user
|
||||||
|
.find_related(entity::Permission)
|
||||||
|
.all(&self.connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(permissions
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| Permission::from(item))
|
||||||
|
.collect())
|
||||||
|
} else {
|
||||||
|
Ok(HashSet::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a type alias for convenience.
|
||||||
|
//
|
||||||
|
// Note that we've supplied our concrete backend here.
|
||||||
|
pub type AuthSession = axum_login::AuthSession<UserRepository>;
|
@ -1,21 +1,11 @@
|
|||||||
use crate::admin::domain::*;
|
|
||||||
use crate::admin::state::AdminRegistry;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use rear::admin::domain::*;
|
||||||
|
use rear::admin::state::AdminRegistry;
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
struct UserRepository {
|
use crate::models::UserRepository;
|
||||||
connection: DatabaseConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserRepository {
|
|
||||||
pub fn new(connection: DatabaseConnection) -> Self {
|
|
||||||
UserRepository {
|
|
||||||
connection: connection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AdminRepository for UserRepository {
|
impl AdminRepository for UserRepository {
|
||||||
@ -91,41 +81,47 @@ 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> {
|
||||||
let username = data.get("username").unwrap().as_str().unwrap();
|
if let Value::Object(data) = data {
|
||||||
|
let username = data.get("username").unwrap().as_str().unwrap();
|
||||||
|
|
||||||
let mut user = entity::user::ActiveModel {
|
let mut user = entity::user::ActiveModel {
|
||||||
username: Set(username.to_owned()),
|
username: Set(username.to_owned()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys = [
|
let keys = [
|
||||||
"password",
|
"password",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
"is_staff",
|
"is_staff",
|
||||||
"is_active",
|
"is_active",
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
];
|
];
|
||||||
|
|
||||||
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(value.as_str().unwrap().to_owned()),
|
"password" => {
|
||||||
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
user.password = Set(UserRepository::encode_password(
|
||||||
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
value.as_str().unwrap().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)),
|
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
||||||
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
"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_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(user) = user.insert(&self.connection).await {
|
if let Ok(user) = user.insert(&self.connection).await {
|
||||||
let id = user.id.to_string();
|
let id = user.id.to_string();
|
||||||
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
@ -1,154 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use axum_login::{AuthUser, AuthnBackend, UserId};
|
|
||||||
use password_auth::verify_password;
|
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::task;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UserDatabase {
|
|
||||||
connection: DatabaseConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserDatabase {
|
|
||||||
pub fn new(connection: DatabaseConnection) -> Self {
|
|
||||||
UserDatabase {
|
|
||||||
connection: connection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct User {
|
|
||||||
id: i64,
|
|
||||||
pub username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we've implemented `Debug` manually to avoid accidentally logging the
|
|
||||||
// password hash.
|
|
||||||
impl std::fmt::Debug for User {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("User")
|
|
||||||
.field("id", &self.id)
|
|
||||||
.field("username", &self.username)
|
|
||||||
.field("password", &"[redacted]")
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthUser for User {
|
|
||||||
type Id = i64;
|
|
||||||
|
|
||||||
fn id(&self) -> Self::Id {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn session_auth_hash(&self) -> &[u8] {
|
|
||||||
self.password.as_bytes() // We use the password hash as the auth
|
|
||||||
// hash--what this means
|
|
||||||
// is when the user changes their password the
|
|
||||||
// auth session becomes invalid.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<entity::user::Model> for User {
|
|
||||||
fn from(value: entity::user::Model) -> Self {
|
|
||||||
User {
|
|
||||||
id: value.id,
|
|
||||||
username: value.username,
|
|
||||||
password: "".to_owned(), // TODO: password field.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This allows us to extract the authentication fields from forms. We use this
|
|
||||||
// to authenticate requests with the backend.
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Credentials {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub next: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub struct NotFoundError {
|
|
||||||
pub details: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NotFoundError {
|
|
||||||
pub fn new(details: &str) -> Self {
|
|
||||||
NotFoundError {
|
|
||||||
details: details.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for NotFoundError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Not Found... Error: {}", self.details)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error(transparent)]
|
|
||||||
DbErr(#[from] sea_orm::DbErr),
|
|
||||||
|
|
||||||
#[error("Not Found: {0}")]
|
|
||||||
NotFound(#[from] NotFoundError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
TaskJoin(#[from] task::JoinError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AuthnBackend for UserDatabase {
|
|
||||||
type User = User;
|
|
||||||
type Credentials = Credentials;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
async fn authenticate(
|
|
||||||
&self,
|
|
||||||
creds: Self::Credentials,
|
|
||||||
) -> Result<Option<Self::User>, Self::Error> {
|
|
||||||
let user_found = entity::User::find()
|
|
||||||
.filter(entity::user::Column::Username.eq(creds.username))
|
|
||||||
.one(&self.connection)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let user = if let Some(user) = user_found {
|
|
||||||
let rear_user: User = user.into();
|
|
||||||
Some(rear_user)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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?
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
|
||||||
let user_found = entity::User::find_by_id(*user_id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await?;
|
|
||||||
if let Some(user) = user_found {
|
|
||||||
let rear_user: User = user.into();
|
|
||||||
Ok(Some(rear_user))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use a type alias for convenience.
|
|
||||||
//
|
|
||||||
// Note that we've supplied our concrete backend here.
|
|
||||||
pub type AuthSession = axum_login::AuthSession<UserDatabase>;
|
|
96
rear_auth/src/views.rs
Normal file
96
rear_auth/src/views.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::Query,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
routing::{get, post},
|
||||||
|
Form, Router,
|
||||||
|
};
|
||||||
|
use axum_messages::{Message, Messages};
|
||||||
|
use rear::service::templates::Templates;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::models::{AuthSession, Credentials};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LoginTemplate {
|
||||||
|
messages: Vec<Message>,
|
||||||
|
next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This allows us to extract the "next" field from the query string. We use this
|
||||||
|
// to redirect after log in.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NextUrl {
|
||||||
|
next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router() -> Router<Templates> {
|
||||||
|
Router::new()
|
||||||
|
.route("/login", post(self::post::login))
|
||||||
|
.route("/login", get(self::get::login))
|
||||||
|
.route("/logout", get(self::get::logout))
|
||||||
|
}
|
||||||
|
|
||||||
|
mod post {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
mut auth_session: AuthSession,
|
||||||
|
messages: Messages,
|
||||||
|
Form(creds): Form<Credentials>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let user = match auth_session.authenticate(creds.clone()).await {
|
||||||
|
Ok(Some(user)) => user,
|
||||||
|
Ok(None) => {
|
||||||
|
messages.error("Invalid credentials");
|
||||||
|
|
||||||
|
let mut login_url = "/login".to_string();
|
||||||
|
if let Some(next) = creds.next {
|
||||||
|
login_url = format!("{}?next={}", login_url, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Redirect::to(&login_url).into_response();
|
||||||
|
}
|
||||||
|
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if auth_session.login(&user).await.is_err() {
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.success(format!("Successfully logged in as {}", user.username));
|
||||||
|
|
||||||
|
if let Some(ref next) = creds.next {
|
||||||
|
Redirect::to(next)
|
||||||
|
} else {
|
||||||
|
Redirect::to("/")
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod get {
|
||||||
|
use super::*;
|
||||||
|
use axum::extract::State;
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
messages: Messages,
|
||||||
|
templates: State<Templates>,
|
||||||
|
Query(NextUrl { next }): Query<NextUrl>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this was taken from the axum_login examples and modified. Redirection might need reverse-routing support.
|
@ -1,4 +1,3 @@
|
|||||||
pub mod empty_repository;
|
pub mod empty_repository;
|
||||||
|
pub mod file_repository;
|
||||||
pub mod static_repository;
|
pub mod static_repository;
|
||||||
pub mod user_repository;
|
|
||||||
pub mod file_repository;
|
|
@ -5,9 +5,8 @@ mod howto;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use admin_examples::static_repository;
|
|
||||||
use admin_examples::user_repository;
|
|
||||||
use admin_examples::file_repository;
|
use admin_examples::file_repository;
|
||||||
|
use admin_examples::static_repository;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::MatchedPath,
|
extract::MatchedPath,
|
||||||
@ -53,7 +52,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Register Admin Apps
|
// Register Admin Apps
|
||||||
static_repository::register(&mut admin);
|
static_repository::register(&mut admin);
|
||||||
user_repository::register(&mut admin, db_connection);
|
//user_repository::register(&mut admin, db_connection);
|
||||||
file_repository::register(&mut admin, "static/admin");
|
file_repository::register(&mut admin, "static/admin");
|
||||||
|
|
||||||
// Create global Application State.
|
// Create global Application State.
|
||||||
|
Loading…
Reference in New Issue
Block a user