backend: relax jwt

This commit is contained in:
L-Nafaryus 2024-03-26 01:26:16 +05:00
parent 1b0e5d53c2
commit 04dcf039c3
Signed by: L-Nafaryus
GPG Key ID: 582F8B0866B294A1
4 changed files with 87 additions and 13 deletions

View File

@ -3,9 +3,10 @@ use std::sync::Arc;
use axum::{ use axum::{
body::Body, body::Body,
extract::{Request, State}, extract::{Request, State},
http::header, http::{header, StatusCode},
middleware::Next, middleware::Next,
response::IntoResponse, response::IntoResponse,
Json,
}; };
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
@ -14,7 +15,7 @@ use crate::{db::user::User, state::AppState};
use super::errors::AuthError; use super::errors::AuthError;
use super::token::TokenClaims; use super::token::TokenClaims;
pub async fn jwt_auth( pub async fn jwt(
cookie_jar: CookieJar, cookie_jar: CookieJar,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
mut req: Request<Body>, mut req: Request<Body>,
@ -27,13 +28,8 @@ pub async fn jwt_auth(
req.headers() req.headers()
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok()) .and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| { .and_then(|auth_value| auth_value.strip_prefix("Bearer "))
if auth_value.starts_with("Bearer ") { .map(|auth_token| auth_token.to_owned())
Some(auth_value[7..].to_owned())
} else {
None
}
})
}); });
let token = token.ok_or_else(|| AuthError::MissingToken)?; let token = token.ok_or_else(|| AuthError::MissingToken)?;
@ -51,3 +47,28 @@ pub async fn jwt_auth(
req.extensions_mut().insert(user); req.extensions_mut().insert(user);
Ok(next.run(req).await) Ok(next.run(req).await)
} }
pub async fn jwt_auth(
cookie_jar: CookieJar,
State(state): State<Arc<AppState>>,
mut req: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
let token = cookie_jar
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
req.headers()
.get(header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| auth_value.strip_prefix("Bearer "))
.map(|auth_token| auth_token.to_owned())
});
let user_id = token
.and_then(|token| TokenClaims::validate(token, state.config.jwt.secret.to_owned()).ok())
.and_then(|claims| uuid::Uuid::parse_str(&claims.sub).ok());
req.extensions_mut().insert(user_id);
Ok(next.run(req).await)
}

View File

@ -34,7 +34,11 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/v1/user/remove", post(user::remove)) .route("/v1/user/remove", post(user::remove))
.route("/v1/user/login", post(user::login)) .route("/v1/user/login", post(user::login))
.route("/v1/user/logout", get(user::logout)) .route("/v1/user/logout", get(user::logout))
.route("/v1/user/profile", get(user::profile).route_layer(jwt)) .route(
"/v1/user/current",
get(user::current).route_layer(jwt.to_owned()),
)
.route("/v1/user/:login", get(user::profile).route_layer(jwt))
.layer(cors) .layer(cors)
.fallback(fallback) .fallback(fallback)
.with_state(state) .with_state(state)

View File

@ -1,5 +1,6 @@
use argon2::Argon2; use argon2::Argon2;
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use axum::extract::Path;
use axum::Extension; use axum::Extension;
use axum::{ use axum::{
extract::State, extract::State,
@ -29,6 +30,7 @@ pub struct RegisterUser {
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct FilteredUser { pub struct FilteredUser {
pub id: String, pub id: String,
pub login: String,
pub name: String, pub name: String,
pub email: String, pub email: String,
pub is_admin: bool, pub is_admin: bool,
@ -43,6 +45,7 @@ impl FilteredUser {
pub fn from(user: &User) -> Self { pub fn from(user: &User) -> Self {
FilteredUser { FilteredUser {
id: user.id.to_string(), id: user.id.to_string(),
login: user.login.to_string(),
name: user.name.to_owned(), name: user.name.to_owned(),
email: user.email.to_owned(), email: user.email.to_owned(),
is_admin: user.is_admin, is_admin: user.is_admin,
@ -137,7 +140,7 @@ pub async fn login(
.http_only(true); .http_only(true);
let mut response = let mut response =
Json(json!({"status": StatusCode::OK.to_string(), "token": token})).into_response(); Json(json!({"status": StatusCode::OK.to_string(), "token": token, "user": json!(FilteredUser::from(&user))})).into_response();
response response
.headers_mut() .headers_mut()
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
@ -162,9 +165,48 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
} }
pub async fn profile( pub async fn profile(
Extension(user): Extension<User>, State(state): State<Arc<AppState>>,
Extension(user_id): Extension<Option<uuid::Uuid>>,
Path(login): Path<String>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> { ) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user = User::find(&state.database, User::by_login(login))
.await
.map_err(|_| ())
.unwrap();
let response = if let Some(user) = user {
json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))})
} else {
json!({"status": StatusCode::NOT_FOUND.to_string()})
};
Ok(Json(response))
}
pub async fn current(
State(state): State<Arc<AppState>>,
Extension(user_id): Extension<Option<uuid::Uuid>>,
) -> Result<impl IntoResponse, AuthError<impl std::error::Error>> {
let user = get_user(state, user_id).await?;
Ok(Json( Ok(Json(
json!({"status":"success","user":json!(FilteredUser::from(&user))}), json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))}),
)) ))
} }
async fn get_user(
state: Arc<AppState>,
user_id: Option<uuid::Uuid>,
) -> Result<User, AuthError<impl std::error::Error>> {
let user = if let Some(user_id) = user_id {
User::find(&state.database, User::by_id(user_id))
.await
.map_err(AuthError::InternalError)
} else {
Err(AuthError::InvalidCredentials)
};
let user = user?.ok_or_else(|| AuthError::MissingUser)?;
Ok(user)
}

View File

@ -102,6 +102,13 @@ impl User {
.filter(users::id.eq(id)) .filter(users::id.eq(id))
} }
pub fn by_login(login: String) -> BoxedQuery<'static> {
users::table
.into_boxed()
.select(User::as_select())
.filter(users::login.eq(login))
}
pub async fn find( pub async fn find(
pool: &Pool, pool: &Pool,
query: BoxedQuery<'static>, query: BoxedQuery<'static>,