4 Commits
Author SHA1 Message Date
schn33fuchs f63131ebc7 User listing, deleting and updating 2026-04-25 14:33:02 +02:00
schn33fuchs 6c1c13e088 Cors layer added
header, origin and method limitations
2026-04-25 14:32:22 +02:00
schn33fuchs 834a0b7aa4 Get currnet user function 2026-04-25 13:33:41 +02:00
schn33fuchs c4d374dbfe Added token validation 2026-04-25 13:21:31 +02:00
9 changed files with 303 additions and 20 deletions
+16
View File
@@ -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"
+2
View File
@@ -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
View File
@@ -1 +1,2 @@
pub mod jwt;
pub mod validation;
+84
View File
@@ -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)
}
+3
View File
@@ -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
}
}
}
+154 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}