6 releases

0.2.4 Jun 29, 2024
0.2.3 Jun 13, 2024
0.2.2 Mar 5, 2024
0.2.1 Feb 17, 2024
0.1.0 May 18, 2023

#549 in HTTP server

Download history 38/week @ 2024-07-25 6/week @ 2024-08-01 6/week @ 2024-09-12 18/week @ 2024-09-19 17/week @ 2024-09-26 1/week @ 2024-10-10

352 downloads per month

MIT/Apache

40KB
869 lines

loginmanager

a simple loginmanager for axum

Usage example

use std::sync::Arc;

use axum::{
    async_trait,
    http::request::Parts,
    middleware::from_extractor,
    response::{Html, IntoResponse, Redirect, Response},
    routing::get,
    Extension, Form, Router,
};
use loginmanager::{AuthContext, AuthUser, CookieSession, CurrentUser, LoginManager};
use sea_orm::{ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, Statement, Value};
use serde::Deserialize;

#[derive(Debug, Clone)]
struct User {
    id: i32,
    name: String,
    password: String,
}

#[async_trait]
impl loginmanager::UserMinix<Parts> for User {
    type Key = i32;

    async fn get_user(id: &Self::Key, req: &mut Parts) -> Option<Self> {
        let state = req.extensions.get::<Arc<AppState>>()?;
        let user = state
            .db()
            .query_one(Statement::from_sql_and_values(
                DatabaseBackend::Sqlite,
                "select id,name,password from user where id=?",
                [Value::from(id.to_string())],
            ))
            .await
            .ok()??;
        let user = Self {
            id: user.try_get::<i32>("", "id").unwrap(),
            name: user.try_get::<String>("", "name").unwrap(),
            password: user.try_get::<String>("", "password").unwrap(),
        };
        Some(user)
    }

    fn get_id(&self) -> &Self::Key {
        &self.id
    }
}

async fn hello_user(AuthUser(user): AuthUser<User>) -> Response {
    return Html(format!(
        "hello {}<br> <a href='/logout'>logout</a>",
        user.name
    ))
    .into_response();
}

async fn login_get() -> impl IntoResponse {
    return Html(
        r#"
    <form method="POST">
        username:<input name="username"></input>
        password:<input name="password" type="password"></input>
        <input type="submit"></input>
    </form>
    "#,
    );
}

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

async fn login_post(
    Extension(state): Extension<Arc<AppState>>,
    mut auth_context: AuthContext,
    Form(form): Form<UserForm>,
) -> Response {
    let user = state
        .db()
        .query_one(Statement::from_sql_and_values(
            DatabaseBackend::Sqlite,
            "select id,name,password from user where name=?",
            [Value::from(&form.username)],
        ))
        .await
        .ok()
        .unwrap();
    if let Some(user) = user {
        let user = User {
            id: user.try_get::<i32>("", "id").unwrap(),
            name: user.try_get::<String>("", "name").unwrap(),
            password: user.try_get::<String>("", "password").unwrap(),
        };
        if user.password == form.password {
            auth_context.login(&user);
            return Redirect::to("/").into_response();
        } else {
            return format!("error password").into_response();
        }
    } else {
        return format!("{:?} not exists.", user).into_response();
    }
}

async fn login_out(
    mut auth_context: AuthContext,
    CurrentUser(user): CurrentUser<User>,
) -> impl IntoResponse {
    auth_context.logout();
    return Redirect::to("/login");
}

pub struct AppState {
    conn: DatabaseConnection,
}

impl AppState {
    pub fn db(&self) -> &DatabaseConnection {
        &self.conn
    }
}

#[tokio::main]
async fn main() {
    // protect api
    let api = Router::new()
        .route("/:path", get(hello_user))
        .route_layer(from_extractor::<AuthUser<User>>());

    let loginmanager = LoginManager::new(CookieSession::new("secret").secure(false))
        .redirect(true)
        .login_view("/login");

    let conn: DatabaseConnection = Database::connect("sqlite::memory:").await.unwrap();
    conn.execute(Statement::from_string(
        DatabaseBackend::Sqlite,
        r#"
        CREATE TABLE "user" (
            "id"	INTEGER,
            "name"	TEXT UNIQUE,
            "password"	TEXT,
            PRIMARY KEY("id")
        );
        INSERT INTO user VALUES (1,"miku","39"),(2,"miku2","392");
        "#
        .to_owned(),
    ))
    .await
    .unwrap();

    let app = Router::new()
        .nest("/api", api)
        .route("/", get(hello_user))
        .route("/login", get(login_get).post(login_post))
        .route("/logout", get(login_out))
        .route("/common", get(|| async { "Hello, World!" }))
        .layer(loginmanager)
        .layer(Extension(Arc::new(AppState { conn })));

    // run it with hyper on localhost:3000
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app.into_make_service())
        .await
        .unwrap();
}

Usage for Actix

Enable features actix_layer, Example.

Dependencies

~4–15MB
~201K SLoC