feat: harden auth and stream uploads

This commit is contained in:
237899745
2026-07-25 18:42:51 +08:00
parent f389dfb567
commit f3c7a77a37
26 changed files with 1006 additions and 561 deletions

View File

@@ -2,11 +2,9 @@ use crate::api::context;
use crate::api::envelope::Envelope;
use crate::error::{AppError, ErrorCode};
use crate::services::billing;
use crate::services::mail;
use crate::services::settings;
use crate::services::{credentials, mail, settings};
use crate::state::AppState;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::extract::{ConnectInfo, Path, Query, State};
use axum::http::HeaderMap;
use axum::routing::{delete, get, post, put};
@@ -15,8 +13,8 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{DateTime, Duration, Utc};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::FromRow;
use std::collections::HashMap;
use std::net::SocketAddr;
use uuid::Uuid;
@@ -172,7 +170,7 @@ async fn update_profile(
if let Some(email) = req.email.as_ref() {
let email = email.trim().to_lowercase();
validate_email(&email)?;
credentials::validate_email(&email)?;
if email != user.email {
next_email = email;
email_changed = true;
@@ -181,7 +179,7 @@ async fn update_profile(
if let Some(username) = req.username.as_ref() {
let username = username.trim().to_string();
validate_username(&username)?;
credentials::validate_username(&username)?;
if username != user.username {
next_username = username;
}
@@ -238,8 +236,8 @@ async fn update_profile(
let mut verification_link: Option<String> = None;
if email_changed && verification_required {
let token = generate_token();
let token_hash = sha256_hex(&token);
let token = credentials::generate_token();
let token_hash = credentials::sha256_hex(&token);
let expires_at = Utc::now() + Duration::hours(24);
sqlx::query(
@@ -317,7 +315,7 @@ async fn update_password(
_ => return Err(AppError::new(ErrorCode::Unauthorized, "未登录")),
};
validate_password(&req.new_password)?;
credentials::validate_password(&req.new_password)?;
#[derive(Debug, FromRow)]
struct PasswordRow {
@@ -330,10 +328,14 @@ async fn update_password(
.await
.map_err(|err| AppError::new(ErrorCode::Internal, "查询用户失败").with_source(err))?;
verify_password(&req.current_password, &row.password_hash)?;
if !credentials::verify_password(&req.current_password, &row.password_hash).await? {
return Err(AppError::new(ErrorCode::Unauthorized, "密码错误"));
}
let new_hash = hash_password(&req.new_password)?;
sqlx::query("UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1")
let new_hash = credentials::hash_password(&req.new_password).await?;
sqlx::query(
"UPDATE users SET password_hash = $2, token_version = token_version + 1, updated_at = NOW() WHERE id = $1",
)
.bind(user_id)
.bind(new_hash)
.execute(&state.db)
@@ -498,6 +500,7 @@ async fn list_history(
#[derive(Debug, FromRow)]
struct FileRow {
task_id: Uuid,
id: Uuid,
original_name: String,
original_size: i64,
@@ -510,11 +513,14 @@ async fn list_history(
}
let now = Utc::now();
let mut result_tasks = Vec::with_capacity(tasks.len());
for task in tasks {
let files: Vec<FileRow> = sqlx::query_as::<_, FileRow>(
let task_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
let files = if task_ids.is_empty() {
Vec::new()
} else {
sqlx::query_as::<_, FileRow>(
r#"
SELECT
task_id,
id,
original_name,
original_size,
@@ -525,16 +531,25 @@ async fn list_history(
error_message,
COALESCE(storage_key, storage_path) IS NOT NULL AS has_storage
FROM task_files
WHERE task_id = $1
ORDER BY created_at ASC
WHERE task_id = ANY($1)
ORDER BY task_id, created_at ASC
"#,
)
.bind(task.id)
.bind(&task_ids)
.fetch_all(&state.db)
.await
.map_err(|err| AppError::new(ErrorCode::Internal, "查询任务文件失败").with_source(err))?;
.map_err(|err| AppError::new(ErrorCode::Internal, "查询任务文件失败").with_source(err))?
};
let mut files_by_task = HashMap::<Uuid, Vec<FileRow>>::new();
for file in files {
files_by_task.entry(file.task_id).or_default().push(file);
}
let file_views = files
let mut result_tasks = Vec::with_capacity(tasks.len());
for task in tasks {
let file_views = files_by_task
.remove(&task.id)
.unwrap_or_default()
.into_iter()
.map(|file| HistoryFileView {
file_id: file.id,
@@ -837,13 +852,7 @@ fn generate_api_key() -> (String, String) {
}
fn normalize_permissions(input: Option<Vec<String>>) -> Result<serde_json::Value, AppError> {
let allowed = [
"compress",
"batch_compress",
"read_stats",
"billing_read",
"webhook_manage",
];
let allowed = ["compress", "batch_compress"];
let mut perms = Vec::<String>::new();
if let Some(values) = input {
@@ -871,65 +880,6 @@ fn normalize_permissions(input: Option<Vec<String>>) -> Result<serde_json::Value
Ok(serde_json::json!(perms))
}
fn validate_email(email: &str) -> Result<(), AppError> {
if email.trim().is_empty() || !email.contains('@') {
return Err(AppError::new(ErrorCode::InvalidRequest, "邮箱格式不正确"));
}
if email.len() > 255 {
return Err(AppError::new(ErrorCode::InvalidRequest, "邮箱过长"));
}
Ok(())
}
fn validate_username(username: &str) -> Result<(), AppError> {
if username.trim().is_empty() {
return Err(AppError::new(ErrorCode::InvalidRequest, "用户名不能为空"));
}
if username.len() > 50 {
return Err(AppError::new(ErrorCode::InvalidRequest, "用户名过长"));
}
Ok(())
}
fn validate_password(password: &str) -> Result<(), AppError> {
if password.len() < 8 {
return Err(AppError::new(ErrorCode::InvalidRequest, "密码至少 8 位"));
}
if password.len() > 128 {
return Err(AppError::new(ErrorCode::InvalidRequest, "密码过长"));
}
Ok(())
}
fn hash_password(password: &str) -> Result<String, AppError> {
let salt = argon2::password_hash::SaltString::generate(&mut rand::rngs::OsRng);
let hashed = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map_err(|err| AppError::new(ErrorCode::Internal, "密码哈希失败").with_source(err))?;
Ok(hashed.to_string())
}
fn verify_password(password: &str, password_hash: &str) -> Result<(), AppError> {
let parsed = PasswordHash::new(password_hash)
.map_err(|err| AppError::new(ErrorCode::Internal, "密码哈希格式错误").with_source(err))?;
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.map_err(|_| AppError::new(ErrorCode::Unauthorized, "密码错误"))?;
Ok(())
}
fn generate_token() -> String {
let mut bytes = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
fn sha256_hex(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
hex::encode(hasher.finalize())
}
fn map_unique_violation(err: sqlx::Error) -> AppError {
if let sqlx::Error::Database(db_err) = &err {
if let Some(code) = db_err.code() {