fix(auth): use 'name' column instead of 'full_name', combine first_name + last_name

- Replace full_name with name in User struct and all queries
- RegisterPayload now takes first_name + last_name instead of full_name
- Combine first_name and last_name into name before saving to DB
- Update all response structs to use 'name' field instead of 'full_name'
- Fix support and dashboard queries to use u.name instead of u.full_name

Root cause: DB has 'name' column, code was using 'full_name' which doesn't exist.
This commit is contained in:
Tracewebstudio Dev 2026-04-13 16:55:09 +02:00
parent f5130569e5
commit 231ff9530f
9 changed files with 50 additions and 47 deletions

View file

@ -49,12 +49,12 @@ async fn list_users(
// Generic list: users + their approved roles
r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
u.id, u.email, u.name, u.status, u.created_at,
COALESCE(array_agg(r.key) FILTER (WHERE r.key IS NOT NULL), '{}') as roles
FROM users u
LEFT JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
LEFT JOIN roles r ON r.id = ur.role_id
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
GROUP BY u.id
ORDER BY u.created_at DESC
LIMIT 100
@ -80,11 +80,11 @@ async fn list_users(
format!(
r#"
SELECT
u.id, u.email, u.full_name, p.status, u.created_at,
u.id, u.email, u.name, p.status, u.created_at,
ARRAY['{}']::text[] as roles
FROM users u
JOIN {} p ON p.user_id = u.id
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 100
"#,
@ -110,12 +110,12 @@ async fn list_customers(
let sql = r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
u.id, u.email, u.name, u.status, u.created_at,
ARRAY['CUSTOMER']::text[] as roles
FROM users u
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
JOIN roles r ON r.id = ur.role_id AND r.key = 'CUSTOMER'
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 50
"#;
@ -138,12 +138,12 @@ async fn list_candidates(
let sql = r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
u.id, u.email, u.name, u.status, u.created_at,
ARRAY['JOB_SEEKER']::text[] as roles
FROM users u
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
JOIN roles r ON r.id = ur.role_id AND r.key = 'JOB_SEEKER'
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 50
"#;

View file

@ -94,7 +94,7 @@ async fn get_submission(
Json(serde_json::json!({
"user": {
"id": user.id,
"name": user.full_name,
"name": user.name,
"email": user.email,
"phone": user.phone,
"status": user.status,
@ -247,7 +247,7 @@ async fn activate_profile_after_final_approval(
.mail
.send_approval_approved_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&display,
)
.await;
@ -303,7 +303,7 @@ async fn reject_profile_after_final_approval(
.mail
.send_approval_rejected_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&display,
reason.unwrap_or("Rejected by final approval"),
)
@ -440,7 +440,7 @@ async fn approve_job(
.await;
let company_info = sqlx::query_as::<_, (String, String)>(
"SELECT u.full_name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
"SELECT u.name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
)
.bind(existing.company_id)
.fetch_optional(&state.pool)
@ -490,7 +490,7 @@ async fn reject_job(
.await;
let company_info = sqlx::query_as::<_, (String, String)>(
"SELECT u.full_name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
"SELECT u.name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
)
.bind(existing.company_id)
.fetch_optional(&state.pool)

View file

@ -35,7 +35,8 @@ pub fn router() -> Router<AppState> {
#[derive(Deserialize)]
pub struct RegisterPayload {
pub full_name: String,
pub first_name: String,
pub last_name: String,
pub email: String,
pub phone: Option<String>,
pub password: String,
@ -91,7 +92,7 @@ pub struct RegisterResponse {
pub user_id: String,
pub email: String,
pub phone: Option<String>,
pub full_name: String,
pub name: String,
pub status: String,
pub email_verified: bool,
pub created_at: String,
@ -101,7 +102,7 @@ pub struct RegisterResponse {
pub struct SessionUser {
pub id: String,
pub email: String,
pub full_name: String,
pub name: String,
pub email_verified: bool,
pub roles: Vec<String>,
pub active_role: Option<String>,
@ -197,10 +198,12 @@ async fn register(
let password_hash = hash_password(&payload.password)
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "INTERNAL_ERROR"))?;
let full_name = format!("{} {}", payload.first_name.trim(), payload.last_name.trim()).trim().to_string();
let user = UserRepository::create(&state.pool, CreateUserPayload {
full_name: payload.full_name,
email: email.clone(),
phone: payload.phone.filter(|p| !p.trim().is_empty()),
name: full_name,
email: email.clone(),
phone: payload.phone.filter(|p| !p.trim().is_empty()),
password_hash,
})
.await
@ -252,13 +255,13 @@ async fn register(
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
cache::otp::record_resend(&mut redis, &user.id.to_string()).await.ok();
let _ = state.mail.send_verification_email(&user.email, &user.full_name.clone().unwrap_or_default(), &otp).await;
let _ = state.mail.send_verification_email(&user.email, &user.name.clone().unwrap_or_default(), &otp).await;
Ok((StatusCode::CREATED, Json(RegisterResponse {
user_id: user.id.to_string(),
email: user.email,
phone: user.phone,
full_name: user.full_name.unwrap_or_default(),
name: user.name.unwrap_or_default(),
status: user.status,
email_verified: user.email_verified,
created_at: user.created_at.to_rfc3339(),
@ -327,7 +330,7 @@ async fn login(
"user": {
"id": user.id.to_string(),
"email": user.email,
"full_name": user.full_name.unwrap_or_default(),
"full_name": user.name.unwrap_or_default(),
"email_verified": user.email_verified,
"active_role": active_role,
"roles": user_roles,
@ -439,7 +442,7 @@ async fn session(
Ok(Json(SessionUser {
id: user.id.to_string(),
email: user.email,
full_name: user.full_name.unwrap_or_default(),
name: user.name.unwrap_or_default(),
email_verified: user.email_verified,
active_role: user_roles.first().cloned(),
roles: user_roles,
@ -469,7 +472,7 @@ async fn verify_email(
// Get user details for welcome email
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let _ = state.mail.send_welcome_email(&user.email, &user.full_name.unwrap_or_default()).await;
let _ = state.mail.send_welcome_email(&user.email, &user.name.unwrap_or_default()).await;
}
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Email verified successfully" }))))
@ -505,7 +508,7 @@ async fn resend_otp(
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
cache::otp::record_resend(&mut redis, &user.id.to_string()).await.ok();
let _ = state.mail.send_verification_email(&user.email, &user.full_name.unwrap_or_default(), &otp).await;
let _ = state.mail.send_verification_email(&user.email, &user.name.unwrap_or_default(), &otp).await;
Ok(silent_ok)
}
@ -530,7 +533,7 @@ async fn forgot_password(
.await
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
let _ = state.mail.send_password_reset_email(&user.email, &user.full_name.unwrap_or_default(), &token).await;
let _ = state.mail.send_password_reset_email(&user.email, &user.name.unwrap_or_default(), &token).await;
Ok(silent_ok)
}
@ -564,7 +567,7 @@ async fn reset_password(
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let _ = state.mail.send_password_changed_email(&user.email, user.full_name.as_deref().unwrap_or_default()).await;
let _ = state.mail.send_password_changed_email(&user.email, user.name.as_deref().unwrap_or_default()).await;
}
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Password reset successfully" }))))
@ -597,7 +600,7 @@ async fn change_password(
.await
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
let _ = state.mail.send_password_changed_email(&user.email, user.full_name.as_deref().unwrap_or_default()).await;
let _ = state.mail.send_password_changed_email(&user.email, user.name.as_deref().unwrap_or_default()).await;
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Password changed successfully" }))))
}

View file

@ -284,7 +284,7 @@ async fn get_my_runtime_config(
"user".to_string(),
serde_json::json!({
"id": user.id.to_string(),
"full_name": user.full_name.unwrap_or_default(),
"name": user.name.unwrap_or_default(),
"email": user.email,
"roles": roles,
"active_role": role_key,

View file

@ -125,7 +125,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
let recent_leads = sqlx::query_as::<_, LeadRow>(
r#"
SELECT r.id, r.title, r.status, r.created_at,
u.full_name AS requester_name
u.name AS requester_name
FROM leads r
LEFT JOIN users u ON u.id = r.created_by_user_id
WHERE r.status IN ('PENDING_APPROVAL', 'APPROVED')

View file

@ -225,7 +225,7 @@ async fn create_delete_account_request(
.mail
.send_account_deleted_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
)
.await;
let _ = sqlx::query(

View file

@ -137,7 +137,7 @@ async fn user_create_ticket(
};
let _ = state.mail.send_support_ticket_created_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&r.id.to_string(),
&body.subject,
&category,
@ -444,7 +444,7 @@ async fn admin_list_cases(
t.id, t.subject, t.description, t.category, t.priority, t.status,
t.requester_name, t.requester_email, t.assigned_to,
t.created_at, t.updated_at,
u.full_name AS user_name, u.email AS user_email
u.name AS user_name, u.email AS user_email
FROM support_tickets t
LEFT JOIN users u ON u.id = t.user_id
WHERE ($1 = '' OR t.status = $1)
@ -586,7 +586,7 @@ async fn admin_get_case(
t.id, t.subject, t.description, t.category, t.priority, t.status,
t.requester_name, t.requester_email, t.assigned_to,
t.created_at, t.updated_at,
u.full_name AS user_name, u.email AS user_email
u.name AS user_name, u.email AS user_email
FROM support_tickets t
LEFT JOIN users u ON u.id = t.user_id
WHERE t.id = $1
@ -832,7 +832,7 @@ async fn admin_add_message(
if let Some(user_email) = ticket.requester_email {
// Try to get user name from user table
let user_name = if let Ok(user) = db::models::user::UserRepository::get_by_email(&state.pool, &user_email).await {
user.full_name.unwrap_or_default()
user.name.unwrap_or_default()
} else {
ticket.requester_name.unwrap_or_default()
};

View file

@ -146,7 +146,7 @@ async fn trigger_rejection(
let display = role_key_to_display(&role_key);
let _ = state.mail.send_approval_rejected_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&display,
reason_str
).await;
@ -182,7 +182,7 @@ async fn approve_verification(
let display = role_key_to_display(&v.role_key);
let _ = state.mail.send_approval_approved_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&display
).await;
}
@ -296,7 +296,7 @@ async fn request_documents(
let display = role_key_to_display(&v.role_key);
let _ = state.mail.send_documents_requested_email(
&user.email,
user.full_name.as_deref().unwrap_or_default(),
user.name.as_deref().unwrap_or_default(),
&display,
&payload.message
).await;

View file

@ -10,7 +10,7 @@ pub struct User {
pub id: Uuid,
pub email: String,
pub password_hash: String,
pub full_name: Option<String>,
pub name: Option<String>,
pub phone: Option<String>,
pub email_verified: bool,
pub phone_verified: bool,
@ -27,7 +27,7 @@ pub struct User {
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateUserPayload {
pub full_name: String,
pub name: String,
pub email: String,
pub phone: Option<String>,
pub password_hash: String,
@ -51,17 +51,17 @@ impl UserRepository {
pub async fn create(pool: &PgPool, payload: CreateUserPayload) -> Result<User, sqlx::Error> {
let user = sqlx::query_as::<_, User>(
r#"
INSERT INTO users (full_name, email, phone, password_hash, email_verified, phone_verified)
INSERT INTO users (name, email, phone, password_hash, email_verified, phone_verified)
VALUES ($1, $2, $3, $4, false, false)
RETURNING
id, email, password_hash, full_name, phone,
id, email, password_hash, name, phone,
email_verified, phone_verified, status,
email_verification_token, email_verification_expires_at,
reset_password_token, reset_password_expires_at,
created_at, updated_at, deleted_at
"#,
)
.bind(payload.full_name)
.bind(&payload.name)
.bind(payload.email.to_lowercase())
.bind(payload.phone)
.bind(payload.password_hash)
@ -74,7 +74,7 @@ impl UserRepository {
pub async fn get_by_email(pool: &PgPool, email: &str) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"
SELECT id, email, password_hash, full_name, phone,
SELECT id, email, password_hash, name, phone,
email_verified, phone_verified, status,
email_verification_token, email_verification_expires_at,
reset_password_token, reset_password_expires_at,
@ -91,7 +91,7 @@ impl UserRepository {
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"
SELECT id, email, password_hash, full_name, phone,
SELECT id, email, password_hash, name, phone,
email_verified, phone_verified, status,
email_verification_token, email_verification_expires_at,
reset_password_token, reset_password_expires_at,
@ -148,7 +148,7 @@ impl UserRepository {
pub async fn get_by_verification_token(pool: &PgPool, token: &str) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"
SELECT id, email, password_hash, full_name, phone,
SELECT id, email, password_hash, name, phone,
email_verified, phone_verified, status,
email_verification_token, email_verification_expires_at,
reset_password_token, reset_password_expires_at,
@ -196,7 +196,7 @@ impl UserRepository {
pub async fn get_by_reset_token(pool: &PgPool, token: &str) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
r#"
SELECT id, email, password_hash, full_name, phone,
SELECT id, email, password_hash, name, phone,
email_verified, phone_verified, status,
email_verification_token, email_verification_expires_at,
reset_password_token, reset_password_expires_at,