1use std::time::{Duration, SystemTime};
7
8use crate::{AuthError, NythosResult, SessionId, TenantId, UserId};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub struct RefreshToken(String);
16
17impl RefreshToken {
18 pub fn new(value: impl Into<String>) -> NythosResult<Self> {
19 let value = value.into();
20
21 if value.trim().is_empty() {
22 return Err(AuthError::ValidationError(
23 "refresh token cannot be empty".to_owned(),
24 ));
25 }
26
27 Ok(Self(value))
28 }
29
30 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33
34 pub fn into_inner(self) -> String {
35 self.0
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
41pub struct Session {
42 id: SessionId,
43 user_id: UserId,
44 tenant_id: TenantId,
45 issued_at: SystemTime,
46 expires_at: SystemTime,
47 revoked: bool,
48}
49
50impl Session {
51 pub fn new(
52 id: SessionId,
53 user_id: UserId,
54 tenant_id: TenantId,
55 issued_at: SystemTime,
56 expires_at: SystemTime,
57 ) -> NythosResult<Self> {
58 if expires_at <= issued_at {
59 return Err(AuthError::ValidationError(
60 "session expiration must be after issue time".to_owned(),
61 ));
62 }
63
64 Ok(Self {
65 id,
66 user_id,
67 tenant_id,
68 issued_at,
69 expires_at,
70 revoked: false,
71 })
72 }
73
74 pub fn with_ttl(
75 id: SessionId,
76 user_id: UserId,
77 tenant_id: TenantId,
78 issued_at: SystemTime,
79 ttl: Duration,
80 ) -> NythosResult<Self> {
81 let expires_at = issued_at.checked_add(ttl).ok_or_else(|| {
82 AuthError::ValidationError("session expiry overflowed system time".to_owned())
83 })?;
84
85 Self::new(id, user_id, tenant_id, issued_at, expires_at)
86 }
87
88 pub const fn id(&self) -> SessionId {
89 self.id
90 }
91
92 pub const fn user_id(&self) -> UserId {
93 self.user_id
94 }
95
96 pub const fn tenant_id(&self) -> TenantId {
97 self.tenant_id
98 }
99
100 pub const fn issued_at(&self) -> SystemTime {
101 self.issued_at
102 }
103
104 pub const fn expires_at(&self) -> SystemTime {
105 self.expires_at
106 }
107
108 pub const fn is_revoked(&self) -> bool {
109 self.revoked
110 }
111
112 pub fn revoke(&mut self) {
113 self.revoked = true;
114 }
115
116 pub fn is_expired_at(&self, now: SystemTime) -> bool {
117 self.expires_at <= now
118 }
119
120 pub fn is_active_at(&self, now: SystemTime) -> bool {
121 !self.revoked && !self.is_expired_at(now)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::{RefreshToken, Session};
128 use crate::{AuthError, SessionId, TenantId, UserId};
129 use std::time::{Duration, SystemTime};
130
131 #[test]
132 fn refresh_token_requires_non_empty_value() {
133 assert!(matches!(
134 RefreshToken::new(""),
135 Err(AuthError::ValidationError(_))
136 ));
137
138 let token = RefreshToken::new("opaque-refresh-token").unwrap();
139 assert_eq!(token.as_str(), "opaque-refresh-token");
140 }
141
142 #[test]
143 fn session_requires_expiry_after_issue_time() {
144 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
145
146 let result = Session::new(
147 SessionId::generate(),
148 UserId::generate(),
149 TenantId::generate(),
150 now,
151 now,
152 );
153
154 assert!(matches!(result, Err(AuthError::ValidationError(_))));
155 }
156
157 #[test]
158 fn session_with_ttl_builds_active_session() {
159 let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
160 let ttl = Duration::from_secs(3_600);
161
162 let session = Session::with_ttl(
163 SessionId::generate(),
164 UserId::generate(),
165 TenantId::generate(),
166 issued_at,
167 ttl,
168 )
169 .unwrap();
170
171 assert_eq!(session.issued_at(), issued_at);
172 assert_eq!(session.expires_at(), issued_at + ttl);
173 assert!(!session.is_revoked());
174 assert!(session.is_active_at(issued_at + Duration::from_secs(60)));
175 }
176
177 #[test]
178 fn session_expiry_helper_matches_expected_semantics() {
179 let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
180 let session = Session::with_ttl(
181 SessionId::generate(),
182 UserId::generate(),
183 TenantId::generate(),
184 issued_at,
185 Duration::from_secs(60),
186 )
187 .unwrap();
188
189 assert!(!session.is_expired_at(issued_at + Duration::from_secs(59)));
190 assert!(session.is_expired_at(issued_at + Duration::from_secs(60)));
191 }
192
193 #[test]
194 fn revoked_session_is_no_longer_active() {
195 let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
196 let mut session = Session::with_ttl(
197 SessionId::generate(),
198 UserId::generate(),
199 TenantId::generate(),
200 issued_at,
201 Duration::from_secs(600),
202 )
203 .unwrap();
204
205 assert!(session.is_active_at(issued_at + Duration::from_secs(1)));
206
207 session.revoke();
208
209 assert!(session.is_revoked());
210 assert!(!session.is_active_at(issued_at + Duration::from_secs(1)));
211 }
212}