The smokesignal.events web application
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 113 lines 3.9 kB view raw
1//! Cookie utilities for session management. 2//! 3//! This module provides the `SessionCookie` struct and utilities for encoding/decoding 4//! session data directly in browser cookies rather than storing tokens in the database. 5 6use anyhow::Result; 7use chrono::{DateTime, Utc}; 8use serde::{Deserialize, Serialize}; 9 10use crate::http::errors::WebSessionError; 11 12/// Session data stored directly in the browser cookie. 13/// 14/// This struct contains all the data needed to authenticate requests without 15/// requiring a database lookup. The cookie is encrypted using the HTTP_COOKIE_KEY. 16#[derive(Clone, PartialEq, Serialize, Deserialize)] 17pub(crate) struct SessionCookie { 18 /// The user's DID (Decentralized Identifier) 19 pub did: String, 20 /// The OAuth access token for AT Protocol requests 21 pub access_token: String, 22 /// The OAuth refresh token for obtaining new access tokens 23 pub refresh_token: Option<String>, 24 /// When the access token expires 25 pub expires_at: DateTime<Utc>, 26 /// The DPoP private key in did:key format for proof-of-possession 27 pub dpop_private_key: String, 28 /// The issuer (authorization server) URL 29 pub issuer: String, 30} 31 32impl SessionCookie { 33 /// Check if the access token has expired 34 pub fn is_expired(&self) -> bool { 35 Utc::now() >= self.expires_at 36 } 37 38 /// Check if the access token will expire within the given duration 39 pub fn expires_within(&self, duration: chrono::Duration) -> bool { 40 Utc::now() + duration >= self.expires_at 41 } 42} 43 44impl TryFrom<String> for SessionCookie { 45 type Error = anyhow::Error; 46 47 fn try_from(value: String) -> Result<Self, Self::Error> { 48 serde_json::from_str(&value) 49 .map_err(WebSessionError::DeserializeFailed) 50 .map_err(Into::into) 51 } 52} 53 54impl TryInto<String> for SessionCookie { 55 type Error = anyhow::Error; 56 57 fn try_into(self) -> Result<String, Self::Error> { 58 serde_json::to_string(&self) 59 .map_err(WebSessionError::SerializeFailed) 60 .map_err(Into::into) 61 } 62} 63 64#[cfg(test)] 65mod tests { 66 use super::*; 67 use chrono::Duration; 68 69 #[test] 70 fn test_session_cookie_serialization() { 71 let session = SessionCookie { 72 did: "did:plc:test123".to_string(), 73 access_token: "test_access_token".to_string(), 74 refresh_token: Some("test_refresh_token".to_string()), 75 expires_at: Utc::now() + Duration::hours(1), 76 dpop_private_key: "did:key:test".to_string(), 77 issuer: "https://bsky.social".to_string(), 78 }; 79 80 let serialized: String = session.clone().try_into().unwrap(); 81 let deserialized: SessionCookie = serialized.try_into().unwrap(); 82 83 assert_eq!(session.did, deserialized.did); 84 assert_eq!(session.access_token, deserialized.access_token); 85 assert_eq!(session.refresh_token, deserialized.refresh_token); 86 assert_eq!(session.dpop_private_key, deserialized.dpop_private_key); 87 assert_eq!(session.issuer, deserialized.issuer); 88 } 89 90 #[test] 91 fn test_is_expired() { 92 let expired_session = SessionCookie { 93 did: "did:plc:test123".to_string(), 94 access_token: "test".to_string(), 95 refresh_token: None, 96 expires_at: Utc::now() - Duration::hours(1), 97 dpop_private_key: "did:key:test".to_string(), 98 issuer: "https://bsky.social".to_string(), 99 }; 100 101 let valid_session = SessionCookie { 102 did: "did:plc:test123".to_string(), 103 access_token: "test".to_string(), 104 refresh_token: None, 105 expires_at: Utc::now() + Duration::hours(1), 106 dpop_private_key: "did:key:test".to_string(), 107 issuer: "https://bsky.social".to_string(), 108 }; 109 110 assert!(expired_session.is_expired()); 111 assert!(!valid_session.is_expired()); 112 } 113}