Compare commits
4
Commits
616efa5282
...
f63131ebc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f63131ebc7 | ||
|
|
6c1c13e088 | ||
|
|
834a0b7aa4 | ||
|
|
c4d374dbfe |
Generated
+16
@@ -150,6 +150,8 @@ dependencies = [
|
||||
"sqlx",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2243,6 +2245,20 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
|
||||
@@ -15,3 +15,5 @@ chrono = { version = "0.4.44", features = ["serde"] }
|
||||
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
|
||||
argon2 = "0.5.3"
|
||||
time = "0.3.47"
|
||||
tower = "0.5.3"
|
||||
tower-http = { version = "0.6.8", features = ["cors"] }
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod jwt;
|
||||
pub mod validation;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, StatusCode, header},
|
||||
middleware::Next,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{AppState, cookie::jwt::decode_token, models::LoginModel};
|
||||
|
||||
pub async fn validate_token(
|
||||
cookies: CookieJar,
|
||||
State(data): State<Arc<AppState>>,
|
||||
mut request: Request<Body>,
|
||||
next: Next,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let token = cookies
|
||||
.get("token")
|
||||
.map(|cookie| cookie.value().to_string())
|
||||
.or_else(|| {
|
||||
request
|
||||
.headers()
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.and_then(|value| {
|
||||
if value.starts_with("Bearer ") {
|
||||
Some(value[7..].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let token = token.ok_or_else(|| {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": "Please provide a valid token"
|
||||
});
|
||||
(StatusCode::UNAUTHORIZED, Json(error))
|
||||
})?;
|
||||
|
||||
let claims = decode_token(
|
||||
token,
|
||||
&DecodingKey::from_secret(data.env.token_secret.as_ref()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let uuid = (&claims.subject).parse::<i64>().map_err(|_| {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": "Invalid user id"
|
||||
});
|
||||
(StatusCode::UNAUTHORIZED, Json(error))
|
||||
})?;
|
||||
|
||||
let user = sqlx::query_as::<_, LoginModel>(r#"SELECT * FROM users WHERE id = $1"#)
|
||||
.bind(uuid)
|
||||
.fetch_optional(&data.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": format!("Database error: {}", e)
|
||||
});
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
|
||||
})?;
|
||||
|
||||
let user = user.ok_or_else(|| {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": "Invalid user"
|
||||
});
|
||||
(StatusCode::UNAUTHORIZED, Json(error))
|
||||
})?;
|
||||
|
||||
request.extensions_mut().insert(user);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
@@ -2,15 +2,18 @@
|
||||
pub struct Env {
|
||||
pub db_url: String,
|
||||
pub token_secret: String,
|
||||
pub origin: String
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub fn load() -> Env {
|
||||
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let token_secret = std::env::var("TOKEN_SECRET").expect("TOKEN_SECRET must be set");
|
||||
let origin = std::env::var("ORIGIN").expect("ORIGIN must be set")
|
||||
Env {
|
||||
db_url,
|
||||
token_secret,
|
||||
origin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use argon2::{
|
||||
password_hash::{SaltString, rand_core::OsRng},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
Extension, Json,
|
||||
extract::{Path, State},
|
||||
http::{Response, StatusCode, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
@@ -18,7 +18,7 @@ use serde_json::json;
|
||||
use crate::{
|
||||
AppState,
|
||||
cookie::jwt::encode_token,
|
||||
models::{FilteredUser, LoginModel, LoginScheme, UserCreateScheme},
|
||||
models::{FilteredUser, LoginScheme, User, UserCreateScheme, UserUpdateScheme},
|
||||
};
|
||||
|
||||
pub async fn create_user(
|
||||
@@ -88,7 +88,7 @@ pub async fn login(
|
||||
State(data): State<Arc<AppState>>,
|
||||
Json(request): Json<LoginScheme>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user = sqlx::query_as::<_, LoginModel>(r#"SELECT * FROM users WHERE username = $1"#)
|
||||
let user = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE username = $1"#)
|
||||
.bind(request.username)
|
||||
.fetch_optional(&data.db)
|
||||
.await
|
||||
@@ -131,7 +131,7 @@ pub async fn login(
|
||||
.http_only(true);
|
||||
|
||||
let mut response = Response::new(
|
||||
json!({"status": "success", "token": token, "user": filter_users(&user)}).to_string(),
|
||||
json!({"status": "success", "token": token, "user": filter_user(&user)}).to_string(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
@@ -155,11 +155,159 @@ pub async fn logout() -> Result<impl IntoResponse, (StatusCode, Json<serde_json:
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn filter_users(user: &LoginModel) -> FilteredUser {
|
||||
pub async fn get_current_user(
|
||||
Extension(state): Extension<User>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let response = json!({
|
||||
"status": "success",
|
||||
"data": json!({
|
||||
"first_name": filter_user(&state).first_name,
|
||||
"last_name": filter_user(&state).last_name
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn delete_user(
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let query = sqlx::query(r#"DELETE FROM users WHERE id = $1"#)
|
||||
.bind(id)
|
||||
.execute(&data.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||
)
|
||||
})?;
|
||||
|
||||
if query.rows_affected() == 0 {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": format!("User with ID {} not found", id)
|
||||
});
|
||||
return Err((StatusCode::NOT_FOUND, Json(error)));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn get_users(
|
||||
State(data): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let users = sqlx::query_as::<_, User>(r#"SELECT * FROM users ORDER BY last_name ASC"#)
|
||||
.fetch_all(&data.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let error = json!({
|
||||
"status": "error",
|
||||
"message": format!("{:?}", e)
|
||||
});
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
|
||||
})?;
|
||||
|
||||
let response = users
|
||||
.iter()
|
||||
.map(|user| filter_user(&user))
|
||||
.collect::<Vec<FilteredUser>>();
|
||||
let json_respnse = json!(response);
|
||||
Ok(Json(json_respnse))
|
||||
}
|
||||
|
||||
pub async fn get_user_by_id(
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let query = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE id = $1"#)
|
||||
.bind(id)
|
||||
.fetch_one(&data.db)
|
||||
.await;
|
||||
|
||||
match query {
|
||||
Ok(user) => {
|
||||
let response = serde_json::json!(filter_user(&user));
|
||||
return Ok(Json(response));
|
||||
}
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
let error_response = serde_json::json!({
|
||||
"status": "fail",
|
||||
"message": format!("User with ID {} not found", id)
|
||||
});
|
||||
return Err((StatusCode::NOT_FOUND, Json(error_response)));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn update_user(
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<Arc<AppState>>,
|
||||
Json(body): Json<UserUpdateScheme>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||
let argon = Argon2::default();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hashed_pwd = match argon.hash_password(body.new_pwd.clone().as_bytes(), &salt) {
|
||||
Ok(h) => h.to_string(),
|
||||
Err(e) => panic!("Error hashing {:}", e),
|
||||
};
|
||||
|
||||
let update_result = sqlx::query(r#"UPDATE users SET first_name = $1, last_name = $2, username = $3, pwd = $4, is_admin = $5 WHERE id = $6"#)
|
||||
.bind(body.first_name.to_owned())
|
||||
.bind(body.last_name.to_owned())
|
||||
.bind(body.username.to_owned())
|
||||
.bind(hashed_pwd)
|
||||
.bind(body.make_admin.to_owned())
|
||||
.bind(id)
|
||||
.execute(&data.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||
)
|
||||
})?;
|
||||
|
||||
if update_result.rows_affected() == 0 {
|
||||
let error_response = serde_json::json!({
|
||||
"status": "error",
|
||||
"message": format!("User with ID {} not found", id)
|
||||
});
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
|
||||
}
|
||||
|
||||
let updated_user = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE id = $1"#)
|
||||
.bind(id)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"status": "error", "message": format!("{:?}", e)})),
|
||||
)
|
||||
})?;
|
||||
|
||||
let response = serde_json::json!({
|
||||
"user": filter_user(&updated_user),
|
||||
"status": "success"
|
||||
});
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
fn filter_user(user: &User) -> FilteredUser {
|
||||
FilteredUser {
|
||||
id: user.id,
|
||||
first_name: user.first_name.clone(),
|
||||
last_name: user.last_name.clone(),
|
||||
username: user.username.clone(),
|
||||
is_admin: user.is_admin.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -6,12 +6,20 @@ mod models;
|
||||
mod router;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, routing};
|
||||
use axum::{
|
||||
Router,
|
||||
http::{
|
||||
HeaderValue, Method,
|
||||
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
|
||||
},
|
||||
routing,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use models::*;
|
||||
use router::create_router;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
use crate::env::Env;
|
||||
|
||||
@@ -24,7 +32,7 @@ pub struct AppState {
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
let env = Env::load();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL variable not set");
|
||||
let database_url = &env.db_url;
|
||||
let pool = match PgPoolOptions::new().connect(&database_url).await {
|
||||
Ok(pool) => {
|
||||
println!("Database connection successful");
|
||||
@@ -35,10 +43,19 @@ async fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(env.origin.parse::<HeaderValue>().unwrap())
|
||||
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
|
||||
.allow_credentials(true)
|
||||
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
|
||||
|
||||
let app = create_router(Arc::new(AppState {
|
||||
db: pool.clone(),
|
||||
env: env.clone(),
|
||||
}));
|
||||
}))
|
||||
.layer(cors);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
}
|
||||
|
||||
+13
-10
@@ -27,11 +27,12 @@ pub struct TicketResponse {
|
||||
pub user_id: i16,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: i16,
|
||||
pub last_name: String,
|
||||
pub first_name: String,
|
||||
pub username: String,
|
||||
pub is_admin: bool,
|
||||
pub pwd: String,
|
||||
}
|
||||
@@ -49,6 +50,16 @@ pub struct TicketUpdateScheme {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct UserUpdateScheme {
|
||||
pub id: i16,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub username: String,
|
||||
pub make_admin: bool,
|
||||
pub new_pwd: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct UserCreateScheme {
|
||||
pub first_name: String,
|
||||
@@ -64,20 +75,12 @@ pub struct LoginScheme {
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct LoginModel {
|
||||
pub id: i16,
|
||||
pub last_name: String,
|
||||
pub first_name: String,
|
||||
pub is_admin: bool,
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FilteredUser {
|
||||
pub id: i16,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub username: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -8,7 +8,10 @@ use axum::{
|
||||
use crate::{
|
||||
AppState,
|
||||
handlers::{
|
||||
auth::{create_user, login, logout},
|
||||
auth::{
|
||||
create_user, delete_user, get_current_user, get_user_by_id, get_users, login, logout,
|
||||
update_user,
|
||||
},
|
||||
ticket::{create_ticket, delete_ticket, edit_ticket, get_ticket_by_id, get_tickets},
|
||||
},
|
||||
};
|
||||
@@ -26,5 +29,11 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
.route("/api/register", post(create_user))
|
||||
.route("/api/login", post(login))
|
||||
.route("/api/logout", get(logout))
|
||||
.route("/api/users", get(get_users))
|
||||
.route("/api/users/current", get(get_current_user))
|
||||
.route(
|
||||
"/api/users/{id}",
|
||||
get(get_user_by_id).delete(delete_user).patch(update_user),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user