feat: auto-verify demo accounts for payment gateway integration
- Auto-verifies emails for accounts ending with @demo.com - Auto-approves COMPANY role for demo accounts - Skips email verification and OTP for demo accounts - Auto-approves profile verification for demo accounts - Allows login without email verification for demo accounts This enables payment gateway companies to login directly and view packages.
This commit is contained in:
parent
0bda2b2f10
commit
b2c93f4e33
3 changed files with 186 additions and 67 deletions
|
|
@ -296,6 +296,9 @@ async fn register(
|
|||
}
|
||||
})?;
|
||||
|
||||
// Check if this is a demo account (payment gateway integration)
|
||||
let is_demo_account = email.ends_with("@demo.com") || email == "paymentgateway@demo.com";
|
||||
|
||||
// Assign signup role immediately (intent-driven). Email verification is still required for login.
|
||||
let role_candidates = resolve_signup_role_candidates(
|
||||
payload.intent.as_deref(),
|
||||
|
|
@ -304,22 +307,25 @@ async fn register(
|
|||
for role_key in role_candidates {
|
||||
let role_id = ensure_role_exists(&state.pool, &role_key).await;
|
||||
if let Some(role_id) = role_id {
|
||||
// For demo accounts, auto-approve the role immediately
|
||||
let status = if is_demo_account { "APPROVED" } else { "PENDING" };
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE user_role_assignments
|
||||
SET status = 'APPROVED'
|
||||
SET status = $3
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(role_id)
|
||||
.bind(status)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_role_assignments (user_id, role_id, status)
|
||||
SELECT $1, $2, 'APPROVED'
|
||||
SELECT $1, $2, $3
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_role_assignments WHERE user_id = $1 AND role_id = $2
|
||||
)
|
||||
|
|
@ -327,12 +333,36 @@ async fn register(
|
|||
)
|
||||
.bind(user.id)
|
||||
.bind(role_id)
|
||||
.execute(&state.pool)
|
||||
.bind(status)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For demo accounts: auto-verify email and skip OTP
|
||||
if is_demo_account {
|
||||
tracing::info!(email = %email, "Demo account auto-verified");
|
||||
let _ = sqlx::query(
|
||||
"UPDATE users SET email_verified = true, status = 'ACTIVE' WHERE id = $1"
|
||||
)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
// Return success with demo flag
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
return Ok((StatusCode::CREATED, Json(RegisterResponse {
|
||||
user_id: user.id.to_string(),
|
||||
email: user.email,
|
||||
phone: None,
|
||||
name: user_name,
|
||||
status: "ACTIVE".to_string(),
|
||||
email_verified: true,
|
||||
created_at: user.created_at.to_rfc3339(),
|
||||
otp: Some("DEMO".to_string()), // Return dummy OTP for demo
|
||||
})));
|
||||
}
|
||||
|
||||
// Store OTP in Redis (15-min TTL, keyed by code → user_id)
|
||||
let otp = format!("{:06}", rand::random::<u32>() % 1_000_000);
|
||||
tracing::info!(otp = %otp, email = %email, "OTP generated for registration");
|
||||
|
|
@ -384,7 +414,10 @@ async fn login(
|
|||
if user.status == "SUSPENDED" {
|
||||
return Err(err(StatusCode::FORBIDDEN, "Account suspended", "ACCOUNT_SUSPENDED"));
|
||||
}
|
||||
if !user.email_verified {
|
||||
|
||||
// Allow demo accounts to login without email verification
|
||||
let is_demo_account = email.ends_with("@demo.com") || email == "paymentgateway@demo.com";
|
||||
if !user.email_verified && !is_demo_account {
|
||||
return Err(err(StatusCode::UNAUTHORIZED, "Email not verified. Check your inbox.", "EMAIL_NOT_VERIFIED"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -305,75 +305,137 @@ async fn submit_for_verification(
|
|||
) -> impl IntoResponse {
|
||||
let role_key = input.role_key.to_uppercase();
|
||||
|
||||
// Guard: reject if an active verification already exists
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
// Check if user is a demo account
|
||||
let is_demo = sqlx::query_scalar::<_, String>("SELECT email FROM users WHERE id = $1")
|
||||
.bind(auth.user_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map(|email| email.ends_with("@demo.com") || email == "paymentgateway@demo.com")
|
||||
.unwrap_or(false);
|
||||
|
||||
if existing.unwrap_or(None).is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({
|
||||
"error": "A verification is already in progress for this role. Please wait for it to be reviewed."
|
||||
})),
|
||||
// For demo accounts: auto-approve verification
|
||||
if is_demo {
|
||||
tracing::info!(user_id = %auth.user_id, role_key = %role_key, "Demo account auto-approved for verification");
|
||||
|
||||
// Update role assignment to APPROVED
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'APPROVED' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Mark profile as VERIFIED
|
||||
set_profile_status(&state, auth.user_id, &role_key, "VERIFIED").await;
|
||||
|
||||
// Create a verification record with APPROVED status
|
||||
let profile_data = input.profile_data.unwrap_or_else(|| {
|
||||
serde_json::json!({
|
||||
"company_name": "Payment Gateway Demo Company",
|
||||
"company_description": "Demo account for reviewing packages",
|
||||
"industry": "Technology",
|
||||
"location": "India"
|
||||
})
|
||||
});
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
match VerificationRepository::create_approved(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Fetch saved profile data or use submitted data
|
||||
let profile_data = match input.profile_data {
|
||||
Some(data) => data,
|
||||
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
||||
};
|
||||
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
// Mark profile as PENDING in role-specific table
|
||||
set_profile_status(&state, auth.user_id, &role_key, "PENDING").await;
|
||||
|
||||
// Mark user_role as PENDING
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": "APPROVED",
|
||||
"message": "Your profile has been auto-approved for demo access."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
} else {
|
||||
// Regular verification flow for non-demo accounts
|
||||
// Guard: reject if an active verification already exists
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
"MEDIUM",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": v.status,
|
||||
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
||||
})),
|
||||
if existing.unwrap_or(None).is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({
|
||||
"error": "A verification is already in progress for this role. Please wait for it to be reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Fetch saved profile data or use submitted data
|
||||
let profile_data = match input.profile_data {
|
||||
Some(data) => data,
|
||||
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
||||
};
|
||||
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
// Mark profile as PENDING in role-specific table
|
||||
set_profile_status(&state, auth.user_id, &role_key, "PENDING").await;
|
||||
|
||||
// Mark user_role as PENDING
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
"MEDIUM",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": v.status,
|
||||
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,30 @@ impl VerificationRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn create_approved(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
role_key: &str,
|
||||
case_type: &str,
|
||||
payload: serde_json::Value,
|
||||
documents: serde_json::Value,
|
||||
) -> Result<Verification, sqlx::Error> {
|
||||
sqlx::query_as::<_, Verification>(
|
||||
r#"
|
||||
INSERT INTO verifications (user_id, role_key, case_type, priority, status, payload, documents, reviewed_at, reviewer_notes)
|
||||
VALUES ($1, $2, $3, 'MEDIUM', 'APPROVED', $4, $5, NOW(), 'Auto-approved for demo account')
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role_key)
|
||||
.bind(case_type)
|
||||
.bind(payload)
|
||||
.bind(documents)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Verification>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1")
|
||||
.bind(id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue