301 lines
8.8 KiB
Rust
301 lines
8.8 KiB
Rust
use crate::db::models::{FilteredUser, LoginUser, NewUser, RegisterUser, TokenClaims, User};
|
|
use crate::error_handle::internal_error;
|
|
use crate::state::AppState;
|
|
use argon2::{password_hash::SaltString, Argon2};
|
|
use argon2::{PasswordHash, PasswordHasher, PasswordVerifier};
|
|
use axum::extract::Request;
|
|
use axum::Extension;
|
|
use axum::{
|
|
body::Body,
|
|
extract::State,
|
|
http::{header, StatusCode},
|
|
middleware::Next,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use axum_extra::extract::cookie::{self, Cookie, SameSite};
|
|
use axum_extra::extract::CookieJar;
|
|
use diesel::{connection, prelude::*};
|
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
|
use rand_core::OsRng;
|
|
use std::sync::Arc;
|
|
|
|
pub async fn healthcheck() -> impl IntoResponse {
|
|
Json(serde_json::json!({
|
|
"status": "success",
|
|
"message": "healthy"
|
|
}))
|
|
}
|
|
|
|
pub async fn register_user(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(body): Json<RegisterUser>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
use crate::db::schema::{users, users::dsl};
|
|
|
|
let connection = state.database.get().await.unwrap();
|
|
let (login, email) = (body.login.clone(), body.email.clone());
|
|
let user_exists = connection
|
|
.interact(move |connection| {
|
|
dsl::users
|
|
.filter(dsl::login.eq(login).or(dsl::email.eq(email)))
|
|
.select(User::as_select())
|
|
.first(connection)
|
|
.optional()
|
|
})
|
|
.await
|
|
.map_err(internal_error)?
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": format!("Database error: {}", e)
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
if user_exists.is_some() {
|
|
return Err((
|
|
StatusCode::CONFLICT,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": "Login or email already exists"
|
|
})),
|
|
));
|
|
}
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
let hashed_password = Argon2::default()
|
|
.hash_password(body.password.as_bytes(), &salt)
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": format!("Error while hashing password: {}", e)
|
|
})),
|
|
)
|
|
})
|
|
.map(|hash| hash.to_string())?;
|
|
|
|
let user = NewUser {
|
|
login: body.login.to_string(),
|
|
hashed_password: hashed_password,
|
|
name: body.name,
|
|
email: body.email,
|
|
is_admin: body.is_admin,
|
|
};
|
|
|
|
let new_user = connection
|
|
.interact(move |connection| {
|
|
diesel::insert_into(users::table)
|
|
.values(&user)
|
|
.returning(User::as_returning())
|
|
.get_result(connection)
|
|
})
|
|
.await
|
|
.map_err(internal_error)?
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": format!("Database error: {}", e)
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
let response = serde_json::json!({"status": "success", "data": serde_json::json!({"user": FilteredUser::from(&new_user)})});
|
|
|
|
Ok(Json(response))
|
|
}
|
|
|
|
pub async fn login_user(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(body): Json<LoginUser>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
use crate::db::schema::{users, users::dsl};
|
|
|
|
let connection = state.database.get().await.unwrap();
|
|
let user = connection
|
|
.interact(move |connection| {
|
|
dsl::users
|
|
.filter(dsl::email.eq(body.email))
|
|
.select(User::as_select())
|
|
.first(connection)
|
|
})
|
|
.await
|
|
.map_err(internal_error)?
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": format!("Invalid login or email: {}", e)
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
let is_valid = match PasswordHash::new(&user.hashed_password) {
|
|
Ok(parsed_hash) => Argon2::default()
|
|
.verify_password(body.password.as_bytes(), &parsed_hash)
|
|
.map_or(false, |_| true),
|
|
Err(_) => false,
|
|
};
|
|
|
|
if !is_valid {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": "Invalid login, email or password"
|
|
})),
|
|
));
|
|
}
|
|
|
|
let now = chrono::Utc::now();
|
|
let iat = now.timestamp() as usize;
|
|
let exp = (now + chrono::Duration::try_minutes(60).unwrap()).timestamp() as usize;
|
|
let claims = TokenClaims {
|
|
sub: user.id.to_string(),
|
|
exp,
|
|
iat,
|
|
};
|
|
|
|
let token = encode(
|
|
&Header::default(),
|
|
&claims,
|
|
&EncodingKey::from_secret(state.config.jwt.secret.as_ref()),
|
|
)
|
|
.unwrap();
|
|
|
|
let cookie = Cookie::build(("token", token.to_owned()))
|
|
.path("/")
|
|
.max_age(time::Duration::hours(1))
|
|
.same_site(SameSite::None)
|
|
.secure(true)
|
|
.http_only(true);
|
|
|
|
let mut response =
|
|
Response::new(serde_json::json!({"status": "success", "token": token}).to_string());
|
|
response
|
|
.headers_mut()
|
|
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn logout_user() -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
let cookie = Cookie::build(("token", ""))
|
|
.path("/")
|
|
.max_age(time::Duration::hours(-1))
|
|
.same_site(SameSite::None)
|
|
.secure(true)
|
|
.http_only(true);
|
|
|
|
let mut response = Response::new(serde_json::json!({"status": "success"}).to_string());
|
|
response
|
|
.headers_mut()
|
|
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn jwt_auth(
|
|
cookie_jar: CookieJar,
|
|
State(state): State<Arc<AppState>>,
|
|
mut req: Request<Body>,
|
|
next: Next,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
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| {
|
|
if auth_value.starts_with("Bearer ") {
|
|
Some(auth_value[7..].to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
});
|
|
|
|
let token = token.ok_or_else(|| {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": "Cannot login without token"
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
let claims = decode::<TokenClaims>(
|
|
&token,
|
|
&DecodingKey::from_secret(state.config.jwt.secret.as_ref()),
|
|
&Validation::default(),
|
|
)
|
|
.map_err(|_| {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(serde_json::json!({"status":"fail","message":"Invalid token"})),
|
|
)
|
|
})?
|
|
.claims;
|
|
|
|
let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(serde_json::json!({"status":"fail","message":"Invalid token"})),
|
|
)
|
|
})?;
|
|
|
|
use crate::db::schema::{users, users::dsl};
|
|
|
|
let connection = state.database.get().await.unwrap();
|
|
let user = connection
|
|
.interact(move |connection| {
|
|
dsl::users
|
|
.filter(dsl::id.eq(user_id))
|
|
.select(User::as_select())
|
|
.first(connection)
|
|
.optional()
|
|
})
|
|
.await
|
|
.map_err(internal_error)?
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": format!("Database error: {}", e)
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
let user = user.ok_or_else(|| {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(serde_json::json!({
|
|
"status": "fail",
|
|
"message": "The user belonging to this token no longer exists"
|
|
})),
|
|
)
|
|
})?;
|
|
|
|
req.extensions_mut().insert(user);
|
|
Ok(next.run(req).await)
|
|
}
|
|
|
|
pub async fn me(
|
|
Extension(user): Extension<User>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
Ok(Json(
|
|
serde_json::json!({"status":"success","data":serde_json::json!({"user":FilteredUser::from(&user)})}),
|
|
))
|
|
}
|