Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use self.pool in impl of UserStore<CustomRole> for PostgresStore<CustomUser> #26

Closed
sean-clarke opened this issue Dec 7, 2022 · 6 comments

Comments

@sean-clarke
Copy link

use axum::async_trait;
use axum_login::{secrecy::SecretVec, AuthUser};

#[derive(Clone, Debug, PartialEq, PartialOrd, sqlx::Type)]
pub enum UserRole {
    Standard,
    Admin,
}

#[derive(Clone, Debug, sqlx::FromRow, sqlx::Decode)]
pub struct User {
    id: i32,
    username: String,
    password_hash: String,
    role: UserRole,
}

impl AuthUser<UserRole> for User {
    fn get_id(&self) -> String {
        format!("{}", self.id)
    }

    fn get_password_hash(&self) -> SecretVec<u8> {
        SecretVec::new(self.password_hash.clone().into())
    }

    fn get_role(&self) -> Option<UserRole> {
        Some(self.role.clone())
    }
}

#[async_trait]
impl axum_login::UserStore<UserRole> for axum_login::PostgresStore<User> {
    type User = User;

    async fn load_user(&self, user_id: &str) -> Result<Option<Self::User>, eyre::Report> {
        let mut connection = self.pool.acquire() ...
    }
}

When I try to use self.pool.acquire in the load_user, I get error that self.pool is private. How can I implement load_user without access to the database pool

@maxcountryman
Copy link
Owner

Would your use case be able to leverage this pattern?

let PgStore = axum_login::PostgresStore<User, UserRole>;

Or do you need to reimplement load_user altogether ?

@sean-clarke
Copy link
Author

sean-clarke commented Dec 7, 2022

Where exactly would I use that? Currently, when I define user_store, I do:

  let user_store = PostgresStore::<User, UserRole>::new(pool.clone());

Additionally, if I mock the load_user fn to just return a standard user instead of going into the db, when I hit the endpoint that uses AuthContext, I get this error:

thread 'tokio-runtime-worker' panicked at 'Auth extension missing. Is the auth layer installed?: MissingExtension(MissingExtension(Error { inner: "Extension of type `axum_login::extractors::AuthContext<be::entities::user::User, axum_login::sqlx_store::SqlxStore<sqlx_core::pool::Pool<sqlx_core::postgres::database::Postgres>, be::entities::user::User>, be::entities::user::UserRole>` was not found. Perhaps you forgot to add it? See `axum::Extension`." }))

My main.rs looks like this:

use axum::{
    extract::State,
    routing::{get, post},
    Json, Router,
};
use axum_login::{
    axum_sessions::{async_session::MemoryStore, SessionLayer},
    AuthLayer, PostgresStore,
};
use rand::Rng;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::net::SocketAddr;

mod entities;

use entities::user::{User, UserRole};

#[tokio::main]
async fn main() {
    let secret = rand::thread_rng().gen::<[u8; 64]>();
    let session_store = MemoryStore::new();
    let session_layer = SessionLayer::new(session_store, &secret).with_secure(false);

    dotenvy::dotenv().ok();
    let database_connection_uri = dotenvy::var("DATABASE_URL").unwrap();

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_connection_uri)
        .await
        .unwrap();

    let user_store = PostgresStore::<User, UserRole>::new(pool.clone());
    let auth_layer = AuthLayer::new(user_store, &secret);

    let app = Router::new()
        .route("/login", post(login))
        .layer(auth_layer)
        .layer(session_layer)
        .with_state(pool);

    let addr = SocketAddr::from(([127, 0, 0, 1], 5000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

type AuthContext = axum_login::extractors::AuthContext<User, PostgresStore<User>, UserRole>;

#[derive(serde::Deserialize)]
struct Login {
    username: String,
    password: String,
}

#[axum::debug_handler]
async fn login(
    mut auth: AuthContext,
    State(pool): State<PgPool>,
    Json(payload): Json<Login>,
) -> impl axum::response::IntoResponse {
    let password_hash = hash_password(payload.password);
    let user_result =
        sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = $1 AND password_hash = $2")
            .bind(payload.username)
            .bind(password_hash)
            .fetch_one(&pool)
            .await;

    let user: User = match user_result {
        Ok(value) => value,
        Err(_) => return Err((axum::http::StatusCode::UNAUTHORIZED, ())),
    };

    auth.login(&user).await.unwrap();
    Ok(())
}

@maxcountryman
Copy link
Owner

Without a more complete example, it's going to be difficult to advise.

The auth context missing usually means that the middleware isn't installed for that route.

@sean-clarke
Copy link
Author

I've updated my above comment to include the full main.rs, and the error

@maxcountryman
Copy link
Owner

maxcountryman commented Dec 8, 2022

Are you able to share your routes as well? The error is about the middleware not being present.

Edit: Oh I see it's in your main.rs. I'll try to take a closer look.

@maxcountryman
Copy link
Owner

All right, so I'm not going to be able to run your code directly since there's a few missing dependencies and modules internal to your project.

That said, I tried my best to replicate your situation with the SQLite example. Here's the diff in case you'd like to see (you'll also have to modify the user_store.db with the role column):

diff --git a/examples/sqlite/src/main.rs b/examples/sqlite/src/main.rs
index 565e23e..0e84e3a 100644
--- a/examples/sqlite/src/main.rs
+++ b/examples/sqlite/src/main.rs
@@ -8,19 +8,26 @@ use axum::{response::IntoResponse, routing::get, Extension, Router};
 use axum_login::{
     axum_sessions::{async_session::MemoryStore, SessionLayer},
     secrecy::SecretVec,
-    AuthLayer, AuthUser, RequireAuthorizationLayer, SqliteStore,
+    AuthLayer, AuthUser, RequireAuthorizationLayer,
 };
 use rand::Rng;
 use sqlx::sqlite::SqlitePoolOptions;
 
-#[derive(Debug, Default, Clone, sqlx::FromRow)]
+#[derive(Clone, Debug, PartialEq, PartialOrd, sqlx::Type)]
+pub enum UserRole {
+    Standard,
+    Admin,
+}
+
+#[derive(Debug, Clone, sqlx::FromRow)]
 struct User {
     id: i64,
     password_hash: String,
     name: String,
+    role: UserRole,
 }
 
-impl AuthUser for User {
+impl AuthUser<UserRole> for User {
     fn get_id(&self) -> String {
         format!("{}", self.id)
     }
@@ -28,9 +35,14 @@ impl AuthUser for User {
     fn get_password_hash(&self) -> SecretVec<u8> {
         SecretVec::new(self.password_hash.clone().into())
     }
+
+    fn get_role(&self) -> Option<UserRole> {
+        Some(self.role.clone())
+    }
 }
 
-type AuthContext = axum_login::extractors::AuthContext<User, SqliteStore<User>>;
+type SqliteStore = axum_login::SqliteStore<User, UserRole>;
+type AuthContext = axum_login::extractors::AuthContext<User, SqliteStore, UserRole>;
 
 #[tokio::main]
 async fn main() {
@@ -44,8 +56,8 @@ async fn main() {
         .await
         .unwrap();
 
-    let user_store = SqliteStore::<User>::new(pool);
-    let auth_layer = AuthLayer::new(user_store, &secret);
+    let user_store = SqliteStore::new(pool);
+    let auth_layer = AuthLayer::<SqliteStore, User, UserRole>::new(user_store, &secret);
 
     async fn login_handler(mut auth: AuthContext) {
         let pool = SqlitePoolOptions::new()
@@ -71,7 +83,7 @@ async fn main() {
 
     let app = Router::new()
         .route("/protected", get(protected_handler))
-        .route_layer(RequireAuthorizationLayer::<User>::login())
+        .route_layer(RequireAuthorizationLayer::<User, UserRole>::login())
         .route("/login", get(login_handler))
         .route("/logout", get(logout_handler))
         .layer(auth_layer)
diff --git a/examples/sqlite/user_store.db b/examples/sqlite/user_store.db
index d26f080..f0b7743 100644
Binary files a/examples/sqlite/user_store.db and b/examples/sqlite/user_store.db differ

In your example, this line seems to be missing something:

type AuthContext = axum_login::extractors::AuthContext<User, PostgresStore<User>, UserRole>;

The PostgresStore should also indicate the concrete role type because otherwise it will default to the unit type. Does changing that line address the issue for you?

Repository owner locked and limited conversation to collaborators Dec 9, 2022
@maxcountryman maxcountryman converted this issue into discussion #32 Dec 9, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants