backend: clean up api and routes, better error handling, user impl
This commit is contained in:
parent
90be183e7e
commit
117efffea2
48
src/api/v1/errors.rs
Normal file
48
src/api/v1/errors.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AuthError<E> {
|
||||||
|
InternalError(E),
|
||||||
|
MissingCredentials,
|
||||||
|
InvalidCredentials,
|
||||||
|
MissingToken,
|
||||||
|
InvalidToken,
|
||||||
|
MissingUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: std::error::Error> IntoResponse for AuthError<E> {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
let (status, message) = match self {
|
||||||
|
Self::InternalError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||||
|
Self::MissingCredentials => {
|
||||||
|
(StatusCode::BAD_REQUEST, "Missing credentials".to_string())
|
||||||
|
}
|
||||||
|
Self::InvalidCredentials => {
|
||||||
|
(StatusCode::UNAUTHORIZED, "Invalid credentials".to_string())
|
||||||
|
}
|
||||||
|
Self::MissingToken => (StatusCode::BAD_REQUEST, "Missing token".to_string()),
|
||||||
|
Self::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token".to_string()),
|
||||||
|
Self::MissingUser => (StatusCode::UNAUTHORIZED, "User not exists".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(json!({
|
||||||
|
"status": status.to_string(),
|
||||||
|
"message": message
|
||||||
|
}))
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal_error<E>(err: E) -> (StatusCode, Json<serde_json::Value>)
|
||||||
|
where
|
||||||
|
E: std::error::Error,
|
||||||
|
{
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({
|
||||||
|
"status": StatusCode::INTERNAL_SERVER_ERROR.to_string(),
|
||||||
|
"message": err.to_string()
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
53
src/api/v1/middleware.rs
Normal file
53
src/api/v1/middleware.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Request, State},
|
||||||
|
http::header,
|
||||||
|
middleware::Next,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
|
use crate::{db::user::User, state::AppState};
|
||||||
|
|
||||||
|
use super::errors::AuthError;
|
||||||
|
use super::token::TokenClaims;
|
||||||
|
|
||||||
|
pub async fn jwt_auth(
|
||||||
|
cookie_jar: CookieJar,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
mut req: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<impl IntoResponse, AuthError<impl std::error::Error>> {
|
||||||
|
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(|| AuthError::MissingToken)?;
|
||||||
|
let claims = TokenClaims::validate(token, state.config.jwt.secret.to_owned())
|
||||||
|
.map_err(|_| AuthError::InvalidToken)?;
|
||||||
|
|
||||||
|
let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| AuthError::InvalidToken)?;
|
||||||
|
|
||||||
|
let user = User::find(&state.database, User::by_id(user_id))
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::InternalError)?;
|
||||||
|
|
||||||
|
let user = user.ok_or_else(|| AuthError::MissingUser)?;
|
||||||
|
|
||||||
|
req.extensions_mut().insert(user);
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
@ -1,300 +1,58 @@
|
|||||||
use crate::db::models::{FilteredUser, LoginUser, NewUser, RegisterUser, TokenClaims, User};
|
pub mod errors;
|
||||||
use crate::error_handle::internal_error;
|
pub mod middleware;
|
||||||
use crate::state::AppState;
|
pub mod token;
|
||||||
use argon2::{password_hash::SaltString, Argon2};
|
pub mod user;
|
||||||
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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
http::{header::*, Method, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub fn routes(state: Arc<AppState>) -> Router {
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
||||||
|
.allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE])
|
||||||
|
.allow_origin([
|
||||||
|
"http://localhost:54600".parse().unwrap(),
|
||||||
|
"http://localhost:5173".parse().unwrap(),
|
||||||
|
])
|
||||||
|
.allow_credentials(true);
|
||||||
|
|
||||||
|
let jwt = axum::middleware::from_fn_with_state(state.to_owned(), middleware::jwt_auth);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/v1/healthcheck", get(healthcheck))
|
||||||
|
.route("/v1/user/register", post(user::register))
|
||||||
|
.route("/v1/user/login", post(user::login))
|
||||||
|
.route("/v1/user/logout", get(user::logout))
|
||||||
|
.route("/v1/user/profile", get(user::profile).route_layer(jwt))
|
||||||
|
.layer(cors)
|
||||||
|
.fallback(fallback)
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn healthcheck() -> impl IntoResponse {
|
pub async fn healthcheck() -> impl IntoResponse {
|
||||||
Json(serde_json::json!({
|
(
|
||||||
"status": "success",
|
StatusCode::OK,
|
||||||
"message": "healthy"
|
Json(json!({
|
||||||
}))
|
"status": StatusCode::OK.to_string(),
|
||||||
}
|
})),
|
||||||
|
|
||||||
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>)> {
|
pub async fn fallback() -> impl IntoResponse {
|
||||||
let cookie = Cookie::build(("token", ""))
|
(
|
||||||
.path("/")
|
StatusCode::NOT_FOUND,
|
||||||
.max_age(time::Duration::hours(-1))
|
Json(json!({
|
||||||
.same_site(SameSite::None)
|
"status": StatusCode::NOT_FOUND.to_string(),
|
||||||
.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)})}),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
36
src/api/v1/token.rs
Normal file
36
src/api/v1/token.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TokenClaims {
|
||||||
|
pub sub: String,
|
||||||
|
pub exp: usize,
|
||||||
|
pub iat: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenClaims {
|
||||||
|
pub fn create(
|
||||||
|
sub: String,
|
||||||
|
secret: String,
|
||||||
|
duration: i64,
|
||||||
|
) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let iat = now.timestamp() as usize;
|
||||||
|
let exp = (now + chrono::Duration::try_seconds(duration).unwrap()).timestamp() as usize;
|
||||||
|
let claims = Self { sub, exp, iat };
|
||||||
|
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claims,
|
||||||
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(token: String, secret: String) -> Result<Self, jsonwebtoken::errors::Error> {
|
||||||
|
Ok(decode::<Self>(
|
||||||
|
&token,
|
||||||
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
|
&Validation::default(),
|
||||||
|
)?
|
||||||
|
.claims)
|
||||||
|
}
|
||||||
|
}
|
142
src/api/v1/user.rs
Normal file
142
src/api/v1/user.rs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
use argon2::Argon2;
|
||||||
|
use argon2::{PasswordHash, PasswordVerifier};
|
||||||
|
use axum::Extension;
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::{header, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::cookie::{Cookie, SameSite};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::db::user::User;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
use super::errors::AuthError;
|
||||||
|
use super::token::TokenClaims;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct RegisterUser {
|
||||||
|
pub login: String,
|
||||||
|
pub password: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct FilteredUser {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilteredUser {
|
||||||
|
pub fn from(user: &User) -> Self {
|
||||||
|
FilteredUser {
|
||||||
|
id: user.id.to_string(),
|
||||||
|
name: user.name.to_owned(),
|
||||||
|
email: user.email.to_owned(),
|
||||||
|
is_admin: user.is_admin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct LoginUser {
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<RegisterUser>,
|
||||||
|
) -> Result<impl IntoResponse, AuthError<impl std::error::Error>> {
|
||||||
|
let user = User::register(
|
||||||
|
&state.database,
|
||||||
|
body.login,
|
||||||
|
body.password,
|
||||||
|
body.name,
|
||||||
|
body.email,
|
||||||
|
body.is_admin,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::InternalError)?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"status": "success",
|
||||||
|
"user": FilteredUser::from(&user)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<LoginUser>,
|
||||||
|
) -> Result<impl IntoResponse, AuthError<impl std::error::Error>> {
|
||||||
|
let user = User::find(&state.database, User::by_email(body.email))
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::InternalError)?;
|
||||||
|
|
||||||
|
let user = match user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Err(AuthError::InvalidCredentials),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !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,
|
||||||
|
} {
|
||||||
|
return Err(AuthError::InvalidCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = TokenClaims::create(
|
||||||
|
user.id.to_string(),
|
||||||
|
state.config.jwt.secret.to_owned(),
|
||||||
|
state.config.jwt.maxage,
|
||||||
|
)
|
||||||
|
.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 =
|
||||||
|
Json(json!({"status": StatusCode::OK.to_string(), "token": token})).into_response();
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout() -> 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 = Json(json!({"status": StatusCode::OK.to_string()})).into_response();
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profile(
|
||||||
|
Extension(user): Extension<User>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
Ok(Json(
|
||||||
|
json!({"status":"success","user":json!(FilteredUser::from(&user))}),
|
||||||
|
))
|
||||||
|
}
|
@ -27,7 +27,7 @@ pub struct Server {
|
|||||||
pub struct Jwt {
|
pub struct Jwt {
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
pub expires_in: String,
|
pub expires_in: String,
|
||||||
pub maxage: i32,
|
pub maxage: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -56,7 +56,7 @@ impl Config {
|
|||||||
secret: env::var("JWT_SECRET").unwrap_or("change_this_secret".to_string()),
|
secret: env::var("JWT_SECRET").unwrap_or("change_this_secret".to_string()),
|
||||||
expires_in: env::var("JWT_EXPIRES_IN").unwrap_or("60m".to_string()),
|
expires_in: env::var("JWT_EXPIRES_IN").unwrap_or("60m".to_string()),
|
||||||
maxage: env::var("JWT_MAXAGE")
|
maxage: env::var("JWT_MAXAGE")
|
||||||
.unwrap_or("60".to_string())
|
.unwrap_or("3600".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
},
|
},
|
||||||
|
63
src/db/errors.rs
Normal file
63
src/db/errors.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use deadpool_diesel::postgres::PoolError;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DatabaseError<E> {
|
||||||
|
Connection(E),
|
||||||
|
Interaction,
|
||||||
|
Operation(E),
|
||||||
|
Migration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: std::fmt::Display> std::fmt::Display for DatabaseError<E> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Connection(e) => write!(f, "Failed to connect to database: {}", e),
|
||||||
|
Self::Interaction => write!(f, "Failed to interact with database"),
|
||||||
|
Self::Operation(e) => write!(f, "Failed operation: {}", e),
|
||||||
|
Self::Migration => write!(f, "Failed to run migrations"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: std::error::Error + 'static> std::error::Error for DatabaseError<E> {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Interaction | Self::Migration => None,
|
||||||
|
Self::Connection(e) => Some(e),
|
||||||
|
Self::Operation(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PoolError> for DatabaseError<PoolError> {
|
||||||
|
fn from(e: PoolError) -> Self {
|
||||||
|
Self::Connection(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserError {
|
||||||
|
Query,
|
||||||
|
Exists,
|
||||||
|
NotFound,
|
||||||
|
HashPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UserError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Query => write!(f, "User query failed"),
|
||||||
|
UserError::Exists => write!(f, "User already exists"),
|
||||||
|
UserError::NotFound => write!(f, "User not found"),
|
||||||
|
UserError::HashPassword => write!(f, "Failed to hash user password"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UserError {}
|
||||||
|
|
||||||
|
impl From<UserError> for DatabaseError<UserError> {
|
||||||
|
fn from(e: UserError) -> Self {
|
||||||
|
Self::Operation(e)
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
pub mod models;
|
pub mod errors;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
use deadpool_diesel::postgres::Manager;
|
use deadpool_diesel::postgres::Manager;
|
||||||
pub use deadpool_diesel::postgres::Pool;
|
pub use deadpool_diesel::postgres::Pool;
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||||
|
|
||||||
use crate::db::models::{NewUser, User};
|
use errors::DatabaseError;
|
||||||
|
|
||||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations/");
|
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations/");
|
||||||
|
|
||||||
@ -16,45 +16,12 @@ pub fn create_pool(database_url: String) -> Pool {
|
|||||||
Pool::builder(manager).build().unwrap()
|
Pool::builder(manager).build().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_migrations(pool: &Pool) {
|
pub async fn run_migrations(pool: &Pool) -> Result<(), DatabaseError<impl std::error::Error>> {
|
||||||
let conn = pool.get().await.unwrap();
|
let connection = pool.get().await.map_err(DatabaseError::Connection)?;
|
||||||
conn.interact(|conn| conn.run_pending_migrations(MIGRATIONS).map(|_| ()))
|
connection
|
||||||
|
.interact(move |connection| connection.run_pending_migrations(MIGRATIONS).map(|_| ()))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.map_err(|_| DatabaseError::Interaction)?
|
||||||
.unwrap();
|
.map_err(|_| DatabaseError::Migration)?;
|
||||||
}
|
Ok(())
|
||||||
|
|
||||||
pub fn create_user(
|
|
||||||
connection: &mut PgConnection,
|
|
||||||
login: String,
|
|
||||||
hashed_password: String,
|
|
||||||
name: String,
|
|
||||||
email: String,
|
|
||||||
is_admin: bool,
|
|
||||||
) -> User {
|
|
||||||
use crate::db::schema::users;
|
|
||||||
|
|
||||||
let new_user = NewUser {
|
|
||||||
login,
|
|
||||||
hashed_password,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
is_admin,
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(users::table)
|
|
||||||
.values(&new_user)
|
|
||||||
.returning(User::as_returning())
|
|
||||||
.get_result(connection)
|
|
||||||
.expect("Error creating new user")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
use crate::db::schema;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, Queryable, Selectable, Clone)]
|
|
||||||
#[diesel(table_name = schema::users)]
|
|
||||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
|
||||||
pub struct User {
|
|
||||||
pub id: uuid::Uuid,
|
|
||||||
pub login: String,
|
|
||||||
pub hashed_password: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
pub is_admin: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Insertable)]
|
|
||||||
#[diesel(table_name = schema::users)]
|
|
||||||
pub struct NewUser {
|
|
||||||
pub login: String,
|
|
||||||
pub hashed_password: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
pub is_admin: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct RegisterUser {
|
|
||||||
pub login: String,
|
|
||||||
pub password: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
pub is_admin: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct FilteredUser {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
pub is_admin: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FilteredUser {
|
|
||||||
pub fn from(user: &User) -> Self {
|
|
||||||
FilteredUser {
|
|
||||||
id: user.id.to_string(),
|
|
||||||
name: user.name.to_owned(),
|
|
||||||
email: user.email.to_owned(),
|
|
||||||
is_admin: user.is_admin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct LoginUser {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct TokenClaims {
|
|
||||||
pub sub: String,
|
|
||||||
pub exp: usize,
|
|
||||||
pub iat: usize,
|
|
||||||
}
|
|
121
src/db/user.rs
Normal file
121
src/db/user.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use crate::db::{errors::*, schema::users, Pool};
|
||||||
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
|
use diesel::{
|
||||||
|
dsl::{AsSelect, SqlTypeOf},
|
||||||
|
pg::Pg,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Queryable, Selectable, Clone)]
|
||||||
|
#[diesel(table_name = users)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct User {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub login: String,
|
||||||
|
pub hashed_password: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Insertable)]
|
||||||
|
#[diesel(table_name = users)]
|
||||||
|
pub struct NewUser {
|
||||||
|
pub login: String,
|
||||||
|
pub hashed_password: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SqlType = SqlTypeOf<AsSelect<User, Pg>>;
|
||||||
|
type BoxedQuery<'a> = users::BoxedQuery<'a, Pg, SqlType>;
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn register(
|
||||||
|
pool: &Pool,
|
||||||
|
login: String,
|
||||||
|
password: String,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
is_admin: bool,
|
||||||
|
) -> Result<User, DatabaseError<impl std::error::Error>> {
|
||||||
|
let user = User::find(pool, User::by_email(email.to_owned()))
|
||||||
|
.await
|
||||||
|
.map_err(|_| UserError::Query)?;
|
||||||
|
|
||||||
|
if user.is_some() {
|
||||||
|
return Err(DatabaseError::Operation(UserError::Exists));
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashed_password = Argon2::default()
|
||||||
|
.hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng))
|
||||||
|
.map_err(|_| UserError::HashPassword)
|
||||||
|
.map(|hash| hash.to_string())?;
|
||||||
|
|
||||||
|
let new_user = NewUser {
|
||||||
|
login,
|
||||||
|
hashed_password,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
is_admin,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = User::create(pool, new_user)
|
||||||
|
.await
|
||||||
|
.map_err(|_| UserError::Query)?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
pool: &Pool,
|
||||||
|
new_user: NewUser,
|
||||||
|
) -> Result<User, DatabaseError<impl std::error::Error>> {
|
||||||
|
let connection = pool.get().await.map_err(DatabaseError::Connection)?;
|
||||||
|
let user = connection
|
||||||
|
.interact(move |connection| {
|
||||||
|
diesel::insert_into(users::table)
|
||||||
|
.values(new_user)
|
||||||
|
.returning(User::as_returning())
|
||||||
|
.get_result(connection)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| DatabaseError::Interaction)?
|
||||||
|
.map_err(|_| DatabaseError::Interaction)?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn by_email(email: String) -> BoxedQuery<'static> {
|
||||||
|
users::table
|
||||||
|
.select(User::as_select())
|
||||||
|
.into_boxed()
|
||||||
|
.filter(users::email.eq(email))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn by_id(id: uuid::Uuid) -> BoxedQuery<'static> {
|
||||||
|
users::table
|
||||||
|
.select(User::as_select())
|
||||||
|
.into_boxed()
|
||||||
|
.filter(users::id.eq(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find(
|
||||||
|
pool: &Pool,
|
||||||
|
query: BoxedQuery<'static>,
|
||||||
|
) -> Result<Option<User>, DatabaseError<impl std::error::Error>> {
|
||||||
|
let connection = pool.get().await.map_err(DatabaseError::Connection)?;
|
||||||
|
let user = connection
|
||||||
|
.interact(move |connection| query.first(connection).optional())
|
||||||
|
.await
|
||||||
|
.map_err(|_| DatabaseError::Interaction)?
|
||||||
|
.map_err(|_| DatabaseError::Interaction)?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub fn remove(pool: Pool, email: String) {}
|
||||||
|
}
|
80
src/main.rs
80
src/main.rs
@ -5,28 +5,18 @@ pub mod error_handle;
|
|||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::Path,
|
||||||
http::{
|
http::{header::CONTENT_TYPE, StatusCode, Uri},
|
||||||
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, COOKIE, ORIGIN},
|
response::{IntoResponse, Response},
|
||||||
HeaderValue, Method, StatusCode, Uri,
|
routing::get,
|
||||||
},
|
|
||||||
middleware,
|
|
||||||
response::{IntoResponse, Json, Response},
|
|
||||||
routing::{get, post},
|
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use diesel::RunQueryDsl;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{env, net::Ipv4Addr};
|
use tower_http::trace::{self, TraceLayer};
|
||||||
use tower_http::{
|
|
||||||
cors::{Any, CorsLayer},
|
|
||||||
trace::{self, TraceLayer},
|
|
||||||
};
|
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::db::{create_user, models::User};
|
|
||||||
use crate::error_handle::*;
|
use crate::error_handle::*;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@ -63,38 +53,16 @@ async fn main() {
|
|||||||
|
|
||||||
let lister = tokio::net::TcpListener::bind(&address).await.unwrap();
|
let lister = tokio::net::TcpListener::bind(&address).await.unwrap();
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
|
||||||
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
|
||||||
.allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE])
|
|
||||||
.allow_origin([
|
|
||||||
"http://localhost:54600".parse().unwrap(),
|
|
||||||
"http://localhost:5173".parse().unwrap(),
|
|
||||||
]) //Any)
|
|
||||||
.allow_credentials(true); //"http://localhost:5173".parse::<HeaderValue>().unwrap());
|
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
.route("/user/login", get(user_login))
|
.route("/user/login", get(user_login))
|
||||||
.route("/assets/*file", get(static_handler))
|
.route("/assets/*file", get(static_handler))
|
||||||
.route("/api/v1/register_user", post(api::v1::register_user))
|
.nest("/api", api::v1::routes(state))
|
||||||
.route("/api/v1/login_user", post(api::v1::login_user))
|
|
||||||
.route(
|
|
||||||
"/api/v1/me",
|
|
||||||
get(api::v1::me).route_layer(middleware::from_fn_with_state(
|
|
||||||
state.clone(),
|
|
||||||
api::v1::jwt_auth,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.layer(cors)
|
|
||||||
.route("/api/v1/healthcheck", get(api::v1::healthcheck))
|
|
||||||
.route("/api/v1/users", get(users))
|
|
||||||
.route("/api/v1/logout_user", get(api::v1::logout_user))
|
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||||
)
|
);
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
println!("listening on http://{}", address);
|
println!("listening on http://{}", address);
|
||||||
|
|
||||||
@ -114,22 +82,6 @@ async fn user_login() -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn user(Path(user): Path<String>) -> impl IntoResponse {}
|
async fn user(Path(user): Path<String>) -> impl IntoResponse {}
|
||||||
|
|
||||||
async fn users(
|
|
||||||
State(state): State<Arc<AppState>>,
|
|
||||||
) -> Result<Json<Vec<User>>, (StatusCode, Json<serde_json::Value>)> {
|
|
||||||
use db::schema::users::dsl::*;
|
|
||||||
|
|
||||||
let conn = state.database.get().await.unwrap();
|
|
||||||
|
|
||||||
let result = conn
|
|
||||||
.interact(move |conn| users.load(conn))
|
|
||||||
.await
|
|
||||||
.map_err(internal_error)?
|
|
||||||
.map_err(internal_error)?;
|
|
||||||
|
|
||||||
Ok(Json(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn static_handler(uri: Uri) -> impl IntoResponse {
|
async fn static_handler(uri: Uri) -> impl IntoResponse {
|
||||||
let mut path = uri.path().trim_start_matches('/').to_string();
|
let mut path = uri.path().trim_start_matches('/').to_string();
|
||||||
|
|
||||||
@ -158,21 +110,3 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
create_user(
|
|
||||||
connection,
|
|
||||||
"L-Nafaryus",
|
|
||||||
"asdasd",
|
|
||||||
"L-Nafaryus",
|
|
||||||
"l.nafaryus@elnafo.ru",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
let results = users
|
|
||||||
.select(User::as_select())
|
|
||||||
.load(connection)
|
|
||||||
.expect("Error loading users");
|
|
||||||
|
|
||||||
println!("Found {} users", results.len());
|
|
||||||
*/
|
|
||||||
|
Loading…
Reference in New Issue
Block a user