use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{Result, ZeptoError};
pub fn generate_api_token() -> String {
let a = Uuid::new_v4().simple().to_string();
let b = Uuid::new_v4().simple().to_string();
format!("{}{}", &a[..32], &b[..32])
}
pub fn verify_bearer_token(header: &str, expected: &str) -> Result<()> {
let token = header
.strip_prefix("Bearer ")
.ok_or_else(|| ZeptoError::Unauthorized("missing Bearer prefix".to_string()))?;
if token == expected {
Ok(())
} else {
Err(ZeptoError::Unauthorized("invalid API token".to_string()))
}
}
pub fn hash_password(password: &str) -> Result<String> {
bcrypt::hash(password, 12).map_err(|e| ZeptoError::Config(format!("bcrypt hash: {e}")))
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
bcrypt::verify(password, hash).map_err(|e| ZeptoError::Config(format!("bcrypt verify: {e}")))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
}
pub fn generate_jwt(username: &str, secret: &str, expires_in_secs: u64) -> Result<String> {
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
.saturating_add(expires_in_secs) as usize;
let claims = Claims {
sub: username.to_string(),
exp,
};
encode(
&Header::default(), &claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|e| ZeptoError::Unauthorized(format!("JWT encode: {e}")))
}
pub fn validate_jwt(token: &str, secret: &str) -> Result<Claims> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(), )
.map_err(|e| ZeptoError::Unauthorized(format!("JWT validation: {e}")))?;
Ok(token_data.claims)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_api_token_length() {
let token = generate_api_token();
assert_eq!(token.len(), 64, "token must be exactly 64 hex chars");
}
#[test]
fn test_generate_api_token_hex_chars() {
let token = generate_api_token();
assert!(
token.chars().all(|c| c.is_ascii_hexdigit()),
"token must contain only hex digits, got: {token}"
);
}
#[test]
fn test_generate_api_token_uniqueness() {
let t1 = generate_api_token();
let t2 = generate_api_token();
assert_ne!(t1, t2, "two generated tokens must differ");
}
#[test]
fn test_verify_bearer_token_valid() {
let secret = "mysecrettoken123";
let header = format!("Bearer {secret}");
assert!(
verify_bearer_token(&header, secret).is_ok(),
"valid bearer token should pass"
);
}
#[test]
fn test_verify_bearer_token_invalid() {
let result = verify_bearer_token("Bearer wrongtoken", "correcttoken");
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"mismatched token should return Unauthorized"
);
}
#[test]
fn test_verify_bearer_token_missing_prefix() {
let result = verify_bearer_token("mysecrettoken123", "mysecrettoken123");
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"missing Bearer prefix should return Unauthorized"
);
}
#[test]
fn test_verify_bearer_token_lowercase_prefix_rejected() {
let result = verify_bearer_token("bearer validtoken", "validtoken");
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"lowercase 'bearer' prefix must be rejected"
);
}
#[test]
fn test_hash_and_verify_password() {
let password = "hunter2";
let hash = hash_password(password).expect("hash must succeed");
assert!(!hash.is_empty(), "hash must not be empty");
let ok = verify_password(password, &hash).expect("verify must succeed");
assert!(ok, "correct password must verify as true");
}
#[test]
fn test_verify_wrong_password() {
let hash = hash_password("correct_password").expect("hash must succeed");
let ok = verify_password("wrong_password", &hash).expect("verify must succeed");
assert!(!ok, "wrong password must verify as false");
}
#[test]
fn test_generate_jwt_and_validate() {
let secret = "super_secret_key_for_testing";
let username = "admin";
let token = generate_jwt(username, secret, 3600).expect("JWT generation must succeed");
assert!(!token.is_empty(), "JWT must not be empty");
let claims = validate_jwt(&token, secret).expect("JWT validation must succeed");
assert_eq!(claims.sub, username, "sub claim must match username");
}
#[test]
fn test_validate_expired_jwt() {
let secret = "super_secret_key_for_testing";
let past_exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
.saturating_sub(3600) as usize;
let claims = Claims {
sub: "admin".to_string(),
exp: past_exp,
};
let expired_token = jsonwebtoken::encode(
&jsonwebtoken::Header::default(),
&claims,
&jsonwebtoken::EncodingKey::from_secret(secret.as_bytes()),
)
.expect("encoding expired token must succeed");
let result = validate_jwt(&expired_token, secret);
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"expired JWT must return Unauthorized, got: {result:?}"
);
}
#[test]
fn test_validate_jwt_wrong_secret() {
let token =
generate_jwt("alice", "correct_secret", 3600).expect("JWT generation must succeed");
let result = validate_jwt(&token, "wrong_secret");
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"wrong secret must return Unauthorized"
);
}
#[test]
fn test_validate_jwt_malformed() {
let result = validate_jwt("not.a.jwt", "secret");
assert!(
matches!(result, Err(ZeptoError::Unauthorized(_))),
"malformed JWT must return Unauthorized"
);
}
}