Added user login, logout and creation functionality
Also minor changes to ticketing and AppState
This commit is contained in:
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@@ -148,6 +148,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ dotenv = "0.15.0"
|
|||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
jsonwebtoken = "10.3.0"
|
jsonwebtoken = "10.3.0"
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
time = "0.3.47"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub struct Error {
|
|||||||
pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
|
pub fn encode_token(header: &Header, id: String, key: &EncodingKey) -> String {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let issued = now.timestamp() as usize;
|
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 {
|
let claims: Claims = Claims {
|
||||||
subject: id,
|
subject: id,
|
||||||
issued: issued,
|
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;
|
pub mod ticket;
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ pub async fn delete_ticket(
|
|||||||
pub async fn get_tickets(
|
pub async fn get_tickets(
|
||||||
State(data): State<Arc<AppState>>,
|
State(data): State<Arc<AppState>>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let tickets =
|
let tickets = sqlx::query_as::<_, Ticket>(
|
||||||
sqlx::query_as(r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#)
|
r#"SELECT * FROM tickets WHERE status <> 'Archived' ORDER BY date DESC"#,
|
||||||
|
)
|
||||||
.fetch_all(&data.db)
|
.fetch_all(&data.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -144,7 +145,7 @@ pub async fn edit_ticket(
|
|||||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
|
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)
|
.bind(id)
|
||||||
.fetch_one(&data.db)
|
.fetch_one(&data.db)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
mod cookie;
|
mod cookie;
|
||||||
|
mod env;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod models;
|
mod models;
|
||||||
mod router;
|
mod router;
|
||||||
@@ -12,13 +13,17 @@ use router::create_router;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
use crate::env::Env;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
db: PgPool,
|
db: PgPool,
|
||||||
|
env: Env,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
let env = Env::load();
|
||||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL variable not set");
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL variable not set");
|
||||||
let pool = match PgPoolOptions::new().connect(&database_url).await {
|
let pool = match PgPoolOptions::new().connect(&database_url).await {
|
||||||
Ok(pool) => {
|
Ok(pool) => {
|
||||||
@@ -30,7 +35,10 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
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();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8001").await.unwrap();
|
||||||
axum::serve(listener, app).await;
|
axum::serve(listener, app).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub struct TicketUpdateScheme {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug, sqlx::FromRow)]
|
||||||
pub struct UserCreateScheme {
|
pub struct UserCreateScheme {
|
||||||
pub first_name: String,
|
pub first_name: String,
|
||||||
pub last_name: String,
|
pub last_name: String,
|
||||||
@@ -64,6 +64,23 @@ pub struct LoginScheme {
|
|||||||
pub pwd: String,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user