clients.rs (6560B)
1 // Database operations for clients table 2 3 use sqlx::PgPool; 4 use anyhow::Result; 5 use uuid::Uuid; 6 use chrono::{DateTime, Utc}; 7 8 /// Client registration record 9 #[derive(Debug, Clone, sqlx::FromRow)] 10 pub struct Client { 11 pub id: Uuid, 12 pub client_id: String, 13 pub secret_hash: String, 14 pub webhook_url: String, 15 pub verifier_url: String, 16 pub verifier_management_api_path: String, 17 pub redirect_uri: Option<String>, 18 pub accepted_issuer_dids: Option<String>, 19 pub created_at: DateTime<Utc>, 20 pub updated_at: DateTime<Utc>, 21 } 22 23 /// Register a new client 24 pub async fn register_client( 25 pool: &PgPool, 26 client_id: &str, 27 client_secret: &str, 28 webhook_url: &str, 29 verifier_url: &str, 30 verifier_management_api_path: Option<&str>, 31 redirect_uri: Option<&str>, 32 accepted_issuer_dids: Option<&str>, 33 ) -> Result<Client> { 34 let api_path = verifier_management_api_path 35 .unwrap_or("/management/api/verifications"); 36 37 let secret_hash = bcrypt::hash(client_secret, bcrypt::DEFAULT_COST)?; 38 39 let client = sqlx::query_as::<_, Client>( 40 r#" 41 INSERT INTO oauth2gw.clients 42 (client_id, secret_hash, webhook_url, verifier_url, verifier_management_api_path, redirect_uri, accepted_issuer_dids) 43 VALUES ($1, $2, $3, $4, $5, $6, $7) 44 RETURNING id, client_id, secret_hash, webhook_url, verifier_url, 45 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 46 "# 47 ) 48 .bind(client_id) 49 .bind(secret_hash) 50 .bind(webhook_url) 51 .bind(verifier_url) 52 .bind(api_path) 53 .bind(redirect_uri) 54 .bind(accepted_issuer_dids) 55 .fetch_one(pool) 56 .await?; 57 58 Ok(client) 59 } 60 61 /// Lookup client by client_id 62 pub async fn get_client_by_id( 63 pool: &PgPool, 64 client_id: &str 65 ) -> Result<Option<Client>> { 66 let client = sqlx::query_as::<_, Client>( 67 r#" 68 SELECT id, client_id, secret_hash, webhook_url, verifier_url, 69 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 70 FROM oauth2gw.clients 71 WHERE client_id = $1 72 "# 73 ) 74 .bind(client_id) 75 .fetch_optional(pool) 76 .await?; 77 78 Ok(client) 79 } 80 81 /// Lookup client by UUID 82 pub async fn get_client_by_uuid( 83 pool: &PgPool, 84 id: Uuid 85 ) -> Result<Option<Client>> { 86 let client = sqlx::query_as::<_, Client>( 87 r#" 88 SELECT id, client_id, secret_hash, webhook_url, verifier_url, 89 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 90 FROM oauth2gw.clients 91 WHERE id = $1 92 "# 93 ) 94 .bind(id) 95 .fetch_optional(pool) 96 .await?; 97 98 Ok(client) 99 } 100 101 /// Get client secret hash by client_id 102 /// 103 /// Returns the secret hash if client exists, None otherwise 104 pub async fn get_client_secret_hash( 105 pool: &PgPool, 106 client_id: &str, 107 ) -> Result<Option<String>> { 108 let result = sqlx::query_scalar::<_, String>( 109 r#" 110 SELECT secret_hash 111 FROM oauth2gw.clients 112 WHERE client_id = $1 113 "# 114 ) 115 .bind(client_id) 116 .fetch_optional(pool) 117 .await?; 118 119 Ok(result) 120 } 121 122 /// Authenticate a client by validating client_secret 123 /// 124 /// Returns the client if authentication succeeds, None otherwise 125 pub async fn authenticate_client( 126 pool: &PgPool, 127 client_id: &str, 128 client_secret: &str 129 ) -> Result<Option<Client>> { 130 let client = sqlx::query_as::<_, Client>( 131 r#" 132 SELECT id, client_id, secret_hash, webhook_url, verifier_url, 133 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 134 FROM oauth2gw.clients 135 WHERE client_id = $1 136 "# 137 ) 138 .bind(client_id) 139 .fetch_optional(pool) 140 .await?; 141 142 match client { 143 Some(c) => { 144 if bcrypt::verify(client_secret, &c.secret_hash)? { 145 Ok(Some(c)) 146 } else { 147 Ok(None) 148 } 149 } 150 None => Ok(None) 151 } 152 } 153 154 /// Update client configuration 155 pub async fn update_client( 156 pool: &PgPool, 157 id: Uuid, 158 webhook_url: Option<&str>, 159 verifier_url: Option<&str>, 160 verifier_management_api_path: Option<&str>, 161 redirect_uri: Option<&str>, 162 accepted_issuer_dids: Option<&str>, 163 ) -> Result<Client> { 164 let current = get_client_by_uuid(pool, id).await? 165 .ok_or_else(|| anyhow::anyhow!("Client not found"))?; 166 167 let new_webhook_url = webhook_url.unwrap_or(¤t.webhook_url); 168 let new_verifier_url = verifier_url.unwrap_or(¤t.verifier_url); 169 let new_verifier_api_path = verifier_management_api_path 170 .unwrap_or(¤t.verifier_management_api_path); 171 let new_redirect_uri = redirect_uri.or(current.redirect_uri.as_deref()); 172 let new_accepted_issuer_dids = accepted_issuer_dids.or(current.accepted_issuer_dids.as_deref()); 173 174 let client = sqlx::query_as::<_, Client>( 175 r#" 176 UPDATE oauth2gw.clients 177 SET 178 webhook_url = $1, 179 verifier_url = $2, 180 verifier_management_api_path = $3, 181 redirect_uri = $4, 182 accepted_issuer_dids = $5, 183 updated_at = CURRENT_TIMESTAMP 184 WHERE id = $6 185 RETURNING id, client_id, secret_hash, webhook_url, verifier_url, 186 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 187 "# 188 ) 189 .bind(new_webhook_url) 190 .bind(new_verifier_url) 191 .bind(new_verifier_api_path) 192 .bind(new_redirect_uri) 193 .bind(new_accepted_issuer_dids) 194 .bind(id) 195 .fetch_one(pool) 196 .await?; 197 198 Ok(client) 199 } 200 201 /// Delete a client (and cascade delete all associated sessions) 202 /// 203 /// WARNING: Deletes all associated data as well! (e.g., verifications)! 204 pub async fn delete_client( 205 pool: &PgPool, 206 id: Uuid 207 ) -> Result<bool> { 208 let result = sqlx::query( 209 r#" 210 DELETE FROM oauth2gw.clients 211 WHERE id = $1 212 "# 213 ) 214 .bind(id) 215 .execute(pool) 216 .await?; 217 218 Ok(result.rows_affected() > 0) 219 } 220 221 /// List all registered clients 222 pub async fn list_clients(pool: &PgPool) -> Result<Vec<Client>> { 223 let clients = sqlx::query_as::<_, Client>( 224 r#" 225 SELECT id, client_id, secret_hash, webhook_url, verifier_url, 226 verifier_management_api_path, redirect_uri, accepted_issuer_dids, created_at, updated_at 227 FROM oauth2gw.clients 228 ORDER BY created_at DESC 229 "# 230 ) 231 .fetch_all(pool) 232 .await?; 233 234 Ok(clients) 235 }