- add: roles to user

- add: register calls for auth server
- add: user lookup by email
- add: start of password reset
- add: Cache trait to allow redis cache mocking
This commit is contained in:
2024-11-26 01:58:26 -05:00
parent 3fc6c6252c
commit 113ab5a4ac
8 changed files with 303 additions and 26 deletions

View File

@@ -1,20 +1,33 @@
use std::error::Error;
use tonic::transport::Channel;
use crate::database::{database_service_client::DatabaseServiceClient, CreateUserRequest, CreateUserResponse, GetUserByUsernameRequest, GetUserRequest, GetUserResponse};
use crate::database::{database_service_client::DatabaseServiceClient, CreateUserRequest, CreateUserResponse, GetUserByUsernameRequest, GetUserByEmailRequest, GetUserRequest, GetUserResponse};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
#[async_trait]
pub trait DatabaseClientTrait: Sized {
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
async fn get_user_by_username(&mut self, user_id: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
async fn create_user(&mut self, username: &str, email: &str, password: &str) -> Result<CreateUserResponse, Box<dyn std::error::Error>>;
async fn store_password_reset(&mut self, email: &str, reset_token: &str, expires_at: DateTime<Utc>) -> Result<(), Box<dyn std::error::Error>>;
async fn get_password_reset(&self, reset_token: &str) -> Result<Option<PasswordReset>, Box<dyn std::error::Error>>;
async fn delete_password_reset(&self, reset_token: &str) -> Result<(), Box<dyn std::error::Error>>;
async fn update_user_password(&self, email: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone)]
pub struct DatabaseClient {
client: DatabaseServiceClient<Channel>,
}
#[derive(Debug)]
pub struct PasswordReset {
pub email: String,
pub reset_token: String,
pub expires_at: DateTime<Utc>,
}
#[async_trait]
impl DatabaseClientTrait for DatabaseClient {
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>> {
@@ -44,6 +57,14 @@ impl DatabaseClientTrait for DatabaseClient {
Ok(response.into_inner())
}
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn Error>> {
let request = tonic::Request::new(GetUserByEmailRequest {
email: email.to_string(),
});
let response = self.client.get_user_by_email(request).await?;
Ok(response.into_inner())
}
async fn create_user(&mut self, username: &str, email: &str, password: &str) -> Result<CreateUserResponse, Box<dyn Error>> {
let request = tonic::Request::new(CreateUserRequest {
username: username.to_string(),
@@ -53,4 +74,36 @@ impl DatabaseClientTrait for DatabaseClient {
let response = self.client.create_user(request).await?;
Ok(response.into_inner())
}
async fn store_password_reset(
&mut self,
email: &str,
reset_token: &str,
expires_at: DateTime<Utc>,
) -> Result<(), Box<dyn Error>> {
Ok(())
}
async fn get_password_reset(
&self,
reset_token: &str,
) -> Result<Option<PasswordReset>, Box<dyn std::error::Error>> {
todo!()
}
async fn delete_password_reset(
&self,
reset_token: &str,
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
async fn update_user_password(
&self,
email: &str,
hashed_password: &str,
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
}

View File

@@ -1,9 +1,11 @@
use chrono::{Duration, Utc};
use rand::Rng;
use tonic::{Request, Response, Status};
use crate::jwt::{generate_token, validate_token};
use crate::users::verify_user;
use crate::users::{verify_user, hash_password};
use crate::database_client::{DatabaseClient, DatabaseClientTrait};
use crate::auth::auth_service_server::{AuthService};
use crate::auth::{LoginRequest, LoginResponse, ValidateTokenRequest, ValidateTokenResponse};
use crate::auth::{LoginRequest, LoginResponse, ValidateTokenRequest, ValidateTokenResponse, RegisterRequest, RegisterResponse, PasswordResetRequest, PasswordResetResponse, ResetPasswordRequest, ResetPasswordResponse};
use tracing::{info, warn};
pub struct MyAuthService<T: DatabaseClientTrait + Clone> {
@@ -49,4 +51,101 @@ impl<T: DatabaseClientTrait + Send + Sync + Clone + 'static> AuthService for MyA
})),
}
}
async fn register(
&self,
request: Request<RegisterRequest>,
) -> Result<Response<RegisterResponse>, Status> {
let req = request.into_inner();
// Hash the password
let hashed_password = hash_password(&req.password);
// Create user in the database
let user = self.db_client.clone().create_user(&req.username, &req.email, &hashed_password)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
Ok(Response::new(RegisterResponse { user_id: user.user_id }))
}
async fn request_password_reset(
&self,
request: Request<PasswordResetRequest>,
) -> Result<Response<PasswordResetResponse>, Status> {
let email = request.into_inner().email;
let user = self.db_client.clone().get_user_by_email(&email).await;
// Check if the email exists
if user.ok().is_some() {
// Generate a reset token
let reset_token: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(32)
.map(char::from)
.collect();
// Set token expiration (e.g., 1 hour)
let expires_at = Utc::now() + Duration::hours(1);
// Store the reset token in the database
self.db_client.clone()
.store_password_reset(&email, &reset_token, expires_at)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
// Send the reset email
// send_email(&email, "Password Reset Request", &format!(
// "Click the link to reset your password: https://example.com/reset?token={}",
// reset_token
// ))
// .map_err(|e| Status::internal(format!("Email error: {}", e)))?;
Ok(Response::new(PasswordResetResponse {
message: "Password reset email sent".to_string(),
}))
} else {
// Respond with a generic message to avoid information leaks
Ok(Response::new(PasswordResetResponse {
message: "If the email exists, a reset link has been sent.".to_string(),
}))
}
}
async fn reset_password(
&self,
request: Request<ResetPasswordRequest>,
) -> Result<Response<ResetPasswordResponse>, Status> {
let req = request.into_inner();
// Validate the reset token
if let Some(password_reset) = self.db_client.clone().get_password_reset(&req.reset_token).await
.map_err(|e| Status::internal(format!("Database error: {}", e)))? {
if password_reset.expires_at < Utc::now() {
return Err(Status::unauthenticated("Token expired"));
}
// Hash the new password
let hashed_password = hash_password(&req.new_password);
// Update the user's password
self.db_client
.update_user_password(&password_reset.email, &hashed_password)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
// Delete the reset token
self.db_client
.delete_password_reset(&req.reset_token)
.await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
Ok(Response::new(ResetPasswordResponse {
message: "Password successfully reset".to_string(),
}))
} else {
Err(Status::unauthenticated("Invalid reset token"))
}
}
}

View File

@@ -1,7 +1,9 @@
use std::error::Error;
use mockall::{mock, predicate::*};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use crate::database::{CreateUserResponse, GetUserResponse};
use crate::database_client::{DatabaseClientTrait};
use crate::database_client::{DatabaseClientTrait, PasswordReset};
#[cfg(test)]
mock! {
@@ -12,7 +14,12 @@ mock! {
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
async fn get_user_by_username(&mut self, user_id: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn Error>>;
async fn create_user(&mut self, username: &str, email: &str, password: &str) -> Result<CreateUserResponse, Box<dyn std::error::Error>>;
async fn store_password_reset(&mut self, email: &str, reset_token: &str, expires_at: DateTime<Utc>) -> Result<(), Box<dyn Error>>;
async fn get_password_reset(&self, reset_token: &str) -> Result<Option<PasswordReset>, Box<dyn Error>>;
async fn delete_password_reset(&self, reset_token: &str) -> Result<(), Box<dyn Error>>;
async fn update_user_password(&self, email: &str, hashed_password: &str) -> Result<(), Box<dyn Error>>;
}
}