klick_domain/authentication/
nonce.rs1use std::{fmt, str::FromStr, string::FromUtf8Error};
2
3use thiserror::Error;
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7use crate::authentication::{EmailAddress, EmailAddressParseError};
8
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Nonce(Uuid);
11
12impl Nonce {
13 pub const STR_LEN: usize = 32;
14
15 #[must_use]
16 pub fn new() -> Self {
17 Self(Uuid::new_v4())
18 }
19}
20
21#[derive(Debug, Error)]
22#[error("invalid nonce")]
23pub struct NonceParseError;
24
25impl FromStr for Nonce {
26 type Err = NonceParseError;
27
28 fn from_str(nonce_str: &str) -> Result<Self, Self::Err> {
29 nonce_str
30 .parse::<Uuid>()
31 .map(Nonce)
32 .map_err(|_| NonceParseError)
33 }
34}
35
36impl fmt::Display for Nonce {
37 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), std::fmt::Error> {
38 write!(f, "{}", self.0.as_simple())
39 }
40}
41
42#[derive(Debug, Clone, Eq, PartialEq)]
43pub struct EmailNonce {
44 pub email: EmailAddress,
45 pub nonce: Nonce,
46}
47
48pub type ActualTokenLen = usize;
49
50#[derive(Debug, Error)]
51pub enum EmailNonceDecodingError {
52 #[error(transparent)]
53 Bs58(#[from] bs58::decode::Error),
54 #[error(transparent)]
55 Utf8(#[from] FromUtf8Error),
56 #[error("nonce is too short: {0} instead of {}", Nonce::STR_LEN)]
57 TooShort(ActualTokenLen),
58 #[error(transparent)]
59 Parse(#[from] NonceParseError),
60 #[error(transparent)]
61 EmailAddress(#[from] EmailAddressParseError),
62}
63
64impl EmailNonce {
65 #[must_use]
66 pub fn encode_to_string(&self) -> String {
67 let nonce = self.nonce.to_string();
68 debug_assert_eq!(Nonce::STR_LEN, nonce.len());
69 let mut concat = String::with_capacity(self.email.as_str().len() + nonce.len());
70 concat += self.email.as_str();
71 concat += &nonce;
72 bs58::encode(concat).into_string()
73 }
74
75 pub fn decode_from_str(encoded: &str) -> Result<Self, EmailNonceDecodingError> {
76 let decoded = bs58::decode(encoded).into_vec()?;
77 let mut concat = String::from_utf8(decoded)?;
78 if concat.len() < Nonce::STR_LEN {
79 return Err(EmailNonceDecodingError::TooShort(concat.len()));
80 }
81 let email_len = concat.len() - Nonce::STR_LEN;
82 let nonce_slice: &str = &concat[email_len..];
83 let nonce = nonce_slice.parse::<Nonce>()?;
84 concat.truncate(email_len);
85 let email = concat.parse()?;
86 Ok(Self { email, nonce })
87 }
88}
89
90#[derive(Debug, Clone, Eq, PartialEq)]
91pub struct AccountToken {
92 pub email_nonce: EmailNonce,
93 pub expires_at: OffsetDateTime,
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn encode_decode_email_nonce() {
102 let example = EmailNonce {
103 email: "test@example.com".parse().unwrap(),
104 nonce: Nonce::new(),
105 };
106 let encoded = example.encode_to_string();
107 let decoded = EmailNonce::decode_from_str(&encoded).unwrap();
108 assert_eq!(example, decoded);
109 }
110
111 #[test]
112 fn decode_empty_email_nonce() {
113 assert!(EmailNonce::decode_from_str("").is_err());
114 }
115
116 #[test]
117 fn should_generate_unique_instances() {
118 let n1 = Nonce::new();
119 let n2 = Nonce::new();
120 assert_ne!(n1, n2);
121 }
122
123 #[test]
124 fn should_convert_from_to_string() {
125 let n1 = Nonce::new();
126 let s1 = n1.to_string();
127 assert_eq!(Nonce::STR_LEN, s1.len());
128 let n2 = s1.parse::<Nonce>().unwrap();
129 assert_eq!(n1, n2);
130 assert_eq!(s1, n2.to_string());
131 }
132}