fix(auth): prevent password_hash from reaching the frontend

Introduce PublicUser struct without password_hash field. The
get_current_user server function now returns PublicUser via
CurrentUserResponse, so Argon2 hashes are never serialized to WASM.

Internal server-side functions (get_current_admin_user) continue
using the full User struct.
This commit is contained in:
xfy 2026-06-03 10:32:15 +08:00
parent 8146a8a779
commit f5413e00cc
4 changed files with 28 additions and 8 deletions

View File

@ -6,7 +6,7 @@ use http::header::{HeaderValue, SET_COOKIE};
use crate::auth::{password, session};
use crate::db::pool::DB_POOL;
use crate::models::user::{User, UserRole};
use crate::models::user::{PublicUser, User, UserRole};
#[allow(dead_code)]
fn validate_username(username: &str) -> Result<(), String> {
@ -261,7 +261,7 @@ pub async fn logout() -> Result<AuthResponse, ServerFnError> {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CurrentUserResponse {
pub user: Option<User>,
pub user: Option<PublicUser>,
}
#[server(GetCurrentUser, "/api")]
@ -289,7 +289,7 @@ pub async fn get_current_user() -> Result<CurrentUserResponse, ServerFnError> {
let row = client
.query_opt(
"SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at
"SELECT u.id, u.username, u.email, u.role, u.created_at
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.token = $1 AND s.expires_at > NOW()",
@ -305,11 +305,10 @@ pub async fn get_current_user() -> Result<CurrentUserResponse, ServerFnError> {
Some(row) => {
let role_str: String = row.get("role");
let role = UserRole::from_str(&role_str).unwrap_or(UserRole::Blocked);
Some(User {
Some(PublicUser {
id: row.get("id"),
username: row.get("username"),
email: row.get("email"),
password_hash: row.get("password_hash"),
role,
created_at: row.get("created_at"),
})

View File

@ -1,10 +1,10 @@
use dioxus::prelude::*;
use std::sync::Arc;
use crate::models::user::User;
use crate::models::user::PublicUser;
#[derive(Clone, Copy)]
pub struct UserContext {
pub user: Signal<Option<Arc<User>>>,
pub user: Signal<Option<Arc<PublicUser>>>,
pub checked: Signal<bool>,
}

View File

@ -27,3 +27,24 @@ pub struct User {
pub role: UserRole,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicUser {
pub id: i32,
pub username: String,
pub email: String,
pub role: UserRole,
pub created_at: DateTime<Utc>,
}
impl From<User> for PublicUser {
fn from(u: User) -> Self {
PublicUser {
id: u.id,
username: u.username,
email: u.email,
role: u.role,
created_at: u.created_at,
}
}
}

View File

@ -59,7 +59,7 @@ pub fn AppRouter() -> Element {
Theme::Light => "",
};
let user = use_signal(|| None::<Arc<crate::models::user::User>>);
let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
let checked = use_signal(|| false);
use_context_provider(|| UserContext { user, checked });