Highly ambitious ATProtocol AppView service and sdks

add oauthClients to slices graphql node, fix slice uri handling in oauth clients (was using a placeholder)

Changed files
+211 -102
api
src
graphql
frontend-v2
+15 -11
api/src/graphql/schema_builder.rs
··· 21 21 add_cancel_job_mutation, add_create_oauth_client_mutation, add_delete_job_mutation, 22 22 add_delete_oauth_client_mutation, add_delete_slice_records_mutation, add_get_sync_summary_query, 23 23 add_jetstream_logs_query, add_jetstream_logs_subscription, add_oauth_clients_query, 24 - add_slice_records_query, add_sparklines_query, add_sparklines_field_to_slice, 25 - add_start_sync_mutation, add_stats_field_to_slice, add_sync_job_logs_query, add_sync_job_query, 26 - add_sync_job_subscription, add_sync_jobs_query, add_update_oauth_client_mutation, 27 - add_upload_blob_mutation, create_blob_upload_response_type, create_collection_stats_type, 28 - create_collection_summary_type, create_delete_slice_records_output_type, 29 - create_jetstream_log_entry_type, create_oauth_client_type, create_slice_record_type, 30 - create_slice_record_edge_type, create_slice_records_connection_type, 31 - create_slice_records_where_input, create_slice_sparkline_type, create_slice_stats_type, 32 - create_sparkline_point_type, create_start_sync_output_type, create_sync_job_result_type, 33 - create_sync_job_type, create_sync_summary_type, 24 + add_oauth_clients_field_to_slice, add_slice_records_query, add_sparklines_query, 25 + add_sparklines_field_to_slice, add_start_sync_mutation, add_stats_field_to_slice, 26 + add_sync_job_logs_query, add_sync_job_query, add_sync_job_subscription, add_sync_jobs_query, 27 + add_update_oauth_client_mutation, add_upload_blob_mutation, create_blob_upload_response_type, 28 + create_collection_stats_type, create_collection_summary_type, 29 + create_delete_slice_records_output_type, create_jetstream_log_entry_type, 30 + create_oauth_client_type, create_slice_record_type, create_slice_record_edge_type, 31 + create_slice_records_connection_type, create_slice_records_where_input, 32 + create_slice_sparkline_type, create_slice_stats_type, create_sparkline_point_type, 33 + create_start_sync_output_type, create_sync_job_result_type, create_sync_job_type, 34 + create_sync_summary_type, 34 35 }; 35 36 use crate::graphql::types::{extract_collection_fields, extract_record_key, GraphQLField, GraphQLType}; 36 37 use crate::graphql::PUBSUB; ··· 134 135 database.clone(), 135 136 slice_uri.clone(), 136 137 &all_collections, 138 + auth_base_url.clone(), 137 139 ); 138 140 139 141 // Create edge and connection types for this collection (Relay standard) ··· 1076 1078 database: Database, 1077 1079 slice_uri: String, 1078 1080 all_collections: &[CollectionMeta], 1081 + auth_base_url: String, 1079 1082 ) -> Object { 1080 1083 let mut object = Object::new(type_name); 1081 1084 ··· 1834 1837 )); 1835 1838 } 1836 1839 1837 - // Add sparklines and stats fields for NetworkSlicesSlice type 1840 + // Add sparklines, stats, and oauth clients fields for NetworkSlicesSlice type 1838 1841 if type_name == "NetworkSlicesSlice" { 1839 1842 object = add_sparklines_field_to_slice(object, database.clone()); 1840 1843 object = add_stats_field_to_slice(object, database.clone()); 1844 + object = add_oauth_clients_field_to_slice(object, auth_base_url); 1841 1845 } 1842 1846 1843 1847 object
+1
api/src/graphql/schema_ext/mod.rs
··· 66 66 pub use oauth::{ 67 67 create_oauth_client_type, 68 68 add_oauth_clients_query, 69 + add_oauth_clients_field_to_slice, 69 70 add_create_oauth_client_mutation, 70 71 add_update_oauth_client_mutation, 71 72 add_delete_oauth_client_mutation,
+107
api/src/graphql/schema_ext/oauth.rs
··· 156 156 oauth_client 157 157 } 158 158 159 + /// Add oauthClients field to NetworkSlicesSlice type 160 + pub fn add_oauth_clients_field_to_slice( 161 + object: Object, 162 + auth_base_url: String, 163 + ) -> Object { 164 + use crate::graphql::schema_builder::RecordContainer; 165 + 166 + let base_url_for_oauth = auth_base_url.clone(); 167 + 168 + object.field( 169 + Field::new( 170 + "oauthClients", 171 + TypeRef::named_nn_list_nn("OAuthClient"), 172 + move |ctx| { 173 + let base_url = base_url_for_oauth.clone(); 174 + 175 + FieldFuture::new(async move { 176 + let container = ctx.parent_value.try_downcast_ref::<RecordContainer>()?; 177 + let slice_uri = &container.record.uri; 178 + 179 + // Get pool from context and create database instance 180 + let pool = ctx.data::<sqlx::PgPool>() 181 + .map_err(|_| Error::new("Database pool not found in context"))?; 182 + let database = crate::database::Database::new(pool.clone()); 183 + 184 + // Fetch OAuth clients from database 185 + let clients = database 186 + .get_oauth_clients_for_slice(slice_uri) 187 + .await 188 + .map_err(|e| Error::new(format!("Failed to fetch OAuth clients: {}", e)))?; 189 + 190 + if clients.is_empty() { 191 + return Ok(Some(FieldValue::list(Vec::<FieldValue<'_>>::new()))); 192 + } 193 + 194 + // Fetch details from AIP server 195 + let http_client = Client::new(); 196 + let mut client_data_list = Vec::new(); 197 + 198 + for oauth_client in clients { 199 + let aip_url = format!("{}/oauth/clients/{}", base_url, oauth_client.client_id); 200 + let mut request_builder = http_client.get(&aip_url); 201 + 202 + if let Some(token) = &oauth_client.registration_access_token { 203 + request_builder = request_builder.bearer_auth(token); 204 + } 205 + 206 + match request_builder.send().await { 207 + Ok(response) if response.status().is_success() => { 208 + if let Ok(response_text) = response.text().await { 209 + if let Ok(aip_client) = serde_json::from_str::<AipClientResponse>(&response_text) { 210 + client_data_list.push(OAuthClientData { 211 + client_id: aip_client.client_id, 212 + client_secret: aip_client.client_secret, 213 + client_name: aip_client.client_name, 214 + redirect_uris: aip_client.redirect_uris, 215 + grant_types: aip_client.grant_types, 216 + response_types: aip_client.response_types, 217 + scope: aip_client.scope, 218 + client_uri: aip_client.client_uri, 219 + logo_uri: aip_client.logo_uri, 220 + tos_uri: aip_client.tos_uri, 221 + policy_uri: aip_client.policy_uri, 222 + created_at: oauth_client.created_at, 223 + created_by_did: oauth_client.created_by_did, 224 + }); 225 + } 226 + } 227 + } 228 + _ => { 229 + // Fallback for clients we can't fetch details for 230 + client_data_list.push(OAuthClientData { 231 + client_id: oauth_client.client_id, 232 + client_secret: None, 233 + client_name: "Unknown".to_string(), 234 + redirect_uris: vec![], 235 + grant_types: vec!["authorization_code".to_string()], 236 + response_types: vec!["code".to_string()], 237 + scope: None, 238 + client_uri: None, 239 + logo_uri: None, 240 + tos_uri: None, 241 + policy_uri: None, 242 + created_at: oauth_client.created_at, 243 + created_by_did: oauth_client.created_by_did, 244 + }); 245 + } 246 + } 247 + } 248 + 249 + // Convert to GraphQL values 250 + let field_values: Vec<FieldValue<'_>> = client_data_list 251 + .into_iter() 252 + .map(|client_data| { 253 + let container = OAuthClientContainer { client: client_data }; 254 + FieldValue::owned_any(container) 255 + }) 256 + .collect(); 257 + 258 + Ok(Some(FieldValue::list(field_values))) 259 + }) 260 + }, 261 + ) 262 + .description("Get all OAuth clients for this slice") 263 + ) 264 + } 265 + 159 266 /// Add oauthClients query to the Query type 160 267 pub fn add_oauth_clients_query(query: Object, slice_uri: String, auth_base_url: String) -> Object { 161 268 query.field(
+3
frontend-v2/schema.graphql
··· 1495 1495 Get statistics for this slice including collection counts, record counts, and actor counts 1496 1496 """ 1497 1497 stats: SliceStats! 1498 + 1499 + """Get all OAuth clients for this slice""" 1500 + oauthClients: [OAuthClient!]! 1498 1501 } 1499 1502 1500 1503 type NetworkSlicesSliceAggregated {
+66 -68
frontend-v2/src/__generated__/OAuthClientsQuery.graphql.ts
··· 1 1 /** 2 - * @generated SignedSource<<4c24e57ecdb7163fc62f4069422aac37>> 2 + * @generated SignedSource<<4bc4595b4bf2e4b263476f66a31ccca4>> 3 3 * @lightSyntaxTransform 4 4 * @nogrep 5 5 */ ··· 41 41 lte?: string | null | undefined; 42 42 }; 43 43 export type OAuthClientsQuery$variables = { 44 - slice: string; 45 44 where?: NetworkSlicesSliceWhereInput | null | undefined; 46 45 }; 47 46 export type OAuthClientsQuery$data = { ··· 51 50 readonly actorHandle: string | null | undefined; 52 51 readonly did: string; 53 52 readonly name: string; 53 + readonly oauthClients: ReadonlyArray<{ 54 + readonly clientId: string; 55 + readonly clientName: string; 56 + readonly clientSecret: string | null | undefined; 57 + readonly clientUri: string | null | undefined; 58 + readonly createdAt: string; 59 + readonly createdByDid: string; 60 + readonly logoUri: string | null | undefined; 61 + readonly policyUri: string | null | undefined; 62 + readonly redirectUris: ReadonlyArray<string>; 63 + readonly scope: string | null | undefined; 64 + readonly tosUri: string | null | undefined; 65 + }>; 66 + readonly uri: string; 54 67 }; 55 68 }>; 56 69 }; 57 - readonly oauthClients: ReadonlyArray<{ 58 - readonly clientId: string; 59 - readonly clientName: string; 60 - readonly clientSecret: string | null | undefined; 61 - readonly clientUri: string | null | undefined; 62 - readonly createdAt: string; 63 - readonly createdByDid: string; 64 - readonly logoUri: string | null | undefined; 65 - readonly policyUri: string | null | undefined; 66 - readonly redirectUris: ReadonlyArray<string>; 67 - readonly scope: string | null | undefined; 68 - readonly tosUri: string | null | undefined; 69 - }>; 70 70 }; 71 71 export type OAuthClientsQuery = { 72 72 response: OAuthClientsQuery$data; ··· 78 78 { 79 79 "defaultValue": null, 80 80 "kind": "LocalArgument", 81 - "name": "slice" 81 + "name": "where" 82 + } 83 + ], 84 + v1 = [ 85 + { 86 + "kind": "Literal", 87 + "name": "first", 88 + "value": 1 82 89 }, 83 90 { 84 - "defaultValue": null, 85 - "kind": "LocalArgument", 86 - "name": "where" 91 + "kind": "Variable", 92 + "name": "where", 93 + "variableName": "where" 87 94 } 88 95 ], 89 - v1 = { 96 + v2 = { 97 + "alias": null, 98 + "args": null, 99 + "kind": "ScalarField", 100 + "name": "name", 101 + "storageKey": null 102 + }, 103 + v3 = { 90 104 "alias": null, 91 - "args": [ 92 - { 93 - "kind": "Variable", 94 - "name": "slice", 95 - "variableName": "slice" 96 - } 97 - ], 105 + "args": null, 106 + "kind": "ScalarField", 107 + "name": "did", 108 + "storageKey": null 109 + }, 110 + v4 = { 111 + "alias": null, 112 + "args": null, 113 + "kind": "ScalarField", 114 + "name": "actorHandle", 115 + "storageKey": null 116 + }, 117 + v5 = { 118 + "alias": null, 119 + "args": null, 120 + "kind": "ScalarField", 121 + "name": "uri", 122 + "storageKey": null 123 + }, 124 + v6 = { 125 + "alias": null, 126 + "args": null, 98 127 "concreteType": "OAuthClient", 99 128 "kind": "LinkedField", 100 129 "name": "oauthClients", ··· 179 208 } 180 209 ], 181 210 "storageKey": null 182 - }, 183 - v2 = [ 184 - { 185 - "kind": "Literal", 186 - "name": "first", 187 - "value": 1 188 - }, 189 - { 190 - "kind": "Variable", 191 - "name": "where", 192 - "variableName": "where" 193 - } 194 - ], 195 - v3 = { 196 - "alias": null, 197 - "args": null, 198 - "kind": "ScalarField", 199 - "name": "name", 200 - "storageKey": null 201 - }, 202 - v4 = { 203 - "alias": null, 204 - "args": null, 205 - "kind": "ScalarField", 206 - "name": "did", 207 - "storageKey": null 208 - }, 209 - v5 = { 210 - "alias": null, 211 - "args": null, 212 - "kind": "ScalarField", 213 - "name": "actorHandle", 214 - "storageKey": null 215 211 }; 216 212 return { 217 213 "fragment": { ··· 220 216 "metadata": null, 221 217 "name": "OAuthClientsQuery", 222 218 "selections": [ 223 - (v1/*: any*/), 224 219 { 225 220 "alias": null, 226 - "args": (v2/*: any*/), 221 + "args": (v1/*: any*/), 227 222 "concreteType": "NetworkSlicesSliceConnection", 228 223 "kind": "LinkedField", 229 224 "name": "networkSlicesSlices", ··· 245 240 "name": "node", 246 241 "plural": false, 247 242 "selections": [ 243 + (v2/*: any*/), 248 244 (v3/*: any*/), 249 245 (v4/*: any*/), 250 - (v5/*: any*/) 246 + (v5/*: any*/), 247 + (v6/*: any*/) 251 248 ], 252 249 "storageKey": null 253 250 } ··· 267 264 "kind": "Operation", 268 265 "name": "OAuthClientsQuery", 269 266 "selections": [ 270 - (v1/*: any*/), 271 267 { 272 268 "alias": null, 273 - "args": (v2/*: any*/), 269 + "args": (v1/*: any*/), 274 270 "concreteType": "NetworkSlicesSliceConnection", 275 271 "kind": "LinkedField", 276 272 "name": "networkSlicesSlices", ··· 292 288 "name": "node", 293 289 "plural": false, 294 290 "selections": [ 291 + (v2/*: any*/), 295 292 (v3/*: any*/), 296 293 (v4/*: any*/), 297 294 (v5/*: any*/), 295 + (v6/*: any*/), 298 296 { 299 297 "alias": null, 300 298 "args": null, ··· 314 312 ] 315 313 }, 316 314 "params": { 317 - "cacheID": "20abc4b49d5c52da4a3ad1935662056a", 315 + "cacheID": "953a2b7074ba3074cca3f11991af440e", 318 316 "id": null, 319 317 "metadata": {}, 320 318 "name": "OAuthClientsQuery", 321 319 "operationKind": "query", 322 - "text": "query OAuthClientsQuery(\n $slice: String!\n $where: NetworkSlicesSliceWhereInput\n) {\n oauthClients(slice: $slice) {\n clientId\n clientSecret\n clientName\n redirectUris\n scope\n clientUri\n logoUri\n tosUri\n policyUri\n createdAt\n createdByDid\n }\n networkSlicesSlices(first: 1, where: $where) {\n edges {\n node {\n name\n did\n actorHandle\n id\n }\n }\n }\n}\n" 320 + "text": "query OAuthClientsQuery(\n $where: NetworkSlicesSliceWhereInput\n) {\n networkSlicesSlices(first: 1, where: $where) {\n edges {\n node {\n name\n did\n actorHandle\n uri\n oauthClients {\n clientId\n clientSecret\n clientName\n redirectUris\n scope\n clientUri\n logoUri\n tosUri\n policyUri\n createdAt\n createdByDid\n }\n id\n }\n }\n }\n}\n" 323 321 } 324 322 }; 325 323 })(); 326 324 327 - (node as any).hash = "b3eda4c7e0bda285a5261efa81e7b5cd"; 325 + (node as any).hash = "4c0e3d21f0879129255130f260edcb75"; 328 326 329 327 export default node;
+19 -23
frontend-v2/src/pages/OAuthClients.tsx
··· 138 138 export default function OAuthClients() { 139 139 const { handle, rkey } = useParams<{ handle: string; rkey: string }>(); 140 140 141 - // Build slice URI from params 142 - const sliceUri = 143 - `at://did:placeholder/${handle}/network.slices.slice/${rkey}`; 144 - 145 141 return ( 146 142 <Suspense 147 143 fallback={ ··· 152 148 </Layout> 153 149 } 154 150 > 155 - <OAuthClientsWrapper sliceUri={sliceUri} handle={handle!} rkey={rkey!} /> 151 + <OAuthClientsWrapper handle={handle!} rkey={rkey!} /> 156 152 </Suspense> 157 153 ); 158 154 } 159 155 160 156 function OAuthClientsWrapper( 161 - { sliceUri, handle, rkey }: { 162 - sliceUri: string; 157 + { handle, rkey }: { 163 158 handle: string; 164 159 rkey: string; 165 160 }, 166 161 ) { 167 162 const { session } = useSessionContext(); 163 + 168 164 const data = useLazyLoadQuery<OAuthClientsQuery>( 169 165 graphql` 170 166 query OAuthClientsQuery( 171 - $slice: String! 172 167 $where: NetworkSlicesSliceWhereInput 173 168 ) { 174 - oauthClients(slice: $slice) { 175 - clientId 176 - clientSecret 177 - clientName 178 - redirectUris 179 - scope 180 - clientUri 181 - logoUri 182 - tosUri 183 - policyUri 184 - createdAt 185 - createdByDid 186 - } 187 169 networkSlicesSlices(first: 1, where: $where) { 188 170 edges { 189 171 node { 190 172 name 191 173 did 192 174 actorHandle 175 + uri 176 + oauthClients { 177 + clientId 178 + clientSecret 179 + clientName 180 + redirectUris 181 + scope 182 + clientUri 183 + logoUri 184 + tosUri 185 + policyUri 186 + createdAt 187 + createdByDid 188 + } 193 189 } 194 190 } 195 191 } 196 192 } 197 193 `, 198 194 { 199 - slice: sliceUri, 200 195 where: { 201 196 actorHandle: { eq: handle }, 202 197 uri: { contains: rkey }, ··· 207 202 208 203 const slice = data.networkSlicesSlices.edges[0]?.node; 209 204 const sliceName = slice?.name; 205 + const sliceUri = slice?.uri || `at://${slice?.did}/network.slices.slice/${rkey}`; 210 206 211 207 // Check if current user is the slice owner or admin 212 208 const isOwner = isSliceOwner(slice, session); ··· 229 225 } 230 226 > 231 227 <OAuthClientsContent 232 - clients={data.oauthClients || []} 228 + clients={slice?.oauthClients || []} 233 229 sliceUri={sliceUri} 234 230 /> 235 231 </Layout>