Refactored user code

This commit is contained in:
2025-03-27 21:55:50 +01:00
parent 5f357d405b
commit 5bc0e08f59
2 changed files with 162 additions and 134 deletions

View File

@@ -28,3 +28,31 @@ pub struct Item {
pub image: String, pub image: String,
pub price: f64, pub price: f64,
} }
#[derive(Serialize, Deserialize)]
pub struct UserLogin {
pub username: String,
pub password: String,
}
#[derive(Serialize, Deserialize)]
pub struct UserRegister {
pub username: String,
pub password: String,
pub email: String,
}
pub struct User {
pub id: i64,
pub uuid: String,
pub username: String,
pub hash: String,
pub email: String,
}
#[derive(Serialize, Deserialize)]
pub struct UserTokenClaims {
pub exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp)
pub kid: i64,
pub uid: String,
}

View File

@@ -1,80 +1,52 @@
use crate::AppState; use crate::AppState;
use actix_web::cookie::{Cookie, SameSite}; use crate::types::{User, UserLogin, UserRegister, UserTokenClaims};
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse, Responder}; use actix_web::{HttpRequest, HttpResponse, Responder, post};
use argon2::password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
};
use argon2::Argon2; use argon2::Argon2;
use argon2::password_hash::{
PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
};
use jsonwebtoken::{ use jsonwebtoken::{
decode, encode, get_current_timestamp, Algorithm, DecodingKey, EncodingKey, Header, Validation, Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, get_current_timestamp,
}; };
use rand::Rng; use rand::Rng;
use serde::{Deserialize, Serialize}; use serde_json::{json, to_string};
use sqlx::{query, query_as}; use sqlx::{query, query_as};
use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime};
use serde_json::{json, to_string};
use uuid::Uuid; use uuid::Uuid;
/*
Will use cookies in final version of app
In the meanwhile, we'll just save the token in the userStore
#[derive(Serialize, Deserialize)] use actix_web::cookie::{Cookie, SameSite};
struct UserLogin { use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime};
username: String, */
password: String,
}
#[derive(Serialize, Deserialize)]
struct UserRegister {
username: String,
password: String,
email: String,
}
struct User {
id: i64,
uuid: String,
username: String,
hash: String,
email: String,
}
struct WebUser {
uuid: String,
username: String,
}
#[derive(Serialize, Deserialize)]
struct UserTokenClaims {
exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp)
kid: i64,
uid: String,
}
#[post("/login")] #[post("/login")]
async fn login(user_login: Json<UserLogin>, app_state: Data<AppState>) -> impl Responder { async fn login(
user_login: Json<UserLogin>,
app_state: Data<AppState>,
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
// Verify that the password is correct // Verify that the password is correct
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let user = query_as!( Ok(
match query_as!(
User, User,
"SELECT * FROM users WHERE username = $1", "SELECT * FROM users WHERE username = $1",
user_login.username user_login.username
) )
.fetch_one(&app_state.database) .fetch_optional(&app_state.database)
.await; .await?
if user.is_err() { {
return HttpResponse::BadRequest().finish(); Some(user) => {
} let hash = PasswordHash::new(&user.hash)?;
let user = user.unwrap();
let hash = PasswordHash::new(&user.hash);
if hash.is_err() {
return HttpResponse::BadRequest().finish();
}
if argon2 if argon2
.verify_password(user_login.password.as_bytes(), &hash.unwrap()) .verify_password(user_login.password.as_bytes(), &hash)
.is_err() .is_err()
{ {
return HttpResponse::BadRequest().finish(); return Ok(HttpResponse::BadRequest().finish());
} }
// Create the JWT // Create the JWT
let header = Header::new(Algorithm::ES256); let header = Header::new(Algorithm::ES256);
@@ -86,81 +58,80 @@ async fn login(user_login: Json<UserLogin>, app_state: Data<AppState>) -> impl R
kid: key_id, kid: key_id,
uid: user.uuid.clone(), uid: user.uuid.clone(),
}; };
let mut key = File::open("priv.pem").unwrap(); let mut key = File::open("priv.pem")?;
let mut buf = vec![]; let mut buf = vec![];
key.read_to_end(&mut buf).unwrap(); key.read_to_end(&mut buf)?;
let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf).unwrap()).unwrap(); let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf)?)?;
let user = json!({ let user = json!({
"uuid": user.uuid, "uuid": user.uuid,
"username": user.username, "username": user.username,
"token": token, "token": token,
}); });
let user_string = to_string(&user)?;
// Send the JWT as cookie // Send the JWT as cookie
HttpResponse::Ok() HttpResponse::Ok().body(user_string)
.body(to_string(&user).unwrap()) }
None => HttpResponse::BadRequest().finish(),
},
)
} }
#[post("/logout")] #[post("/logout")]
async fn logout(req: HttpRequest, app_state: Data<AppState>) -> impl Responder { async fn logout(
req: HttpRequest,
app_state: Data<AppState>,
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
// Put the (KeyId, User) pair in the revoked table // Put the (KeyId, User) pair in the revoked table
// And remove data from client // And remove data from client
let token = req.headers().get("Authorization"); match req.headers().get("Authorization") {
if token.is_none() { Some(token) => {
return HttpResponse::BadRequest().finish(); let token = token.to_str()?;
} let token = match token.split_once(" ") {
let token = token.unwrap(); Some((_, token)) => token,
let token = token.to_str(); None => return Ok(HttpResponse::BadRequest().finish()),
if token.is_err() { };
return HttpResponse::BadRequest().finish(); let mut key = File::open("pub.pem")?;
}
let token = token.unwrap().split_once(" ").unwrap().1;
let mut key = File::open("pub.pem").unwrap();
let mut buf = vec![]; let mut buf = vec![];
key.read_to_end(&mut buf).unwrap(); key.read_to_end(&mut buf)?;
let token = decode::<UserTokenClaims>( let token = decode::<UserTokenClaims>(
token, token,
&DecodingKey::from_ec_pem(&buf).unwrap(), &DecodingKey::from_ec_pem(&buf).unwrap(),
&Validation::new(Algorithm::ES256), &Validation::new(Algorithm::ES256),
); )?;
match token {
Ok(token) => {
let exp = token.claims.exp as i64; let exp = token.claims.exp as i64;
if query!( query!(
"INSERT INTO revoked ( token_id, user_id, expires ) VALUES ( $1, $2, $3 )", "INSERT INTO revoked ( token_id, user_id, expires ) VALUES ( $1, $2, $3 )",
token.claims.kid, token.claims.kid,
token.claims.uid, token.claims.uid,
exp exp
) )
.execute(&app_state.database) .execute(&app_state.database)
.await .await?;
.is_err() Ok(HttpResponse::Ok().finish())
{
return HttpResponse::InternalServerError()
.reason("Query fail")
.finish();
} }
None => Ok(HttpResponse::BadRequest().finish()),
} }
Err(e) => {
let message = format!("Error: {e}");
println!("{}", message);
return HttpResponse::InternalServerError()
.reason("Token caca")
.finish();
}
}
HttpResponse::Ok().finish()
} }
#[post("/register")] #[post("/register")]
async fn register(user_register: Json<UserRegister>, app_state: Data<AppState>) -> impl Responder { async fn register(
let uuid = Uuid::new_v4().to_string(); user_register: Json<UserRegister>,
app_state: Data<AppState>,
) -> Result<impl Responder, Box<(dyn Error + 'static)>> {
let mut uuid = Uuid::new_v4().to_string();
while query!("SELECT (uuid) FROM users WHERE uuid = $1", uuid)
.fetch_optional(&app_state.database)
.await?
.is_some()
{
uuid = Uuid::new_v4().to_string();
}
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hash = argon2 let hash = argon2
.hash_password(user_register.password.as_bytes(), &salt) .hash_password(user_register.password.as_bytes(), &salt)?
.unwrap()
.to_string(); .to_string();
if query!( query!(
"INSERT INTO users (uuid, username, hash, email) VALUES ($1, $2, $3, $4)", "INSERT INTO users (uuid, username, hash, email) VALUES ($1, $2, $3, $4)",
uuid, uuid,
user_register.username, user_register.username,
@@ -168,10 +139,39 @@ async fn register(user_register: Json<UserRegister>, app_state: Data<AppState>)
user_register.email user_register.email
) )
.execute(&app_state.database) .execute(&app_state.database)
.await .await?;
.is_err() Ok(HttpResponse::Ok().finish())
{ }
return HttpResponse::InternalServerError().finish();
}; async fn verify_token(
HttpResponse::Ok().finish() app_state: Data<AppState>,
token: &str,
) -> Result<bool, Box<(dyn Error + 'static)>> {
let mut key = File::open("pub.pem")?;
let mut buf = vec![];
key.read_to_end(&mut buf)?;
let token = decode::<UserTokenClaims>(
token,
&DecodingKey::from_ec_pem(&buf).unwrap(),
&Validation::new(Algorithm::ES256),
)?;
let exp = token.claims.exp as u64;
let now = get_current_timestamp();
if exp > now {
return Ok(false);
}
let kid = token.claims.kid;
let uid = token.claims.uid;
if query!(
"SELECT token_id FROM revoked WHERE token_id = $1 AND user_id = $2",
kid,
uid
)
.fetch_optional(&app_state.database)
.await?
.is_some()
{
return Ok(false);
}
Ok(true)
} }