Built for people who think better out loud.
1use sqlx::Row;
2use sqlx::PgPool;
3use uuid::Uuid;
4
5use crate::domain::CurrentUser;
6
7pub async fn load_current_user_by_session(
8 db_pool: &PgPool,
9 session_id: Uuid,
10) -> Result<Option<CurrentUser>, sqlx::Error> {
11 let record = sqlx::query(
12 r#"
13 SELECT users.did, users.handle
14 FROM sessions
15 JOIN users ON users.did = sessions.user_did
16 WHERE sessions.id = $1
17 AND sessions.expires_at > NOW()
18 "#,
19 )
20 .bind(session_id)
21 .fetch_optional(db_pool)
22 .await?;
23
24 let Some(record) = record else {
25 return Ok(None);
26 };
27
28 let did: String = record.try_get("did")?;
29 let handle: Option<String> = record.try_get("handle")?;
30
31 Ok(Some(CurrentUser { did, handle }))
32}
33
34/// Inserts or updates the user's handle.
35pub async fn upsert_user(
36 db_pool: &PgPool,
37 did: &str,
38 handle: Option<&str>,
39) -> Result<(), sqlx::Error> {
40 sqlx::query(
41 r#"
42 INSERT INTO users (did, handle)
43 VALUES ($1, $2)
44 ON CONFLICT (did)
45 DO UPDATE SET handle = EXCLUDED.handle
46 "#,
47 )
48 .bind(did)
49 .bind(handle)
50 .execute(db_pool)
51 .await?;
52
53 Ok(())
54}
55
56/// Stores OAuth tokens for a user.
57pub async fn store_tokens(
58 db_pool: &PgPool,
59 user_did: &str,
60 access_token: &str,
61 refresh_token: Option<&str>,
62 token_type: &str,
63 scopes: &str,
64 expires_in: u32,
65 issuer: &str,
66 dpop_private_key: &str,
67) -> Result<(), sqlx::Error> {
68 let expires_at = chrono::Utc::now() + chrono::Duration::seconds(i64::from(expires_in));
69 sqlx::query(
70 r#"
71 INSERT INTO oauth_tokens (
72 user_did,
73 access_token,
74 refresh_token,
75 token_type,
76 scopes,
77 expires_at,
78 issuer,
79 dpop_private_key,
80 updated_at
81 )
82 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
83 ON CONFLICT (user_did)
84 DO UPDATE SET
85 access_token = EXCLUDED.access_token,
86 refresh_token = EXCLUDED.refresh_token,
87 token_type = EXCLUDED.token_type,
88 scopes = EXCLUDED.scopes,
89 expires_at = EXCLUDED.expires_at,
90 issuer = EXCLUDED.issuer,
91 dpop_private_key = EXCLUDED.dpop_private_key,
92 updated_at = NOW()
93 "#,
94 )
95 .bind(user_did)
96 .bind(access_token)
97 .bind(refresh_token)
98 .bind(token_type)
99 .bind(scopes)
100 .bind(expires_at)
101 .bind(issuer)
102 .bind(dpop_private_key)
103 .execute(db_pool)
104 .await?;
105
106 Ok(())
107}
108
109/// Creates a new session for a user.
110pub async fn create_session(
111 db_pool: &PgPool,
112 session_id: Uuid,
113 user_did: &str,
114 expires_at: chrono::DateTime<chrono::Utc>,
115) -> Result<(), sqlx::Error> {
116 sqlx::query(
117 r#"
118 INSERT INTO sessions (id, user_did, expires_at)
119 VALUES ($1, $2, $3)
120 "#,
121 )
122 .bind(session_id)
123 .bind(user_did)
124 .bind(expires_at)
125 .execute(db_pool)
126 .await?;
127 Ok(())
128}
129
130/// Deletes a session by id.
131pub async fn delete_session(db_pool: &PgPool, session_id: Uuid) -> Result<(), sqlx::Error> {
132 sqlx::query(
133 r#"
134 DELETE FROM sessions WHERE id = $1
135 "#,
136 )
137 .bind(session_id)
138 .execute(db_pool)
139 .await?;
140 Ok(())
141}