feat: init
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::common::Role;
|
||||
|
||||
pub fn merge_optional_text(next: Option<String>, current: Option<String>) -> Option<String> {
|
||||
next.map(|v| {
|
||||
let value = v.trim().to_string();
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
})
|
||||
.unwrap_or(current)
|
||||
}
|
||||
|
||||
pub fn ensure_affected(rows_affected: u64, not_found: &str) -> Result<(), AppError> {
|
||||
if rows_affected == 0 {
|
||||
Err(AppError::NotFound(not_found.into()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_enum<T>(
|
||||
next: Option<String>,
|
||||
current: T,
|
||||
unknown: T,
|
||||
name: &str,
|
||||
) -> Result<T, AppError>
|
||||
where
|
||||
T: std::str::FromStr + PartialEq,
|
||||
{
|
||||
let Some(value) = next else {
|
||||
return Ok(current);
|
||||
};
|
||||
let parsed = value
|
||||
.trim()
|
||||
.parse::<T>()
|
||||
.map_err(|_| AppError::BadRequest(format!("invalid {name}")))?;
|
||||
if parsed == unknown {
|
||||
return Err(AppError::BadRequest(format!("invalid {name}")));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn required_text(value: String, field: &str) -> Result<String, AppError> {
|
||||
let value = value.trim().to_string();
|
||||
if value.is_empty() {
|
||||
return Err(AppError::BadRequest(format!("{field} is required")));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn clamp_limit_offset(limit: i64, offset: i64) -> (i64, i64) {
|
||||
(limit.clamp(1, 100), offset.max(0))
|
||||
}
|
||||
|
||||
pub fn role_level(role: Role) -> i32 {
|
||||
match role {
|
||||
Role::Owner => 100,
|
||||
Role::Admin => 90,
|
||||
Role::Maintainer => 70,
|
||||
Role::Member => 50,
|
||||
Role::Contributor => 30,
|
||||
Role::Viewer => 10,
|
||||
Role::Guest => 5,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constant_time_eq(a: &str, b: &str) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
a.bytes()
|
||||
.zip(b.bytes())
|
||||
.fold(0, |acc, (x, y)| acc | (x ^ y))
|
||||
== 0
|
||||
}
|
||||
|
||||
pub fn sha256_hex(data: &[u8]) -> String {
|
||||
use sha2::Digest;
|
||||
sha2::Sha256::digest(data)
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn extract_storage_key_from_url(url: &str) -> Option<String> {
|
||||
let path = url.split_once("://").map(|(_, rest)| rest)?;
|
||||
let path = path.split_once('/').map(|(_, rest)| rest).unwrap_or(path);
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(path.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn avatar_extension(
|
||||
content_type: Option<&str>,
|
||||
file_name: Option<&str>,
|
||||
) -> Result<&'static str, AppError> {
|
||||
if let Some(ct) = content_type.map(str::trim).filter(|v| !v.is_empty()) {
|
||||
return match ct.to_ascii_lowercase().as_str() {
|
||||
"image/png" => Ok("png"),
|
||||
"image/jpeg" | "image/jpg" => Ok("jpg"),
|
||||
"image/webp" => Ok("webp"),
|
||||
"image/gif" => Ok("gif"),
|
||||
_ => Err(AppError::BadRequest(
|
||||
"unsupported avatar content type".into(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
let Some(file_name) = file_name else {
|
||||
return Err(AppError::BadRequest(
|
||||
"avatar content type is required".into(),
|
||||
));
|
||||
};
|
||||
match file_name
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"png" => Ok("png"),
|
||||
"jpg" | "jpeg" => Ok("jpg"),
|
||||
"webp" => Ok("webp"),
|
||||
"gif" => Ok("gif"),
|
||||
_ => Err(AppError::BadRequest("unsupported avatar file type".into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_avatar_size(size: usize, configured_max_size: u64) -> Result<(), AppError> {
|
||||
const MAX_AVATAR_SIZE: u64 = 5 * 1024 * 1024;
|
||||
const MIN_AVATAR_SIZE: u64 = 1024;
|
||||
if size == 0 {
|
||||
return Err(AppError::BadRequest("avatar is empty".into()));
|
||||
}
|
||||
let max_size = configured_max_size.clamp(MIN_AVATAR_SIZE, MAX_AVATAR_SIZE) as usize;
|
||||
if size > max_size {
|
||||
return Err(AppError::BadRequest("avatar is too large".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_password_strength(password: &str) -> Result<(), AppError> {
|
||||
if password.len() < 8 {
|
||||
return Err(AppError::PasswordTooWeak);
|
||||
}
|
||||
if !password.chars().any(|c| c.is_uppercase())
|
||||
|| !password.chars().any(|c| c.is_lowercase())
|
||||
|| !password.chars().any(|c| c.is_numeric())
|
||||
{
|
||||
return Err(AppError::PasswordTooWeak);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user