Added user login, logout and creation functionality

Also minor changes to ticketing and AppState
This commit is contained in:
2026-04-24 19:46:02 +02:00
parent fe04483e76
commit 51b6f89df2
10 changed files with 226 additions and 16 deletions

View File

@@ -0,0 +1,165 @@
use std::{sync::Arc, usize};
use argon2::{
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
password_hash::{SaltString, rand_core::OsRng},
};
use axum::{
Json,
extract::State,
http::{Response, StatusCode, header},
response::IntoResponse,
};
use axum_extra::extract::cookie::{Cookie, SameSite};
use chrono::format;
use jsonwebtoken::{EncodingKey, Header};
use serde_json::json;
use crate::{
AppState,
cookie::jwt::encode_token,
models::{FilteredUser, LoginModel, LoginScheme, UserCreateScheme},
};
pub async fn create_user(
State(data): State<Arc<AppState>>,
Json(request): Json<UserCreateScheme>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
if request.username.is_empty() || request.pwd.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "Missing credential"})),
));
}
let exist_check = sqlx::query_as::<_, UserCreateScheme>(
r#"SELECT firstname, name, username, is_admin, passwd FROM users WHERE username = $1"#,
)
.bind(&request.username)
.fetch_optional(&data.db)
.await
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": format!("{:?}", e)})),
)
})?;
if let Some(_) = exist_check {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "user already exists"})),
));
}
let argon = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
let hashed_pwd = match argon.hash_password(request.pwd.clone().as_bytes(), &salt) {
Ok(h) => h.to_string(),
Err(e) => panic!("Error hashing {:}", e),
};
let user = sqlx::query("INSERT INTO login (username, passwd, firstname, name, is_admin) VALUES ($1, $2, $3, $4, $5)")
.bind(request.username)
.bind(&hashed_pwd)
.bind(request.first_name)
.bind(request.last_name)
.bind(request.is_admin)
.execute(&data.db)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{}", e)})),
)
})?;
if user.rows_affected() < 1 {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": "Error creating user"})),
));
} else {
Ok(Json(json!({"status": "success", "result": "User created"})))
}
}
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"#)
.bind(request.username)
.fetch_optional(&data.db)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"status": "error", "message": format!("{}", e)})),
)
})?
.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(json!({"status": "error", "message": "Invalid username"})),
)
})?;
let pwd_hash = PasswordHash::new(&user.pwd);
let valid_pwd = Argon2::default()
.verify_password(&request.pwd.as_bytes(), &pwd_hash.unwrap())
.is_ok();
if !valid_pwd {
let error_response = serde_json::json!({
"status": "error",
"message": "Invalid password"
});
return Err((StatusCode::BAD_REQUEST, Json(error_response)));
}
let token = encode_token(
&Header::default(),
user.id.clone().to_string(),
&EncodingKey::from_secret(data.env.token_secret.as_ref()),
);
let cookie = Cookie::build(("token", token.to_owned()))
.path("/")
.max_age(time::Duration::hours(1))
.same_site(SameSite::Lax)
.http_only(true);
let mut response = Response::new(
json!({"status": "success", "token": token, "user": filter_users(&user)}).to_string(),
);
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::Lax)
.http_only(true);
let mut response = Response::new(
json!({"status": "success", "message": "successfully logged out"}).to_string(),
);
response
.headers_mut()
.insert(header::SET_COOKIE, cookie.to_string().parse().unwrap());
Ok(response)
}
fn filter_users(user: &LoginModel) -> FilteredUser {
FilteredUser {
id: user.id,
first_name: user.first_name.clone(),
last_name: user.last_name.clone(),
is_admin: user.is_admin.clone(),
}
}

View File

@@ -1 +1,2 @@
pub mod auth;
pub mod ticket;

View File

@@ -68,17 +68,18 @@ pub async fn delete_ticket(
pub async fn get_tickets(
State(data): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let tickets =
sqlx::query_as(r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#)
.fetch_all(&data.db)
.await
.map_err(|e| {
let error_response = serde_json::json!({
"status": "error",
"message": format!("Database error: {}", e),
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
})?;
let tickets = sqlx::query_as::<_, Ticket>(
r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#,
)
.fetch_all(&data.db)
.await
.map_err(|e| {
let error_response = serde_json::json!({
"status": "error",
"message": format!("Database error: {}", e),
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
})?;
let ticket_response = tickets
.iter()
@@ -144,7 +145,7 @@ pub async fn edit_ticket(
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
}
let updated_ticket = sqlx::query_as(r#"SELECT * FROM tickets WHERE id = $1"#)
let updated_ticket = sqlx::query_as::<_, Ticket>(r#"SELECT * FROM tickets WHERE id = $1"#)
.bind(id)
.fetch_one(&data.db)
.await