+15
-11
api/src/graphql/schema_builder.rs
+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
+1
api/src/graphql/schema_ext/mod.rs
+107
api/src/graphql/schema_ext/oauth.rs
+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
+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
+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
+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>