wip: implementing auth further
This commit is contained in:
@@ -23,6 +23,8 @@ sea-orm = { version = "0.12.10", features = [
|
||||
"sqlx-postgres",
|
||||
] }
|
||||
axum-login = "0.15.3"
|
||||
axum-messages = "0.6.1"
|
||||
async-trait = "0.1.80"
|
||||
password-auth = "1.0.0"
|
||||
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() {}
|
||||
|
||||
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>;
|
||||
198
rear_auth/src/user_admin_repository.rs
Normal file
198
rear_auth/src/user_admin_repository.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
use rear::admin::domain::*;
|
||||
use rear::admin::state::AdminRegistry;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::models::UserRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for UserRepository {
|
||||
type Key = i64;
|
||||
|
||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||
if let Ok(i) = s.parse::<i64>() {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||
RepoInfo {
|
||||
name: "User",
|
||||
lookup_key: "id",
|
||||
display_list: &["id", "username"],
|
||||
fields: &[
|
||||
"username",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
],
|
||||
// fields_readonly: &["last_login", "date_joined"]
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||
let results = if let Ok(results) = entity::User::find().all(&self.connection).await {
|
||||
results
|
||||
} else {
|
||||
return RepositoryList::Empty;
|
||||
};
|
||||
let repository_items: Vec<RepositoryItem> = results
|
||||
.into_iter()
|
||||
.filter_map(|item| match serde_json::to_value(&item) {
|
||||
Ok(fields) => {
|
||||
let id = item.id.to_string();
|
||||
Some(model.build_item(&*id, fields))
|
||||
}
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect();
|
||||
RepositoryList::List {
|
||||
values: repository_items,
|
||||
}
|
||||
}
|
||||
|
||||
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 mut user = entity::user::ActiveModel {
|
||||
username: Set(username.to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let keys = [
|
||||
"password",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
];
|
||||
|
||||
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_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
model: &RepositoryContext,
|
||||
id: &Self::Key,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem> {
|
||||
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();
|
||||
|
||||
// change values
|
||||
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());
|
||||
}
|
||||
|
||||
// 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()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn replace(
|
||||
&mut self,
|
||||
model: &RepositoryContext,
|
||||
id: &Self::Key,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem> {
|
||||
self.update(model, id, data).await
|
||||
}
|
||||
|
||||
async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option<Value> {
|
||||
let id: i32 = *id as i32;
|
||||
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(user) = user {
|
||||
let delete_result = user.delete(&self.connection).await.unwrap();
|
||||
debug!("deleted rows: {}", delete_result.rows_affected);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) {
|
||||
let app_key = registry.register_app("Auth");
|
||||
let repo = UserRepository::new(db);
|
||||
let model_config = AdminModelConfig {
|
||||
app_key: app_key,
|
||||
name: "User".to_owned(),
|
||||
};
|
||||
let model_result = registry.register_model(model_config, repo);
|
||||
match model_result {
|
||||
Err(err) => panic!("{}", err),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user