scufflecloud_core/models/
sessions.rs1use base64::Engine;
2use diesel::Selectable;
3use diesel::prelude::{AsChangeset, Associations, Identifiable, Insertable, Queryable};
4
5use super::impl_enum;
6use crate::cedar::CedarEntity;
7use crate::chrono_ext::ChronoDateTimeExt;
8use crate::id::{Id, PrefixedId};
9use crate::models::users::{User, UserId};
10
11impl_enum!(DeviceAlgorithm, crate::schema::sql_types::DeviceAlgorithm, {
12 RsaOaepSha256 => b"RSA_OAEP_SHA256",
13});
14
15impl From<DeviceAlgorithm> for pb::scufflecloud::core::v1::DeviceAlgorithm {
16 fn from(value: DeviceAlgorithm) -> Self {
17 match value {
18 DeviceAlgorithm::RsaOaepSha256 => pb::scufflecloud::core::v1::DeviceAlgorithm::RsaOaepSha256,
19 }
20 }
21}
22
23impl From<pb::scufflecloud::core::v1::DeviceAlgorithm> for DeviceAlgorithm {
24 fn from(value: pb::scufflecloud::core::v1::DeviceAlgorithm) -> Self {
25 match value {
26 pb::scufflecloud::core::v1::DeviceAlgorithm::RsaOaepSha256 => DeviceAlgorithm::RsaOaepSha256,
27 }
28 }
29}
30
31pub(crate) type UserSessionRequestId = Id<UserSessionRequest>;
32
33#[derive(Debug, Queryable, Selectable, Insertable, Identifiable, AsChangeset, serde::Serialize)]
34#[diesel(table_name = crate::schema::user_session_requests)]
35#[diesel(check_for_backend(diesel::pg::Pg))]
36pub struct UserSessionRequest {
37 pub id: UserSessionRequestId,
38 pub device_name: String,
39 pub device_ip: ipnetwork::IpNetwork,
40 pub code: String,
41 pub approved_by: Option<UserId>,
42 pub expires_at: chrono::DateTime<chrono::Utc>,
43}
44
45impl PrefixedId for UserSessionRequest {
46 const PREFIX: &'static str = "sr";
47}
48
49impl<G> CedarEntity<G> for UserSessionRequest {
50 const ENTITY_TYPE: &'static str = "UserSessionRequest";
51
52 fn entity_id(&self) -> cedar_policy::EntityId {
53 cedar_policy::EntityId::new(self.id.to_string_unprefixed())
54 }
55}
56
57impl From<UserSessionRequest> for pb::scufflecloud::core::v1::UserSessionRequest {
58 fn from(value: UserSessionRequest) -> Self {
59 pb::scufflecloud::core::v1::UserSessionRequest {
60 id: value.id.to_string(),
61 name: value.device_name,
62 ip: value.device_ip.to_string(),
63 approved_by: value.approved_by.map(|id| id.to_string()),
64 expires_at: Some(value.expires_at.to_prost_timestamp_utc()),
65 }
66 }
67}
68
69pub(crate) type MagicLinkUserSessionRequestId = Id<MagicLinkUserSessionRequest>;
70
71#[derive(Debug, Queryable, Selectable, Insertable, Identifiable, AsChangeset, serde::Serialize)]
72#[diesel(table_name = crate::schema::magic_link_user_session_requests)]
73#[diesel(check_for_backend(diesel::pg::Pg))]
74pub struct MagicLinkUserSessionRequest {
75 pub id: MagicLinkUserSessionRequestId,
76 pub user_id: UserId,
77 pub code: Vec<u8>,
78 pub expires_at: chrono::DateTime<chrono::Utc>,
79}
80
81impl PrefixedId for MagicLinkUserSessionRequest {
82 const PREFIX: &'static str = "ml";
83}
84
85impl<G> CedarEntity<G> for MagicLinkUserSessionRequest {
86 const ENTITY_TYPE: &'static str = "MagicLinkUserSessionRequest";
87
88 fn entity_id(&self) -> cedar_policy::EntityId {
89 cedar_policy::EntityId::new(self.id.to_string_unprefixed())
90 }
91}
92
93pub(crate) type UserSessionTokenId = Id<UserSessionToken>;
94
95#[derive(Debug, serde::Serialize)]
97pub struct UserSessionToken {
98 pub id: UserSessionTokenId,
99 pub token: Vec<u8>,
100 pub expires_at: chrono::DateTime<chrono::Utc>,
101}
102
103impl PrefixedId for UserSessionToken {
104 const PREFIX: &'static str = "st";
105}
106
107impl<G> CedarEntity<G> for UserSessionToken {
108 const ENTITY_TYPE: &'static str = "UserSessionToken";
109
110 fn entity_id(&self) -> cedar_policy::EntityId {
111 cedar_policy::EntityId::new(self.id.to_string_unprefixed())
112 }
113}
114
115#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, Clone, serde::Serialize)]
116#[diesel(table_name = crate::schema::user_sessions)]
117#[diesel(primary_key(user_id, device_fingerprint))]
118#[diesel(belongs_to(User))]
119#[diesel(check_for_backend(diesel::pg::Pg))]
120pub struct UserSession {
121 pub user_id: UserId,
122 pub device_fingerprint: Vec<u8>,
123 pub device_algorithm: DeviceAlgorithm,
124 pub device_pk_data: Vec<u8>,
125 pub last_used_at: chrono::DateTime<chrono::Utc>,
126 pub last_ip: ipnetwork::IpNetwork,
127 pub token_id: Option<UserSessionTokenId>,
128 pub token: Option<Vec<u8>>,
129 pub token_expires_at: Option<chrono::DateTime<chrono::Utc>>,
130 pub expires_at: chrono::DateTime<chrono::Utc>,
131 pub mfa_pending: bool,
132}
133
134impl<G> CedarEntity<G> for UserSession {
135 const ENTITY_TYPE: &'static str = "UserSession";
136
137 fn entity_id(&self) -> cedar_policy::EntityId {
138 let user_id = (*self.user_id).to_string();
139 let fingerprint = base64::prelude::BASE64_STANDARD.encode(&self.device_fingerprint);
140 cedar_policy::EntityId::new(format!("{user_id}:{fingerprint}"))
141 }
142}
143
144impl From<UserSession> for pb::scufflecloud::core::v1::UserSession {
145 fn from(value: UserSession) -> Self {
146 pb::scufflecloud::core::v1::UserSession {
147 user_id: value.user_id.to_string(),
148 device_fingerprint: value.device_fingerprint,
149 last_used_at: Some(value.last_used_at.to_prost_timestamp_utc()),
150 last_ip: value.last_ip.to_string(),
151 token_id: value.token_id.map(|id| id.to_string()),
152 token_expires_at: value.token_expires_at.map(|t| t.to_prost_timestamp_utc()),
153 expires_at: Some(value.expires_at.to_prost_timestamp_utc()),
154 mfa_pending: value.mfa_pending,
155 }
156 }
157}
158
159pub(crate) type EmailRegistrationRequestId = Id<EmailRegistrationRequest>;
160
161#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, serde::Serialize)]
162#[diesel(table_name = crate::schema::email_registration_requests)]
163#[diesel(belongs_to(User))]
164#[diesel(check_for_backend(diesel::pg::Pg))]
165pub struct EmailRegistrationRequest {
166 pub id: EmailRegistrationRequestId,
167 pub user_id: Option<UserId>,
168 pub email: String,
169 pub code: Vec<u8>,
170 pub expires_at: chrono::DateTime<chrono::Utc>,
171}
172
173impl PrefixedId for EmailRegistrationRequest {
174 const PREFIX: &'static str = "er";
175}
176
177impl<G> CedarEntity<G> for EmailRegistrationRequest {
178 const ENTITY_TYPE: &'static str = "EmailRegistrationRequest";
179
180 fn entity_id(&self) -> cedar_policy::EntityId {
181 cedar_policy::EntityId::new(self.id.to_string_unprefixed())
182 }
183}