Added user login, logout and creation functionality
Also minor changes to ticketing and AppState
This commit is contained in:
@@ -13,7 +13,7 @@ pub struct Error {
|
||||
pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
|
||||
let now = chrono::Utc::now();
|
||||
let issued = now.timestamp() as usize;
|
||||
let expires = (now + chrono::Duration::minutes(90)).timestamp() as usize;
|
||||
let expires = (now + chrono::Duration::minutes(60)).timestamp() as usize;
|
||||
let claims: Claims = Claims {
|
||||
subject: id,
|
||||
issued: issued,
|
||||
|
||||
@@ -1 +1 @@
|
||||
mod jwt;
|
||||
pub mod jwt;
|
||||
|
||||
16
backend/src/env.rs
Normal file
16
backend/src/env.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Env {
|
||||
pub db_url: String,
|
||||
pub token_secret: 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");
|
||||
Env {
|
||||
db_url,
|
||||
token_secret,
|
||||
}
|
||||
}
|
||||
}
|
||||
165
backend/src/handlers/auth.rs
Normal file
165
backend/src/handlers/auth.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod auth;
|
||||
pub mod ticket;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(unused_imports)]
|
||||
mod cookie;
|
||||
mod env;
|
||||
mod handlers;
|
||||
mod models;
|
||||
mod router;
|
||||
@@ -12,13 +13,17 @@ use router::create_router;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
|
||||
use crate::env::Env;
|
||||
|
||||
pub struct AppState {
|
||||
db: PgPool,
|
||||
env: Env,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
let env = Env::load();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL variable not set");
|
||||
let pool = match PgPoolOptions::new().connect(&database_url).await {
|
||||
Ok(pool) => {
|
||||
@@ -30,7 +35,10 @@ async fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let app = create_router(Arc::new(AppState { db: pool.clone() }));
|
||||
let app = create_router(Arc::new(AppState {
|
||||
db: pool.clone(),
|
||||
env: env.clone(),
|
||||
}));
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap();
|
||||
axum::serve(listener, app).await;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ pub struct TicketUpdateScheme {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Deserialize, Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct UserCreateScheme {
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
@@ -64,6 +64,23 @@ pub struct LoginScheme {
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, sqlx::FromRow)]
|
||||
pub struct LoginModel {
|
||||
pub id: i16,
|
||||
pub first_name: String,
|
||||
pub last_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 is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub subject: String,
|
||||
|
||||
Reference in New Issue
Block a user