use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
lazy_static! {
static ref USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_@.+\\-]+$").expect("Invalid username regex");
static ref TOKEN_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_\\-]+$").expect("Invalid token regex");
}
#[derive(
Default,
Debug,
derive_more::Display,
derive_more::AsRef,
derive_more::FromStr,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
)]
pub struct Username(String);
impl<T: AsRef<str>> PartialEq<T> for Username {
fn eq(&self, other: &T) -> bool {
self.0 == other.as_ref()
}
}
impl Username {
pub fn new(username: impl Into<String>) -> Result<Self, String> {
let username = username.into();
if username.is_empty() {
return Err("Username cannot be empty".to_string());
}
if username.len() > 50 {
return Err("Username cannot be longer than 50 characters".to_string());
}
if !USERNAME_REGEX.is_match(&username) {
return Err("Username contains invalid characters".to_string());
}
Ok(Self(username))
}
pub fn new_unchecked(username: impl Into<String>) -> Self {
Self(username.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl From<String> for Username {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Username {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Default, derive_more::FromStr, Clone, Eq, Hash, Serialize, Deserialize)]
pub struct Password(String);
impl Password {
pub fn new(password: impl Into<String>) -> Result<Self, String> {
let password = password.into();
if password.is_empty() {
return Err("Password cannot be empty".to_string());
}
if password.len() < 8 {
return Err("Password must be at least 8 characters long".to_string());
}
if password.len() > 128 {
return Err("Password cannot be longer than 128 characters".to_string());
}
Ok(Self(password))
}
pub fn new_unchecked(password: impl Into<String>) -> Self {
Self(password.into())
}
pub fn expose_secret(&self) -> &str {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Password").field(&"[REDACTED]").finish()
}
}
impl std::fmt::Display for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[REDACTED]")
}
}
impl PartialEq for Password {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
#[derive(
Default, derive_more::From, derive_more::FromStr, Clone, Eq, Hash, Serialize, Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Token(String);
impl Token {
pub fn new(token: impl Into<String>) -> Result<Self, String> {
let token = token.into();
if token.is_empty() {
return Err("Token cannot be empty".to_string());
}
if token.len() < 10 {
return Err("Token must be at least 10 characters long".to_string());
}
if token.len() > 256 {
return Err("Token cannot be longer than 256 characters".to_string());
}
if !TOKEN_REGEX.is_match(&token) {
return Err("Token contains invalid characters".to_string());
}
Ok(Self(token))
}
pub fn new_unchecked(token: impl Into<String>) -> Self {
Self(token.into())
}
pub fn expose_secret(&self) -> &str {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl From<&str> for Token {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl std::fmt::Debug for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Token").field(&"[REDACTED]").finish()
}
}
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[REDACTED]")
}
}
impl PartialEq for Token {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKey {
pub key_id: String,
pub name: String,
pub description: Option<String>,
pub permissions: Vec<String>,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_usernames() {
let valid_usernames = vec![
"user123",
"test_user",
"user@domain.com",
"user+tag@domain.com",
"user.name@domain.com",
"user-name",
"user_name",
];
for username in valid_usernames {
assert!(
Username::new(username).is_ok(),
"Username '{}' should be valid",
username
);
}
}
#[test]
fn test_invalid_usernames() {
let too_long = "a".repeat(51);
let invalid_usernames = vec![
"", "user space", "user#", "user$", &too_long, ];
for username in invalid_usernames {
assert!(
Username::new(username).is_err(),
"Username '{}' should be invalid",
username
);
}
}
#[test]
fn test_username_length_limit() {
let exactly_50_chars = "a".repeat(50);
assert!(Username::new(&exactly_50_chars).is_ok());
let too_long = "a".repeat(51);
assert!(Username::new(&too_long).is_err());
}
#[test]
fn test_validate_username_function() {
assert!(Username::new("valid_user").is_ok());
assert!(Username::new("user@domain.com").is_ok());
assert!(Username::new("").is_err());
assert!(Username::new("user space").is_err());
assert!(Username::new(&"a".repeat(51)).is_err());
}
}