The smokesignal.events web application

feature: sync did to tap

+159 -2
+81 -1
src/http/handle_admin_handles.rs
··· 1 1 use anyhow::Result; 2 + use atproto_tap::TapClient; 2 3 use axum::{ 3 4 extract::{Path, Query}, 4 5 response::{IntoResponse, Redirect}, ··· 16 17 pagination::{Pagination, PaginationView}, 17 18 }, 18 19 select_template, 19 - storage::identity_profile::{handle_list, handle_nuke}, 20 + storage::identity_profile::{get_all_dids, handle_list, handle_nuke}, 20 21 }; 21 22 22 23 pub(crate) async fn handle_admin_handles( ··· 108 109 Ok(Redirect::to("/admin/handles").into_response()) 109 110 } 110 111 } 112 + 113 + /// Sync all known DIDs to TAP for event streaming 114 + pub(crate) async fn handle_admin_tap_sync_all( 115 + admin_ctx: AdminRequestContext, 116 + HxRequest(hx_request): HxRequest, 117 + ) -> Result<impl IntoResponse, WebError> { 118 + let error_template = select_template!(false, false, admin_ctx.language); 119 + 120 + // Check if TAP is enabled 121 + if !admin_ctx.web_context.config.enable_tap { 122 + return contextual_error!( 123 + admin_ctx.web_context, 124 + admin_ctx.language, 125 + error_template, 126 + template_context! { 127 + message => "TAP is not enabled in the configuration." 128 + }, 129 + "TAP is not enabled" 130 + ); 131 + } 132 + 133 + // Get all DIDs from the database 134 + let dids = match get_all_dids(&admin_ctx.web_context.pool).await { 135 + Ok(dids) => dids, 136 + Err(err) => { 137 + return contextual_error!( 138 + admin_ctx.web_context, 139 + admin_ctx.language, 140 + error_template, 141 + template_context! {}, 142 + err 143 + ); 144 + } 145 + }; 146 + 147 + if dids.is_empty() { 148 + tracing::info!("No DIDs to sync to TAP"); 149 + if hx_request { 150 + let hx_redirect = HxRedirect::from("/admin/identity_profiles"); 151 + return Ok((StatusCode::OK, hx_redirect, "").into_response()); 152 + } else { 153 + return Ok(Redirect::to("/admin/identity_profiles").into_response()); 154 + } 155 + } 156 + 157 + // Create TAP client and sync DIDs 158 + let tap_client = TapClient::new( 159 + &admin_ctx.web_context.config.tap_hostname, 160 + admin_ctx.web_context.config.tap_password.clone(), 161 + ); 162 + 163 + // Convert Vec<String> to Vec<&str> for add_repos 164 + let did_refs: Vec<&str> = dids.iter().map(|s| s.as_str()).collect(); 165 + 166 + match tap_client.add_repos(&did_refs).await { 167 + Ok(()) => { 168 + tracing::info!(count = dids.len(), "Successfully synced all DIDs to TAP"); 169 + } 170 + Err(err) => { 171 + tracing::error!(error = %err, "Failed to sync DIDs to TAP"); 172 + return contextual_error!( 173 + admin_ctx.web_context, 174 + admin_ctx.language, 175 + error_template, 176 + template_context! { 177 + message => format!("Failed to sync DIDs to TAP: {}", err) 178 + }, 179 + err 180 + ); 181 + } 182 + } 183 + 184 + if hx_request { 185 + let hx_redirect = HxRedirect::from("/admin/identity_profiles"); 186 + Ok((StatusCode::OK, hx_redirect, "").into_response()) 187 + } else { 188 + Ok(Redirect::to("/admin/identity_profiles").into_response()) 189 + } 190 + }
+19
src/http/handle_oauth_aip_callback.rs
··· 10 10 use anyhow::Result; 11 11 use atproto_client::errors::SimpleError; 12 12 use atproto_identity::resolve::IdentityResolver; 13 + use atproto_tap::TapClient; 13 14 use axum::{ 14 15 extract::State, 15 16 response::{IntoResponse, Redirect}, ··· 212 213 { 213 214 // Log the error but don't fail the authentication flow 214 215 tracing::warn!(?err, "Failed to send confirmation email to new user"); 216 + } 217 + 218 + // Register new user's DID with TAP for event streaming 219 + if is_new_user && web_context.config.enable_tap { 220 + let tap_client = TapClient::new( 221 + &web_context.config.tap_hostname, 222 + web_context.config.tap_password.clone(), 223 + ); 224 + if let Err(err) = tap_client.add_repos(&[&document.id]).await { 225 + // Log the error but don't fail the authentication flow 226 + tracing::warn!( 227 + did = %document.id, 228 + error = %err, 229 + "Failed to register DID with TAP" 230 + ); 231 + } else { 232 + tracing::info!(did = %document.id, "Registered new user DID with TAP"); 233 + } 215 234 } 216 235 217 236 // Import Bluesky profile for new users
+19
src/http/handle_oauth_callback.rs
··· 9 9 resources::oauth_authorization_server, 10 10 workflow::{OAuthClient, oauth_complete}, 11 11 }; 12 + use atproto_tap::TapClient; 12 13 use axum::{ 13 14 extract::State, 14 15 response::{IntoResponse, Redirect}, ··· 235 236 return contextual_error!(web_context, language, error_template, default_context, err); 236 237 } 237 238 }; 239 + 240 + // Register new user's DID with TAP for event streaming 241 + if is_new_user && web_context.config.enable_tap { 242 + let tap_client = TapClient::new( 243 + &web_context.config.tap_hostname, 244 + web_context.config.tap_password.clone(), 245 + ); 246 + if let Err(err) = tap_client.add_repos(&[&document.id]).await { 247 + // Log the error but don't fail the authentication flow 248 + tracing::warn!( 249 + did = %document.id, 250 + error = %err, 251 + "Failed to register DID with TAP" 252 + ); 253 + } else { 254 + tracing::info!(did = %document.id, "Registered new user DID with TAP"); 255 + } 256 + } 238 257 239 258 // Import Bluesky profile for new users 240 259 if is_new_user {
+5 -1
src/http/server.rs
··· 25 25 }, 26 26 handle_admin_event::{handle_admin_event, handle_admin_event_nuke}, 27 27 handle_admin_events::handle_admin_events, 28 - handle_admin_handles::{handle_admin_handles, handle_admin_nuke_identity}, 28 + handle_admin_handles::{handle_admin_handles, handle_admin_nuke_identity, handle_admin_tap_sync_all}, 29 29 handle_admin_identity_profile::{ 30 30 handle_admin_identity_profile, handle_admin_identity_profile_ban_did, 31 31 handle_admin_identity_profile_ban_pds, handle_admin_identity_profile_nuke, ··· 176 176 router 177 177 .route("/admin", get(handle_admin_index)) 178 178 .route("/admin/identity_profiles", get(handle_admin_handles)) 179 + .route( 180 + "/admin/identity_profiles/tap-sync", 181 + post(handle_admin_tap_sync_all), 182 + ) 179 183 .route( 180 184 "/admin/identity_profile", 181 185 get(handle_admin_identity_profile),
+10
src/storage/identity_profile.rs
··· 447 447 )) 448 448 } 449 449 450 + /// Get all DIDs from identity profiles 451 + pub async fn get_all_dids(pool: &StoragePool) -> Result<Vec<String>, StorageError> { 452 + let dids = sqlx::query_scalar::<_, String>("SELECT did FROM identity_profiles ORDER BY did") 453 + .fetch_all(pool) 454 + .await 455 + .map_err(StorageError::UnableToExecuteQuery)?; 456 + 457 + Ok(dids) 458 + } 459 + 450 460 /// Get an identity profile by DID 451 461 pub async fn identity_profile_get( 452 462 pool: &StoragePool,
+25
templates/en-us/admin_handles.html
··· 6 6 <h1 class="title">Identity Records ({{ total_count }})</h1> 7 7 <p class="subtitle">View all registered identities</p> 8 8 9 + {# TAP Sync Section #} 10 + <div class="box mb-5"> 11 + <h2 class="title is-5"> 12 + <span class="icon-text"> 13 + <span class="icon"><i class="fas fa-sync"></i></span> 14 + <span>TAP Sync</span> 15 + </span> 16 + </h2> 17 + <div class="content"> 18 + <p> 19 + Sync all known identity DIDs to TAP (Trusted Attestation Protocol) for event streaming. 20 + This ensures TAP is tracking repositories for all registered users. 21 + </p> 22 + </div> 23 + <button class="button is-info" 24 + hx-post="/admin/identity_profiles/tap-sync" 25 + hx-confirm="Are you sure you want to sync all {{ total_count }} DIDs to TAP? This may take a moment." 26 + hx-target="body" 27 + data-loading-disable 28 + data-loading-class="is-loading"> 29 + <span class="icon"><i class="fas fa-sync"></i></span> 30 + <span>Sync All DIDs to TAP</span> 31 + </button> 32 + </div> 33 + 9 34 <table class="table is-fullwidth is-striped"> 10 35 <thead> 11 36 <tr>