Skip to content

Commit f683bf5

Browse files
authored
Merge pull request #7 from intelliDean/rev
added email verification
2 parents e8c12c8 + 53e4082 commit f683bf5

27 files changed

Lines changed: 603 additions & 123 deletions

File tree

crates/api/src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::config::swagger_config::ApiDoc;
22
use crate::handlers::{
33
add_bank::add_bank_account,
44
all_banks::all_banks,
5+
audit_logs::get_user_audit_logs,
56
current_user::current_user_details,
67
exchange_rate::get_exchange_rate as get_exchange_rate_handler,
78
get_transaction::get_transactions,
@@ -21,6 +22,7 @@ use crate::handlers::{
2122
user_bank_accounts::user_bank_accounts,
2223
user_transaction::get_user_transaction,
2324
user_wallets::get_user_wallets,
25+
verify_email::{resend_verification, verify_email},
2426
withdraw::withdraw,
2527
};
2628
use axum::{
@@ -138,6 +140,8 @@ fn create_secured_routers(state: &Arc<AppState>) -> Router<Arc<AppState>> {
138140
"/api/wallet/withdraw/{bank_account_id}",
139141
axum::routing::post(withdraw),
140142
)
143+
.route("/api/user/audit-logs", get(get_user_audit_logs))
144+
.route("/api/auth/resend-verification", post(resend_verification))
141145
.layer(middleware::from_fn_with_state(
142146
state.clone(),
143147
SecurityConfig::auth_middleware,
@@ -161,6 +165,7 @@ fn create_public_routers(metric_handle: PrometheusHandle) -> Router<Arc<AppState
161165
.route("/api/users/resolve", get(resolve_user))
162166
.route("/api/exchange-rate", get(get_exchange_rate_handler))
163167
.route("/api/health", axum::routing::get(health_check))
168+
.route("/api/auth/verify-email", get(verify_email))
164169
}
165170

166171
async fn https_redirect_middleware(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use axum::{extract::Query, extract::State, Extension, Json};
2+
use payego_core::app_state::AppState;
3+
use payego_core::repositories::audit_repository::AuditLogRepository;
4+
use payego_core::security::Claims;
5+
use payego_primitives::error::ApiError;
6+
use serde::Deserialize;
7+
use std::sync::Arc;
8+
9+
#[derive(Deserialize)]
10+
pub struct AuditLogQuery {
11+
pub page: Option<i64>,
12+
pub size: Option<i64>,
13+
}
14+
15+
pub async fn get_user_audit_logs(
16+
State(state): State<Arc<AppState>>,
17+
Extension(claims): Extension<Claims>,
18+
Query(query): Query<AuditLogQuery>,
19+
) -> Result<Json<serde_json::Value>, ApiError> {
20+
let user_id = claims.user_id()?;
21+
let limit = query.size.unwrap_or(20).min(100);
22+
let offset = (query.page.unwrap_or(1) - 1) * limit;
23+
24+
let mut conn = state
25+
.db
26+
.get()
27+
.map_err(|e| ApiError::DatabaseConnection(e.to_string()))?;
28+
29+
let logs = AuditLogRepository::find_by_user_paginated(&mut conn, user_id, limit, offset)?;
30+
31+
Ok(Json(serde_json::json!({
32+
"status": "success",
33+
"data": logs,
34+
"page": query.page.unwrap_or(1),
35+
"limit": limit
36+
})))
37+
}

crates/api/src/handlers/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod add_bank;
22
pub mod all_banks;
3+
pub mod audit_logs;
34
pub mod current_user;
45
pub mod delete_bank;
56
pub mod exchange_rate;
@@ -16,7 +17,6 @@ pub mod refresh_token;
1617
pub mod register;
1718
pub mod resolve_account;
1819
pub mod resolve_user;
19-
pub mod send_verification;
2020
pub mod social_login;
2121
pub mod stripe_webhook;
2222
pub mod top_up;
@@ -25,4 +25,5 @@ pub mod transfer_internal;
2525
pub mod user_bank_accounts;
2626
pub mod user_transaction;
2727
pub mod user_wallets;
28+
pub mod verify_email;
2829
pub mod withdraw;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use axum::{extract::Query, extract::State, Extension, Json};
2+
use payego_core::app_state::AppState;
3+
use payego_core::security::Claims;
4+
use payego_core::services::auth_service::verification::VerificationService;
5+
use payego_primitives::error::{ApiError, AuthError};
6+
use serde::Deserialize;
7+
use std::sync::Arc;
8+
9+
#[derive(Deserialize)]
10+
pub struct VerifyEmailQuery {
11+
pub token: String,
12+
}
13+
14+
pub async fn verify_email(
15+
State(state): State<Arc<AppState>>,
16+
Query(query): Query<VerifyEmailQuery>,
17+
) -> Result<Json<serde_json::Value>, ApiError> {
18+
VerificationService::verify_email(&state, &query.token).await?;
19+
20+
Ok(Json(serde_json::json!({
21+
"status": "success",
22+
"message": "Email verified successfully"
23+
})))
24+
}
25+
26+
pub async fn resend_verification(
27+
State(state): State<Arc<AppState>>,
28+
Extension(claims): Extension<Claims>,
29+
) -> Result<Json<serde_json::Value>, ApiError> {
30+
let user_id = claims.user_id()?;
31+
32+
let mut conn = state
33+
.db
34+
.get()
35+
.map_err(|e| ApiError::DatabaseConnection(e.to_string()))?;
36+
let user =
37+
payego_core::repositories::user_repository::UserRepository::find_by_id(&mut conn, user_id)?
38+
.ok_or_else(|| ApiError::Auth(AuthError::InternalError("User not found".into())))?;
39+
40+
VerificationService::send_verification_email(&state, user_id, &user.email).await?;
41+
42+
Ok(Json(serde_json::json!({
43+
"status": "success",
44+
"message": "Verification email resent"
45+
})))
46+
}

crates/core/src/repositories/audit_repository.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,19 @@ impl AuditLogRepository {
1313
.map_err(ApiError::Database)?;
1414
Ok(())
1515
}
16+
17+
pub fn find_by_user_paginated(
18+
conn: &mut PgConnection,
19+
user_id: uuid::Uuid,
20+
limit: i64,
21+
offset: i64,
22+
) -> Result<Vec<payego_primitives::models::entities::audit_log::AuditLog>, ApiError> {
23+
audit_logs::table
24+
.filter(audit_logs::user_id.eq(user_id))
25+
.order(audit_logs::created_at.desc())
26+
.limit(limit)
27+
.offset(offset)
28+
.load::<payego_primitives::models::entities::audit_log::AuditLog>(conn)
29+
.map_err(ApiError::Database)
30+
}
1631
}

crates/core/src/repositories/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pub mod bank_repository;
44
pub mod token_repository;
55
pub mod transaction_repository;
66
pub mod user_repository;
7+
pub mod verification_repository;
78
pub mod wallet_repository;

crates/core/src/repositories/user_repository.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,12 @@ impl UserRepository {
5555
}
5656
})
5757
}
58+
59+
pub fn mark_email_verified(conn: &mut PgConnection, user_id: Uuid) -> Result<(), ApiError> {
60+
diesel::update(users::table.find(user_id))
61+
.set(users::email_verified_at.eq(chrono::Utc::now()))
62+
.execute(conn)
63+
.map(|_| ())
64+
.map_err(ApiError::Database)
65+
}
5866
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use crate::repositories::user_repository::UserRepository;
2+
use diesel::prelude::*;
3+
use payego_primitives::error::{ApiError, AuthError};
4+
use payego_primitives::models::entities::verification_token::{
5+
NewVerificationToken, VerificationToken,
6+
};
7+
use payego_primitives::schema::verification_tokens;
8+
use uuid::Uuid;
9+
10+
pub struct VerificationRepository;
11+
12+
impl VerificationRepository {
13+
pub fn create(
14+
conn: &mut PgConnection,
15+
new_token: NewVerificationToken,
16+
) -> Result<VerificationToken, ApiError> {
17+
diesel::insert_into(verification_tokens::table)
18+
.values(&new_token)
19+
.get_result(conn)
20+
.map_err(ApiError::Database)
21+
}
22+
23+
pub fn find_by_token(
24+
conn: &mut PgConnection,
25+
token_hash: &str,
26+
) -> Result<Option<VerificationToken>, ApiError> {
27+
verification_tokens::table
28+
.filter(verification_tokens::token_hash.eq(token_hash))
29+
.first::<VerificationToken>(conn)
30+
.optional()
31+
.map_err(ApiError::Database)
32+
}
33+
34+
pub fn delete_for_user(conn: &mut PgConnection, user_id: Uuid) -> Result<(), ApiError> {
35+
diesel::delete(verification_tokens::table.filter(verification_tokens::user_id.eq(user_id)))
36+
.execute(conn)
37+
.map(|_| ())
38+
.map_err(ApiError::Database)
39+
}
40+
41+
pub fn consume_token(
42+
conn: &mut PgConnection,
43+
token_hash: &str,
44+
) -> Result<VerificationToken, ApiError> {
45+
let token = Self::find_by_token(conn, token_hash)?.ok_or_else(|| {
46+
ApiError::Auth(AuthError::VerificationError(
47+
"Invalid or expired verification token".into(),
48+
))
49+
})?;
50+
51+
if token.expires_at < chrono::Utc::now().naive_utc() {
52+
return Err(ApiError::Auth(AuthError::VerificationError(
53+
"Verification token has expired".into(),
54+
)));
55+
}
56+
57+
// Verify user and delete token
58+
UserRepository::mark_email_verified(conn, token.user_id)?;
59+
Self::delete_for_user(conn, token.user_id)?;
60+
61+
Ok(token)
62+
}
63+
}

crates/core/src/services/auth_service/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod logout;
33
pub mod register;
44
pub mod token;
55
pub mod user;
6+
pub mod verification;

crates/core/src/services/auth_service/register.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ impl RegisterService {
5353
ApiError::Internal("Authentication service error".into())
5454
})?;
5555

56+
// 📧 Trigger Email Verification
57+
let _ = Box::pin(crate::services::auth_service::verification::VerificationService::send_verification_email(
58+
state,
59+
user.id,
60+
&user.email,
61+
))
62+
.await
63+
.map_err(|e| {
64+
error!(user_id = %user.id, "Failed to send verification email: {}", e);
65+
});
66+
5667
let _ = AuditService::log_event(
5768
state,
5869
Some(user.id),

0 commit comments

Comments
 (0)