use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub email: String,
pub password_hash: String,
pub created_at: u64,
pub updated_at: u64,
}
impl User {
pub fn new(email: &str, password: &str) -> Result<Self, String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| e.to_string())?
.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(Self {
id: Uuid::new_v4().to_string(),
email: email.to_string(),
password_hash: hash,
created_at: now,
updated_at: now,
})
}
pub fn verify_password(&self, password: &str) -> bool {
let parsed = match PasswordHash::new(&self.password_hash) {
Ok(h) => h,
Err(_) => return false,
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
}
pub fn change_password(&mut self, new_password: &str) -> Result<(), String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
self.password_hash = argon2
.hash_password(new_password.as_bytes(), &salt)
.map_err(|e| e.to_string())?
.to_string();
self.updated_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ticket {
pub id: String,
pub code: String,
pub created_by: String,
pub used_by: Option<String>,
pub used_at: Option<u64>,
pub created_at: u64,
}
impl Ticket {
pub fn new(created_by: &str) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
id: Uuid::new_v4().to_string(),
code: Uuid::new_v4()
.to_string()
.replace("-", "")
.to_uppercase(),
created_by: created_by.to_string(),
used_by: None,
used_at: None,
created_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub token: String,
pub user_id: String,
pub created_at: u64,
pub expires_at: u64,
}
impl Session {
pub fn new(user_id: &str) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
token: Uuid::new_v4().to_string(),
user_id: user_id.to_string(),
created_at: now,
expires_at: now + 7 * 24 * 60 * 60,
}
}
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now > self.expires_at
}
pub fn cookie_max_age_secs(&self) -> u64 {
if self.expires_at > self.created_at {
self.expires_at - self.created_at
} else {
0
}
}
}
pub const SESSION_COOKIE: &str = "corium_session";
pub const ADMIN_API_KEY_COOKIE: &str = "corium_admin_key";
#[derive(Debug, Clone)]
pub struct HurlTest {
pub method: String,
pub path: String,
pub params: std::collections::HashMap<String, String>,
pub body: Option<String>,
}
impl HurlTest {
pub fn parse(input: &str) -> Result<Self, String> {
let lines: Vec<&str> = input.lines().collect();
if lines.is_empty() {
return Err("Empty test input".to_string());
}
let first_line = lines[0].trim();
if first_line.is_empty() {
return Err("First line is empty".to_string());
}
let parts: Vec<&str> = first_line.splitn(2, ' ').collect();
if parts.is_empty() {
return Err("Invalid format. Expected: METHOD /path".to_string());
}
let method = if parts.len() == 2 {
parts[0].to_uppercase()
} else {
"GET".to_string()
};
let url = if parts.len() >= 2 {
parts[1].to_string()
} else {
parts[0].to_string()
};
let mut params = std::collections::HashMap::new();
let mut body = None;
let path = if let Some(query_start) = url.find('?') {
let path = url[..query_start].to_string();
let query = &url[query_start + 1..];
for pair in query.split('&') {
if let Some(eq_pos) = pair.find('=') {
let key = pair[..eq_pos].to_string();
let value = pair[eq_pos + 1..].to_string();
params.insert(key, value);
} else if !pair.is_empty() {
params.insert(pair.to_string(), String::new());
}
}
path
} else {
url
};
let mut in_body = false;
let mut body_lines = Vec::new();
for line in lines.iter().skip(1) {
let line = line.trim();
if line.is_empty() {
if !body_lines.is_empty() || in_body {
in_body = true;
}
continue;
}
if in_body {
body_lines.push(line.to_string());
} else if line.contains('=') {
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].to_string();
let value = line[eq_pos + 1..].to_string();
params.insert(key, value);
}
}
}
if !body_lines.is_empty() {
body = Some(body_lines.join("\n"));
}
Ok(Self {
method,
path,
params,
body,
})
}
}
pub fn parse_simple_params(input: &str) -> std::collections::HashMap<String, String> {
let mut params = std::collections::HashMap::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].to_string();
let value = line[eq_pos + 1..].to_string();
params.insert(key, value);
}
}
params
}