scufflecloud_core/models/
users.rs1use 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}