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

@@ -1,177 +1,177 @@
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::{post, HttpRequest, HttpResponse, Responder};
use argon2::password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
};
use actix_web::{HttpRequest, HttpResponse, Responder, post};
use argon2::Argon2;
use argon2::password_hash::{
PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
};
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 serde::{Deserialize, Serialize};
use serde_json::{json, to_string};
use sqlx::{query, query_as};
use std::error::Error;
use std::fs::File;
use std::io::Read;
use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime};
use serde_json::{json, to_string};
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)]
struct UserLogin {
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,
}
use actix_web::cookie::{Cookie, SameSite};
use actix_web::cookie::time::{Duration, OffsetDateTime, UtcDateTime};
*/
#[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
let argon2 = Argon2::default();
let user = query_as!(
User,
"SELECT * FROM users WHERE username = $1",
user_login.username
Ok(
match query_as!(
User,
"SELECT * FROM users WHERE username = $1",
user_login.username
)
.fetch_optional(&app_state.database)
.await?
{
Some(user) => {
let hash = PasswordHash::new(&user.hash)?;
if argon2
.verify_password(user_login.password.as_bytes(), &hash)
.is_err()
{
return Ok(HttpResponse::BadRequest().finish());
}
// Create the JWT
let header = Header::new(Algorithm::ES256);
// Put a random KeyId
let mut rng = rand::rng();
let key_id: i64 = rng.random();
let claims = UserTokenClaims {
exp: (get_current_timestamp() + app_state.token_expiration) as usize,
kid: key_id,
uid: user.uuid.clone(),
};
let mut key = File::open("priv.pem")?;
let mut buf = vec![];
key.read_to_end(&mut buf)?;
let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf)?)?;
let user = json!({
"uuid": user.uuid,
"username": user.username,
"token": token,
});
let user_string = to_string(&user)?;
// Send the JWT as cookie
HttpResponse::Ok().body(user_string)
}
None => HttpResponse::BadRequest().finish(),
},
)
.fetch_one(&app_state.database)
.await;
if user.is_err() {
return HttpResponse::BadRequest().finish();
}
let user = user.unwrap();
let hash = PasswordHash::new(&user.hash);
if hash.is_err() {
return HttpResponse::BadRequest().finish();
}
if argon2
.verify_password(user_login.password.as_bytes(), &hash.unwrap())
.is_err()
{
return HttpResponse::BadRequest().finish();
}
// Create the JWT
let header = Header::new(Algorithm::ES256);
// Put a random KeyId
let mut rng = rand::rng();
let key_id: i64 = rng.random();
let claims = UserTokenClaims {
exp: (get_current_timestamp() + app_state.token_expiration) as usize,
kid: key_id,
uid: user.uuid.clone(),
};
let mut key = File::open("priv.pem").unwrap();
let mut buf = vec![];
key.read_to_end(&mut buf).unwrap();
let token = encode(&header, &claims, &EncodingKey::from_ec_pem(&buf).unwrap()).unwrap();
let user = json!({
"uuid": user.uuid,
"username": user.username,
"token": token,
});
// Send the JWT as cookie
HttpResponse::Ok()
.body(to_string(&user).unwrap())
}
#[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
// And remove data from client
let token = req.headers().get("Authorization");
if token.is_none() {
return HttpResponse::BadRequest().finish();
}
let token = token.unwrap();
let token = token.to_str();
if token.is_err() {
return HttpResponse::BadRequest().finish();
}
let token = token.unwrap().split_once(" ").unwrap().1;
let mut key = File::open("pub.pem").unwrap();
let mut buf = vec![];
key.read_to_end(&mut buf).unwrap();
let token = decode::<UserTokenClaims>(
token,
&DecodingKey::from_ec_pem(&buf).unwrap(),
&Validation::new(Algorithm::ES256),
);
match token {
Ok(token) => {
match req.headers().get("Authorization") {
Some(token) => {
let token = token.to_str()?;
let token = match token.split_once(" ") {
Some((_, token)) => token,
None => return Ok(HttpResponse::BadRequest().finish()),
};
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 i64;
if query!(
query!(
"INSERT INTO revoked ( token_id, user_id, expires ) VALUES ( $1, $2, $3 )",
token.claims.kid,
token.claims.uid,
exp
)
.execute(&app_state.database)
.await
.is_err()
{
return HttpResponse::InternalServerError()
.reason("Query fail")
.finish();
}
}
Err(e) => {
let message = format!("Error: {e}");
println!("{}", message);
return HttpResponse::InternalServerError()
.reason("Token caca")
.finish();
.execute(&app_state.database)
.await?;
Ok(HttpResponse::Ok().finish())
}
None => Ok(HttpResponse::BadRequest().finish()),
}
HttpResponse::Ok().finish()
}
#[post("/register")]
async fn register(user_register: Json<UserRegister>, app_state: Data<AppState>) -> impl Responder {
let uuid = Uuid::new_v4().to_string();
async fn register(
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 salt = SaltString::generate(&mut OsRng);
let hash = argon2
.hash_password(user_register.password.as_bytes(), &salt)
.unwrap()
.hash_password(user_register.password.as_bytes(), &salt)?
.to_string();
if query!(
query!(
"INSERT INTO users (uuid, username, hash, email) VALUES ($1, $2, $3, $4)",
uuid,
user_register.username,
hash,
user_register.email
)
.execute(&app_state.database)
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
};
HttpResponse::Ok().finish()
.execute(&app_state.database)
.await?;
Ok(HttpResponse::Ok().finish())
}
async fn verify_token(
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)
}