scufflecloud_core/models/
users.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3
4use diesel::prelude::{AsChangeset, Associations, Identifiable, Insertable, Queryable};
5use diesel::query_dsl::methods::{FilterDsl, SelectDsl};
6use diesel::{ExpressionMethods, Selectable};
7use diesel_async::RunQueryDsl;
8
9use crate::CoreConfig;
10use crate::cedar::CedarEntity;
11use crate::chrono_ext::ChronoDateTimeExt;
12use crate::id::{Id, PrefixedId};
13use crate::models::OrganizationId;
14use crate::schema::organization_members;
15use crate::std_ext::ResultExt;
16
17pub(crate) type UserId = Id<User>;
18
19#[derive(Debug, Queryable, Selectable, Insertable, Identifiable, AsChangeset, serde::Serialize, Clone)]
20#[diesel(table_name = crate::schema::users)]
21#[diesel(check_for_backend(diesel::pg::Pg))]
22pub struct User {
23    pub id: UserId,
24    pub preferred_name: Option<String>,
25    pub first_name: Option<String>,
26    pub last_name: Option<String>,
27    pub password_hash: Option<String>,
28    pub primary_email: Option<String>,
29}
30
31impl PrefixedId for User {
32    const PREFIX: &'static str = "u";
33}
34
35impl<G: CoreConfig> CedarEntity<G> for User {
36    const ENTITY_TYPE: &'static str = "User";
37
38    fn entity_id(&self) -> cedar_policy::EntityId {
39        cedar_policy::EntityId::new(self.id.to_string_unprefixed())
40    }
41
42    async fn parents(&self, global: &Arc<G>) -> Result<HashSet<cedar_policy::EntityUid>, tonic::Status> {
43        let mut db = global
44            .db()
45            .await
46            .into_tonic_internal_err("failed to get database connection")?;
47
48        let organization_ids = organization_members::dsl::organization_members
49            .filter(organization_members::dsl::user_id.eq(self.id))
50            .select(organization_members::dsl::organization_id)
51            .load::<OrganizationId>(&mut db)
52            .await
53            .into_tonic_internal_err("failed to load organization members")?;
54
55        Ok(organization_ids
56            .into_iter()
57            .map(|id| CedarEntity::<G>::entity_uid(&id))
58            .collect::<HashSet<_>>())
59    }
60}
61
62impl From<User> for pb::scufflecloud::core::v1::User {
63    fn from(value: User) -> Self {
64        pb::scufflecloud::core::v1::User {
65            id: value.id.to_string(),
66            preferred_name: value.preferred_name,
67            first_name: value.first_name,
68            last_name: value.last_name,
69            primary_email: value.primary_email,
70            created_at: Some(tinc::well_known::prost::Timestamp::from(value.id.datetime())),
71        }
72    }
73}
74
75#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, serde::Serialize)]
76#[diesel(table_name = crate::schema::user_emails)]
77#[diesel(primary_key(email))]
78#[diesel(belongs_to(User))]
79#[diesel(check_for_backend(diesel::pg::Pg))]
80pub struct UserEmail {
81    pub email: String,
82    pub user_id: UserId,
83    pub created_at: chrono::DateTime<chrono::Utc>,
84}
85
86impl<G> CedarEntity<G> for UserEmail {
87    const ENTITY_TYPE: &'static str = "UserEmail";
88
89    fn entity_id(&self) -> cedar_policy::EntityId {
90        cedar_policy::EntityId::new(&self.email)
91    }
92}
93
94impl From<UserEmail> for pb::scufflecloud::core::v1::UserEmail {
95    fn from(value: UserEmail) -> Self {
96        pb::scufflecloud::core::v1::UserEmail {
97            email: value.email,
98            created_at: Some(value.created_at.to_prost_timestamp_utc()),
99        }
100    }
101}
102
103#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, serde::Serialize)]
104#[diesel(primary_key(sub))]
105#[diesel(table_name = crate::schema::user_google_accounts)]
106#[diesel(belongs_to(User))]
107#[diesel(check_for_backend(diesel::pg::Pg))]
108pub struct UserGoogleAccount {
109    pub sub: String,
110    pub user_id: UserId,
111    pub access_token: String,
112    pub access_token_expires_at: chrono::DateTime<chrono::Utc>,
113    pub created_at: chrono::DateTime<chrono::Utc>,
114}
115
116impl<G> CedarEntity<G> for UserGoogleAccount {
117    const ENTITY_TYPE: &'static str = "UserGoogleAccount";
118
119    fn entity_id(&self) -> cedar_policy::EntityId {
120        cedar_policy::EntityId::new(&self.sub)
121    }
122}