The smokesignal.events web application

chore: formatting

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

+359 -276
+4 -3
src/atproto/lexicon/events_smokesignal_calendar_event.rs
··· 143 } = &event_response.value; 144 145 assert_eq!(name, "Pigeons Playing Ping Pong @ Neptune Theatre"); 146 - assert!(text 147 - .as_ref() 148 - .is_some_and(|value| value == "Pigeons Playing Ping Pong @ Neptune Theatre")); 149 150 // Verify datetime fields are present and correctly parsed 151 assert!(starts_at.is_some(), "Expected starts_at to be present");
··· 143 } = &event_response.value; 144 145 assert_eq!(name, "Pigeons Playing Ping Pong @ Neptune Theatre"); 146 + assert!( 147 + text.as_ref() 148 + .is_some_and(|value| value == "Pigeons Playing Ping Pong @ Neptune Theatre") 149 + ); 150 151 // Verify datetime fields are present and correctly parsed 152 assert!(starts_at.is_some(), "Expected starts_at to be present");
+8 -6
src/bin/crypto.rs
··· 1 use std::env; 2 3 - use base64::{engine::general_purpose, Engine as _}; 4 use rand::RngCore; 5 6 fn main() { 7 let mut rng = rand::thread_rng(); 8 9 - env::args().for_each(|arg| if arg.as_str() == "key" { 10 - let mut key: [u8; 64] = [0; 64]; 11 - rng.fill_bytes(&mut key); 12 - let encoded: String = general_purpose::STANDARD_NO_PAD.encode(key); 13 - println!("{encoded}"); 14 }); 15 }
··· 1 use std::env; 2 3 + use base64::{Engine as _, engine::general_purpose}; 4 use rand::RngCore; 5 6 fn main() { 7 let mut rng = rand::thread_rng(); 8 9 + env::args().for_each(|arg| { 10 + if arg.as_str() == "key" { 11 + let mut key: [u8; 64] = [0; 64]; 12 + rng.fill_bytes(&mut key); 13 + let encoded: String = general_purpose::STANDARD_NO_PAD.encode(key); 14 + println!("{encoded}"); 15 + } 16 }); 17 }
+1 -1
src/bin/smokesignal.rs
··· 1 use anyhow::Result; 2 use atproto_identity::key::identify_key; 3 - use atproto_identity::resolve::{create_resolver, IdentityResolver, InnerIdentityResolver}; 4 use atproto_oauth_axum::state::OAuthClientConfig; 5 use smokesignal::{ 6 http::{
··· 1 use anyhow::Result; 2 use atproto_identity::key::identify_key; 3 + use atproto_identity::resolve::{IdentityResolver, InnerIdentityResolver, create_resolver}; 4 use atproto_oauth_axum::state::OAuthClientConfig; 5 use smokesignal::{ 6 http::{
+2 -2
src/config.rs
··· 1 use anyhow::{Context, Result}; 2 - use atproto_identity::key::{identify_key, to_public, KeyData, KeyType}; 3 use axum_extra::extract::cookie::Key; 4 - use base64::{engine::general_purpose, Engine as _}; 5 use ordermap::OrderMap; 6 7 use crate::config_errors::ConfigError;
··· 1 use anyhow::{Context, Result}; 2 + use atproto_identity::key::{KeyData, KeyType, identify_key, to_public}; 3 use axum_extra::extract::cookie::Key; 4 + use base64::{Engine as _, engine::general_purpose}; 5 use ordermap::OrderMap; 6 7 use crate::config_errors::ConfigError;
+3 -1
src/config_errors.rs
··· 130 /// 131 /// This error occurs when oauth_backend is set to "aip" but 132 /// required AIP configuration values are missing. 133 - #[error("error-config-18 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set")] 134 AipConfigurationIncomplete, 135 136 /// Error when oauth_backend has an invalid value.
··· 130 /// 131 /// This error occurs when oauth_backend is set to "aip" but 132 /// required AIP configuration values are missing. 133 + #[error( 134 + "error-config-18 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set" 135 + )] 136 AipConfigurationIncomplete, 137 138 /// Error when oauth_backend has an invalid value.
+1 -1
src/http/cache_countries.rs
··· 1 - use anyhow::{anyhow, Result}; 2 use once_cell::sync::OnceCell; 3 use std::{collections::BTreeMap, sync::Arc}; 4
··· 1 + use anyhow::{Result, anyhow}; 2 use once_cell::sync::OnceCell; 3 use std::{collections::BTreeMap, sync::Arc}; 4
-17
src/http/errors/middleware_errors.rs
··· 25 SerializeFailed(serde_json::Error), 26 } 27 28 - /// Represents errors that can occur during authentication middleware operations. 29 - /// 30 - /// These errors typically happen in the authentication middleware layer when 31 - /// processing requests, including cryptographic operations and session validation. 32 - #[derive(Debug, Error)] 33 - pub(crate) enum AuthMiddlewareError { 34 - /// Error when content signing fails. 35 - /// 36 - /// This error occurs when the authentication middleware attempts to 37 - /// cryptographically sign content but the operation fails. 38 - #[error("error-authmiddleware-1 Unable to sign content: {0:?}")] 39 - SigningFailed(anyhow::Error), 40 - } 41 - 42 #[derive(Debug, Error)] 43 pub(crate) enum MiddlewareAuthError { 44 #[error("error-middleware-auth-1 Access Denied: {0}")] ··· 49 50 #[error("error-middleware-auth-3 Unhandled Auth Error: {0:?}")] 51 Anyhow(#[from] anyhow::Error), 52 - 53 - #[error(transparent)] 54 - AuthError(#[from] AuthMiddlewareError), 55 } 56 57 impl IntoResponse for MiddlewareAuthError {
··· 25 SerializeFailed(serde_json::Error), 26 } 27 28 #[derive(Debug, Error)] 29 pub(crate) enum MiddlewareAuthError { 30 #[error("error-middleware-auth-1 Access Denied: {0}")] ··· 35 36 #[error("error-middleware-auth-3 Unhandled Auth Error: {0:?}")] 37 Anyhow(#[from] anyhow::Error), 38 } 39 40 impl IntoResponse for MiddlewareAuthError {
+3 -1
src/http/errors/migrate_rsvp_error.rs
··· 11 /// This error occurs when attempting to migrate an RSVP with a status 12 /// that doesn't match one of the expected values ('going', 'interested', 13 /// or 'notgoing'). 14 - #[error("error-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'.")] 15 InvalidRsvpStatus(String), 16 17 /// Error when a user is not authorized to migrate an RSVP.
··· 11 /// This error occurs when attempting to migrate an RSVP with a status 12 /// that doesn't match one of the expected values ('going', 'interested', 13 /// or 'notgoing'). 14 + #[error( 15 + "error-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'." 16 + )] 17 InvalidRsvpStatus(String), 18 19 /// Error when a user is not authorized to migrate an RSVP.
+1 -1
src/http/errors/mod.rs
··· 21 pub(crate) use event_view_errors::EventViewError; 22 pub(crate) use import_error::ImportError; 23 pub(crate) use login_error::LoginError; 24 - pub(crate) use middleware_errors::{AuthMiddlewareError, WebSessionError}; 25 pub(crate) use migrate_event_error::MigrateEventError; 26 pub(crate) use migrate_rsvp_error::MigrateRsvpError; 27 pub(crate) use rsvp_error::RSVPError;
··· 21 pub(crate) use event_view_errors::EventViewError; 22 pub(crate) use import_error::ImportError; 23 pub(crate) use login_error::LoginError; 24 + pub(crate) use middleware_errors::WebSessionError; 25 pub(crate) use migrate_event_error::MigrateEventError; 26 pub(crate) use migrate_rsvp_error::MigrateRsvpError; 27 pub(crate) use rsvp_error::RSVPError;
+1 -1
src/http/event_view.rs
··· 17 }, 18 http::utils::truncate_text, 19 storage::{ 20 errors::StorageError, 21 event::{ 22 count_event_rsvps, extract_event_details, get_event_rsvp_counts, 23 model::{Event, EventWithRole}, 24 }, 25 identity_profile::{handles_by_did, model::IdentityProfile}, 26 - StoragePool, 27 }, 28 }; 29
··· 17 }, 18 http::utils::truncate_text, 19 storage::{ 20 + StoragePool, 21 errors::StorageError, 22 event::{ 23 count_event_rsvps, extract_event_details, get_event_rsvp_counts, 24 model::{Event, EventWithRole}, 25 }, 26 identity_profile::{handles_by_did, model::IdentityProfile}, 27 }, 28 }; 29
+2 -2
src/http/handle_admin_denylist.rs
··· 1 use anyhow::Result; 2 use axum::{ 3 extract::Query, 4 response::{IntoResponse, Redirect}, 5 - Form, 6 }; 7 use axum_template::RenderHtml; 8 use minijinja::context as template_context; ··· 12 use crate::{ 13 contextual_error, 14 http::{ 15 - context::{admin_template_context, AdminRequestContext}, 16 errors::WebError, 17 pagination::{Pagination, PaginationView}, 18 },
··· 1 use anyhow::Result; 2 use axum::{ 3 + Form, 4 extract::Query, 5 response::{IntoResponse, Redirect}, 6 }; 7 use axum_template::RenderHtml; 8 use minijinja::context as template_context; ··· 12 use crate::{ 13 contextual_error, 14 http::{ 15 + context::{AdminRequestContext, admin_template_context}, 16 errors::WebError, 17 pagination::{Pagination, PaginationView}, 18 },
+1 -1
src/http/handle_admin_handles.rs
··· 11 use crate::{ 12 contextual_error, 13 http::{ 14 - context::{admin_template_context, AdminRequestContext}, 15 errors::WebError, 16 pagination::{Pagination, PaginationView}, 17 },
··· 11 use crate::{ 12 contextual_error, 13 http::{ 14 + context::{AdminRequestContext, admin_template_context}, 15 errors::WebError, 16 pagination::{Pagination, PaginationView}, 17 },
+1 -1
src/http/handle_admin_import_event.rs
··· 16 }, 17 contextual_error, 18 http::{ 19 - context::{admin_template_context, AdminRequestContext}, 20 errors::{AdminImportEventError, CommonError, LoginError, WebError}, 21 }, 22 select_template,
··· 16 }, 17 contextual_error, 18 http::{ 19 + context::{AdminRequestContext, admin_template_context}, 20 errors::{AdminImportEventError, CommonError, LoginError, WebError}, 21 }, 22 select_template,
+4 -4
src/http/handle_admin_import_rsvp.rs
··· 13 use crate::{ 14 atproto::lexicon::{ 15 community::lexicon::calendar::rsvp::{ 16 - Rsvp as CommunityRsvpLexicon, RsvpStatus as CommunityRsvpStatusLexicon, 17 - NSID as COMMUNITY_RSVP_NSID, 18 }, 19 events::smokesignal::calendar::rsvp::{ 20 - Rsvp as SmokesignalRsvpLexicon, NSID as SMOKESIGNAL_RSVP_NSID, 21 }, 22 }, 23 contextual_error, 24 http::{ 25 - context::{admin_template_context, AdminRequestContext}, 26 errors::{AdminImportRsvpError, CommonError, LoginError, WebError}, 27 }, 28 select_template,
··· 13 use crate::{ 14 atproto::lexicon::{ 15 community::lexicon::calendar::rsvp::{ 16 + NSID as COMMUNITY_RSVP_NSID, Rsvp as CommunityRsvpLexicon, 17 + RsvpStatus as CommunityRsvpStatusLexicon, 18 }, 19 events::smokesignal::calendar::rsvp::{ 20 + NSID as SMOKESIGNAL_RSVP_NSID, Rsvp as SmokesignalRsvpLexicon, 21 }, 22 }, 23 contextual_error, 24 http::{ 25 + context::{AdminRequestContext, admin_template_context}, 26 errors::{AdminImportRsvpError, CommonError, LoginError, WebError}, 27 }, 28 select_template,
+1 -1
src/http/handle_admin_index.rs
··· 3 use axum_template::RenderHtml; 4 use minijinja::context as template_context; 5 6 - use crate::http::context::{admin_template_context, AdminRequestContext}; 7 8 use super::errors::WebError; 9
··· 3 use axum_template::RenderHtml; 4 use minijinja::context as template_context; 5 6 + use crate::http::context::{AdminRequestContext, admin_template_context}; 7 8 use super::errors::WebError; 9
+2 -2
src/http/handle_create_event.rs
··· 1 - use std::collections::HashMap; 2 use anyhow::Result; 3 use axum::extract::State; 4 use axum::response::IntoResponse; ··· 11 use http::Method; 12 use http::StatusCode; 13 use minijinja::context as template_context; 14 15 use crate::atproto::auth::{ 16 create_dpop_auth_from_aip_session, create_dpop_auth_from_oauth_session, ··· 54 HxBoosted(hx_boosted): HxBoosted, 55 Form(mut build_event_form): Form<BuildEventForm>, 56 ) -> Result<impl IntoResponse, WebError> { 57 - let current_handle = auth.require(&web_context.config, "/event")?; 58 59 let is_development = cfg!(debug_assertions); 60
··· 1 use anyhow::Result; 2 use axum::extract::State; 3 use axum::response::IntoResponse; ··· 10 use http::Method; 11 use http::StatusCode; 12 use minijinja::context as template_context; 13 + use std::collections::HashMap; 14 15 use crate::atproto::auth::{ 16 create_dpop_auth_from_aip_session, create_dpop_auth_from_oauth_session, ··· 54 HxBoosted(hx_boosted): HxBoosted, 55 Form(mut build_event_form): Form<BuildEventForm>, 56 ) -> Result<impl IntoResponse, WebError> { 57 + let current_handle = auth.require("/event")?; 58 59 let is_development = cfg!(debug_assertions); 60
+8 -7
src/http/handle_create_rsvp.rs
··· 16 use crate::{ 17 atproto::lexicon::{ 18 com::atproto::repo::StrongRef, 19 - community::lexicon::calendar::rsvp::{Rsvp, RsvpStatus, NSID}, 20 }, 21 contextual_error, 22 http::{ ··· 28 utils::url_from_aturi, 29 }, 30 select_template, 31 - storage::{ 32 - event::{rsvp_insert_with_metadata, RsvpInsertParams} 33 - }, 34 }; 35 - use atproto_client::com::atproto::repo::{put_record, PutRecordRequest, PutRecordResponse}; 36 37 pub(crate) async fn handle_create_rsvp( 38 method: Method, ··· 43 HxBoosted(hx_boosted): HxBoosted, 44 Form(mut build_rsvp_form): Form<BuildRSVPForm>, 45 ) -> Result<impl IntoResponse, WebError> { 46 - let current_handle = auth.require(&web_context.config, "/rsvp")?; 47 48 // Check if user has email address set 49 - let identity_has_email = current_handle.email.as_ref().is_some_and(|value| !value.is_empty()); 50 51 let default_context = template_context! { 52 current_handle,
··· 16 use crate::{ 17 atproto::lexicon::{ 18 com::atproto::repo::StrongRef, 19 + community::lexicon::calendar::rsvp::{NSID, Rsvp, RsvpStatus}, 20 }, 21 contextual_error, 22 http::{ ··· 28 utils::url_from_aturi, 29 }, 30 select_template, 31 + storage::event::{RsvpInsertParams, rsvp_insert_with_metadata}, 32 }; 33 + use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; 34 35 pub(crate) async fn handle_create_rsvp( 36 method: Method, ··· 41 HxBoosted(hx_boosted): HxBoosted, 42 Form(mut build_rsvp_form): Form<BuildRSVPForm>, 43 ) -> Result<impl IntoResponse, WebError> { 44 + let current_handle = auth.require("/rsvp")?; 45 46 // Check if user has email address set 47 + let identity_has_email = current_handle 48 + .email 49 + .as_ref() 50 + .is_some_and(|value| !value.is_empty()); 51 52 let default_context = template_context! { 53 current_handle,
+57 -57
src/http/handle_delete_event.rs
··· 6 use minijinja::context as template_context; 7 use serde::{Deserialize, Serialize}; 8 9 - use atproto_client::com::atproto::repo::{delete_record, DeleteRecordRequest}; 10 11 use crate::{ 12 atproto::{ 13 - auth::{create_dpop_auth_from_oauth_session, create_dpop_auth_from_aip_session}, 14 lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID, 15 }, 16 contextual_error, 17 http::{context::UserRequestContext, errors::WebError, middleware_auth::Auth}, 18 select_template, 19 - storage::{event::{event_delete, event_exists}}, 20 - config::OAuthBackendConfig, 21 }; 22 23 #[derive(Debug, Deserialize, Serialize)] ··· 80 if form.confirm.as_deref() == Some("true") { 81 // Create DPoP authentication based on auth type 82 let dpop_auth = match &ctx.auth { 83 - Auth::Pds { session, .. } => { 84 - match create_dpop_auth_from_oauth_session(session) { 85 - Ok(auth) => auth, 86 - Err(err) => { 87 - tracing::error!("Failed to create DPoP auth from OAuth session: {}", err); 88 - return contextual_error!( 89 - ctx.web_context, 90 - ctx.language, 91 - error_template, 92 - default_context, 93 - err, 94 - StatusCode::INTERNAL_SERVER_ERROR 95 - ); 96 - } 97 } 98 - } 99 - Auth::Aip { .. } => { 100 - match &ctx.web_context.config.oauth_backend { 101 - OAuthBackendConfig::AIP { hostname, .. } => { 102 - let access_token = match &ctx.auth { 103 - Auth::Aip { access_token, .. } => access_token, 104 - _ => unreachable!("We already matched on Auth::Aip"), 105 - }; 106 - 107 - match create_dpop_auth_from_aip_session( 108 - &ctx.web_context.http_client, 109 - hostname, 110 - access_token, 111 - ).await { 112 - Ok(auth) => auth, 113 - Err(err) => { 114 - tracing::error!("Failed to create DPoP auth from AIP session: {}", err); 115 - return contextual_error!( 116 - ctx.web_context, 117 - ctx.language, 118 - error_template, 119 - default_context, 120 - err, 121 - StatusCode::INTERNAL_SERVER_ERROR 122 - ); 123 - } 124 } 125 } 126 - _ => { 127 - tracing::error!("AIP auth found but OAuth backend is not AIP"); 128 - return contextual_error!( 129 - ctx.web_context, 130 - ctx.language, 131 - error_template, 132 - default_context, 133 - anyhow!("Authentication configuration mismatch"), 134 - StatusCode::INTERNAL_SERVER_ERROR 135 - ); 136 - } 137 } 138 - } 139 Auth::Unauthenticated => { 140 // This should not happen due to the check above 141 return Ok(StatusCode::FORBIDDEN.into_response()); ··· 156 &dpop_auth, 157 &current_handle.pds, 158 delete_record_request, 159 - ).await { 160 Ok(_) => { 161 tracing::info!("Successfully deleted event from PDS: {}", lookup_aturi); 162 }
··· 6 use minijinja::context as template_context; 7 use serde::{Deserialize, Serialize}; 8 9 + use atproto_client::com::atproto::repo::{DeleteRecordRequest, delete_record}; 10 11 use crate::{ 12 atproto::{ 13 + auth::{create_dpop_auth_from_aip_session, create_dpop_auth_from_oauth_session}, 14 lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID, 15 }, 16 + config::OAuthBackendConfig, 17 contextual_error, 18 http::{context::UserRequestContext, errors::WebError, middleware_auth::Auth}, 19 select_template, 20 + storage::event::{event_delete, event_exists}, 21 }; 22 23 #[derive(Debug, Deserialize, Serialize)] ··· 80 if form.confirm.as_deref() == Some("true") { 81 // Create DPoP authentication based on auth type 82 let dpop_auth = match &ctx.auth { 83 + Auth::Pds { session, .. } => match create_dpop_auth_from_oauth_session(session) { 84 + Ok(auth) => auth, 85 + Err(err) => { 86 + tracing::error!("Failed to create DPoP auth from OAuth session: {}", err); 87 + return contextual_error!( 88 + ctx.web_context, 89 + ctx.language, 90 + error_template, 91 + default_context, 92 + err, 93 + StatusCode::INTERNAL_SERVER_ERROR 94 + ); 95 } 96 + }, 97 + Auth::Aip { .. } => match &ctx.web_context.config.oauth_backend { 98 + OAuthBackendConfig::AIP { hostname, .. } => { 99 + let access_token = match &ctx.auth { 100 + Auth::Aip { access_token, .. } => access_token, 101 + _ => unreachable!("We already matched on Auth::Aip"), 102 + }; 103 + 104 + match create_dpop_auth_from_aip_session( 105 + &ctx.web_context.http_client, 106 + hostname, 107 + access_token, 108 + ) 109 + .await 110 + { 111 + Ok(auth) => auth, 112 + Err(err) => { 113 + tracing::error!("Failed to create DPoP auth from AIP session: {}", err); 114 + return contextual_error!( 115 + ctx.web_context, 116 + ctx.language, 117 + error_template, 118 + default_context, 119 + err, 120 + StatusCode::INTERNAL_SERVER_ERROR 121 + ); 122 } 123 } 124 } 125 + _ => { 126 + tracing::error!("AIP auth found but OAuth backend is not AIP"); 127 + return contextual_error!( 128 + ctx.web_context, 129 + ctx.language, 130 + error_template, 131 + default_context, 132 + anyhow!("Authentication configuration mismatch"), 133 + StatusCode::INTERNAL_SERVER_ERROR 134 + ); 135 + } 136 + }, 137 Auth::Unauthenticated => { 138 // This should not happen due to the check above 139 return Ok(StatusCode::FORBIDDEN.into_response()); ··· 154 &dpop_auth, 155 &current_handle.pds, 156 delete_record_request, 157 + ) 158 + .await 159 + { 160 Ok(_) => { 161 tracing::info!("Successfully deleted event from PDS: {}", lookup_aturi); 162 }
+5 -5
src/http/handle_edit_event.rs
··· 15 use crate::{ 16 atproto::{ 17 lexicon::community::lexicon::calendar::event::{ 18 - Event as LexiconCommunityEvent, EventLink, EventLocation, Mode, NamedUri, Status, 19 - NSID as LexiconCommunityEventNSID, 20 }, 21 lexicon::community::lexicon::location::Address, 22 }, ··· 26 http::errors::{CommonError, WebError}, 27 http::event_form::BuildLocationForm, 28 http::event_form::{BuildEventContentState, BuildEventForm, BuildLinkForm, BuildStartsForm}, 29 - http::location_edit_status::{check_location_edit_status, LocationEditStatus}, 30 http::timezones::supported_timezones, 31 http::utils::url_from_aturi, 32 select_template, ··· 35 identity_profile::{handle_for_did, handle_for_handle}, 36 }, 37 }; 38 - use atproto_client::com::atproto::repo::{put_record, PutRecordRequest, PutRecordResponse}; 39 40 pub(crate) async fn handle_edit_event( 41 ctx: UserRequestContext, ··· 45 Path((handle_slug, event_rkey)): Path<(String, String)>, 46 Form(mut build_event_form): Form<BuildEventForm>, 47 ) -> Result<impl IntoResponse, WebError> { 48 - let current_handle = ctx.auth.require(&ctx.web_context.config, "/")?; 49 50 let default_context = template_context! { 51 current_handle,
··· 15 use crate::{ 16 atproto::{ 17 lexicon::community::lexicon::calendar::event::{ 18 + Event as LexiconCommunityEvent, EventLink, EventLocation, Mode, 19 + NSID as LexiconCommunityEventNSID, NamedUri, Status, 20 }, 21 lexicon::community::lexicon::location::Address, 22 }, ··· 26 http::errors::{CommonError, WebError}, 27 http::event_form::BuildLocationForm, 28 http::event_form::{BuildEventContentState, BuildEventForm, BuildLinkForm, BuildStartsForm}, 29 + http::location_edit_status::{LocationEditStatus, check_location_edit_status}, 30 http::timezones::supported_timezones, 31 http::utils::url_from_aturi, 32 select_template, ··· 35 identity_profile::{handle_for_did, handle_for_handle}, 36 }, 37 }; 38 + use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; 39 40 pub(crate) async fn handle_edit_event( 41 ctx: UserRequestContext, ··· 45 Path((handle_slug, event_rkey)): Path<(String, String)>, 46 Form(mut build_event_form): Form<BuildEventForm>, 47 ) -> Result<impl IntoResponse, WebError> { 48 + let current_handle = ctx.auth.require("/")?; 49 50 let default_context = template_context! { 51 current_handle,
+37 -23
src/http/handle_export_rsvps.rs
··· 2 3 use anyhow::Result; 4 use atproto_record::aturi::ATURI; 5 - use axum::{ 6 - extract::Path, 7 - response::IntoResponse, 8 - }; 9 - use http::{header::CONTENT_DISPOSITION, HeaderValue}; 10 11 use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; 12 use crate::http::context::UserRequestContext; ··· 16 /// Generate a CSV string from RSVP export data 17 fn generate_csv(rsvps: Vec<crate::storage::event::RsvpExportData>) -> String { 18 let mut csv = String::new(); 19 - 20 // Add CSV header 21 csv.push_str("event,rsvp,did,handle,status,created_at,email\n"); 22 - 23 // Add data rows 24 for rsvp in rsvps { 25 let created_at_str = match rsvp.created_at { 26 Some(dt) => dt.to_rfc3339(), 27 None => String::new(), 28 }; 29 - 30 let handle_str = rsvp.handle.unwrap_or_default(); 31 let email_str = rsvp.email.unwrap_or_default(); 32 - 33 // Escape CSV fields that might contain commas or quotes 34 let event = escape_csv_field(&rsvp.event_aturi); 35 let rsvp_aturi = escape_csv_field(&rsvp.rsvp_aturi); ··· 38 let status = escape_csv_field(&rsvp.status); 39 let created_at = escape_csv_field(&created_at_str); 40 let email = escape_csv_field(&email_str); 41 - 42 csv.push_str(&format!( 43 "{},{},{},{},{},{},{}\n", 44 event, rsvp_aturi, did, handle, status, created_at, email 45 )); 46 } 47 - 48 csv 49 } 50 ··· 63 // Extract the DID, collection, and record key from the AT-URI 64 // Format: at://did:plc:example/collection.nsid/recordkey 65 if let Ok(aturi) = ATURI::from_str(event_aturi) { 66 - return format!("{}-{}-{}.csv", aturi.authority, aturi.collection, aturi.record_key); 67 } 68 - 69 // Fallback if parsing fails 70 "rsvp-export.csv".to_string() 71 } ··· 75 Path((handle_slug, event_rkey)): Path<(String, String)>, 76 ) -> Result<impl IntoResponse, WebError> { 77 // Require authentication 78 - let current_handle = ctx.auth.require(&ctx.web_context.config, "/")?; 79 80 // Check if the current user is the event organizer 81 if handle_slug != current_handle.did { 82 - return Err(WebError::from(crate::http::errors::CommonError::NotAuthorized)); 83 } 84 85 let lookup_aturi = format!( ··· 90 // Get the event 91 let event_exists = event_exists(&ctx.web_context.pool, &lookup_aturi).await; 92 if let Err(_) = event_exists { 93 - return Err(WebError::from(crate::http::errors::CommonError::NotAuthorized)); 94 } 95 96 if !event_exists.unwrap() { 97 - return Err(WebError::from(crate::http::errors::CommonError::NotAuthorized)); 98 } 99 100 // Get all RSVPs for the event with detailed information ··· 113 114 let response = ( 115 [ 116 - (http::header::CONTENT_TYPE, HeaderValue::from_static("text/csv; charset=utf-8")), 117 - (CONTENT_DISPOSITION, HeaderValue::from_str(&content_disposition).unwrap()), 118 ], 119 csv_content, 120 ); ··· 138 139 #[test] 140 fn test_generate_filename() { 141 - let aturi = "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/community.lexicon.calendar.event/3ltbbcuygrc2c"; 142 - let expected = "did:plc:cbkjy5n7bk3ax2wplmtjofq2-community.lexicon.calendar.event-3ltbbcuygrc2c.csv"; 143 assert_eq!(generate_filename(aturi), expected); 144 } 145 ··· 169 170 let csv = generate_csv(rsvps); 171 let lines: Vec<&str> = csv.lines().collect(); 172 - 173 assert_eq!(lines.len(), 3); // Header + 2 data rows 174 assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at,email"); 175 assert!(lines[1].contains("user1@example.com")); 176 assert!(lines[2].contains("interested")); 177 } 178 - }
··· 2 3 use anyhow::Result; 4 use atproto_record::aturi::ATURI; 5 + use axum::{extract::Path, response::IntoResponse}; 6 + use http::{HeaderValue, header::CONTENT_DISPOSITION}; 7 8 use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; 9 use crate::http::context::UserRequestContext; ··· 13 /// Generate a CSV string from RSVP export data 14 fn generate_csv(rsvps: Vec<crate::storage::event::RsvpExportData>) -> String { 15 let mut csv = String::new(); 16 + 17 // Add CSV header 18 csv.push_str("event,rsvp,did,handle,status,created_at,email\n"); 19 + 20 // Add data rows 21 for rsvp in rsvps { 22 let created_at_str = match rsvp.created_at { 23 Some(dt) => dt.to_rfc3339(), 24 None => String::new(), 25 }; 26 + 27 let handle_str = rsvp.handle.unwrap_or_default(); 28 let email_str = rsvp.email.unwrap_or_default(); 29 + 30 // Escape CSV fields that might contain commas or quotes 31 let event = escape_csv_field(&rsvp.event_aturi); 32 let rsvp_aturi = escape_csv_field(&rsvp.rsvp_aturi); ··· 35 let status = escape_csv_field(&rsvp.status); 36 let created_at = escape_csv_field(&created_at_str); 37 let email = escape_csv_field(&email_str); 38 + 39 csv.push_str(&format!( 40 "{},{},{},{},{},{},{}\n", 41 event, rsvp_aturi, did, handle, status, created_at, email 42 )); 43 } 44 + 45 csv 46 } 47 ··· 60 // Extract the DID, collection, and record key from the AT-URI 61 // Format: at://did:plc:example/collection.nsid/recordkey 62 if let Ok(aturi) = ATURI::from_str(event_aturi) { 63 + return format!( 64 + "{}-{}-{}.csv", 65 + aturi.authority, aturi.collection, aturi.record_key 66 + ); 67 } 68 + 69 // Fallback if parsing fails 70 "rsvp-export.csv".to_string() 71 } ··· 75 Path((handle_slug, event_rkey)): Path<(String, String)>, 76 ) -> Result<impl IntoResponse, WebError> { 77 // Require authentication 78 + let current_handle = ctx.auth.require("/")?; 79 80 // Check if the current user is the event organizer 81 if handle_slug != current_handle.did { 82 + return Err(WebError::from( 83 + crate::http::errors::CommonError::NotAuthorized, 84 + )); 85 } 86 87 let lookup_aturi = format!( ··· 92 // Get the event 93 let event_exists = event_exists(&ctx.web_context.pool, &lookup_aturi).await; 94 if let Err(_) = event_exists { 95 + return Err(WebError::from( 96 + crate::http::errors::CommonError::NotAuthorized, 97 + )); 98 } 99 100 if !event_exists.unwrap() { 101 + return Err(WebError::from( 102 + crate::http::errors::CommonError::NotAuthorized, 103 + )); 104 } 105 106 // Get all RSVPs for the event with detailed information ··· 119 120 let response = ( 121 [ 122 + ( 123 + http::header::CONTENT_TYPE, 124 + HeaderValue::from_static("text/csv; charset=utf-8"), 125 + ), 126 + ( 127 + CONTENT_DISPOSITION, 128 + HeaderValue::from_str(&content_disposition).unwrap(), 129 + ), 130 ], 131 csv_content, 132 ); ··· 150 151 #[test] 152 fn test_generate_filename() { 153 + let aturi = 154 + "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/community.lexicon.calendar.event/3ltbbcuygrc2c"; 155 + let expected = 156 + "did:plc:cbkjy5n7bk3ax2wplmtjofq2-community.lexicon.calendar.event-3ltbbcuygrc2c.csv"; 157 assert_eq!(generate_filename(aturi), expected); 158 } 159 ··· 183 184 let csv = generate_csv(rsvps); 185 let lines: Vec<&str> = csv.lines().collect(); 186 + 187 assert_eq!(lines.len(), 3); // Header + 2 data rows 188 assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at,email"); 189 assert!(lines[1].contains("user1@example.com")); 190 assert!(lines[2].contains("interested")); 191 } 192 + }
+11 -11
src/http/handle_import.rs
··· 18 community::lexicon::calendar::{ 19 event::{Event as LexiconCommunityEvent, NSID as LEXICON_COMMUNITY_EVENT_NSID}, 20 rsvp::{ 21 - Rsvp as LexiconCommunityRsvp, RsvpStatus as LexiconCommunityRsvpStatus, 22 - NSID as LEXICON_COMMUNITY_RSVP_NSID, 23 }, 24 }, 25 events::smokesignal::calendar::{ 26 event::{Event as SmokeSignalEvent, NSID as SMOKESIGNAL_EVENT_NSID}, 27 rsvp::{ 28 - Rsvp as SmokeSignalRsvp, RsvpStatus as SmokeSignalRsvpStatus, 29 - NSID as SMOKESIGNAL_RSVP_NSID, 30 }, 31 }, 32 }, ··· 40 select_template, 41 storage::event::{event_insert_with_metadata, rsvp_insert_with_metadata}, 42 }; 43 - use atproto_client::com::atproto::repo::{list_records, ListRecordsParams}; 44 45 pub(crate) async fn handle_import( 46 State(web_context): State<WebContext>, ··· 49 HxRequest(hx_request): HxRequest, 50 HxBoosted(hx_boosted): HxBoosted, 51 ) -> Result<impl IntoResponse, WebError> { 52 - let current_handle = auth.require(&web_context.config, "/import")?; 53 54 let default_context = template_context! { 55 current_handle, ··· 185 error_template, 186 template_context! {}, 187 ImportError::FailedToListCommunityEvents(err.to_string()) 188 - ) 189 } 190 } 191 } ··· 268 error_template, 269 template_context! {}, 270 ImportError::FailedToListCommunityRSVPs(err.to_string()) 271 - ) 272 } 273 } 274 } ··· 336 error_template, 337 template_context! {}, 338 ImportError::FailedToListSmokesignalEvents(err.to_string()) 339 - ) 340 } 341 } 342 } ··· 415 error_template, 416 template_context! {}, 417 ImportError::FailedToListSmokesignalRSVPs(err.to_string()) 418 - ) 419 } 420 } 421 } ··· 426 error_template, 427 template_context! {}, 428 ImportError::UnsupportedCollectionType(collection.clone()) 429 - ) 430 } 431 }; 432
··· 18 community::lexicon::calendar::{ 19 event::{Event as LexiconCommunityEvent, NSID as LEXICON_COMMUNITY_EVENT_NSID}, 20 rsvp::{ 21 + NSID as LEXICON_COMMUNITY_RSVP_NSID, Rsvp as LexiconCommunityRsvp, 22 + RsvpStatus as LexiconCommunityRsvpStatus, 23 }, 24 }, 25 events::smokesignal::calendar::{ 26 event::{Event as SmokeSignalEvent, NSID as SMOKESIGNAL_EVENT_NSID}, 27 rsvp::{ 28 + NSID as SMOKESIGNAL_RSVP_NSID, Rsvp as SmokeSignalRsvp, 29 + RsvpStatus as SmokeSignalRsvpStatus, 30 }, 31 }, 32 }, ··· 40 select_template, 41 storage::event::{event_insert_with_metadata, rsvp_insert_with_metadata}, 42 }; 43 + use atproto_client::com::atproto::repo::{ListRecordsParams, list_records}; 44 45 pub(crate) async fn handle_import( 46 State(web_context): State<WebContext>, ··· 49 HxRequest(hx_request): HxRequest, 50 HxBoosted(hx_boosted): HxBoosted, 51 ) -> Result<impl IntoResponse, WebError> { 52 + let current_handle = auth.require("/import")?; 53 54 let default_context = template_context! { 55 current_handle, ··· 185 error_template, 186 template_context! {}, 187 ImportError::FailedToListCommunityEvents(err.to_string()) 188 + ); 189 } 190 } 191 } ··· 268 error_template, 269 template_context! {}, 270 ImportError::FailedToListCommunityRSVPs(err.to_string()) 271 + ); 272 } 273 } 274 } ··· 336 error_template, 337 template_context! {}, 338 ImportError::FailedToListSmokesignalEvents(err.to_string()) 339 + ); 340 } 341 } 342 } ··· 415 error_template, 416 template_context! {}, 417 ImportError::FailedToListSmokesignalRSVPs(err.to_string()) 418 + ); 419 } 420 } 421 } ··· 426 error_template, 427 template_context! {}, 428 ImportError::UnsupportedCollectionType(collection.clone()) 429 + ); 430 } 431 }; 432
+29 -18
src/http/handle_index.rs
··· 24 utils::url_from_aturi, 25 }, 26 select_template, 27 - storage::{ 28 - event::activity_list_recent, 29 - identity_profile::handles_by_did, 30 - }, 31 }; 32 33 #[derive(Deserialize, Serialize, PartialEq)] ··· 132 133 #[derive(Debug)] 134 enum GroupedActivity<'a> { 135 - Individual { 136 - did: String, 137 - activity: &'a crate::storage::event::model::ActivityItem 138 }, 139 - GroupedRsvp { 140 - dids: Vec<String>, 141 first_activity: &'a crate::storage::event::model::ActivityItem, 142 rsvp_statuses: Vec<String>, 143 - count: usize 144 }, 145 } 146 ··· 155 let mut group_dids = vec![activity.did.clone()]; 156 let mut group_statuses = vec![activity.rsvp_status.clone().unwrap_or_default()]; 157 let mut j = i + 1; 158 - 159 while j < activity_list.len() { 160 let next_activity = activity_list[j]; 161 if next_activity.activity_type == "rsvp" ··· 207 let organizer_did = extract_did_from_aturi(&activity.event_aturi); 208 needed_dids.insert(organizer_did); 209 } 210 - GroupedActivity::GroupedRsvp { dids, first_activity, .. } => { 211 for did in dids { 212 needed_dids.insert(did.clone()); 213 } ··· 232 .map(|profile| profile.handle.clone()) 233 .unwrap_or_else(|| did.clone()); 234 235 - let event_url = url_from_aturi(&web_context.config.external_base, &activity.event_aturi) 236 - .unwrap_or_else(|_| format!("/event/{}", activity.event_aturi)); 237 238 let event_organizer_did = extract_did_from_aturi(&activity.event_aturi); 239 let event_organizer_handle = handles ··· 254 updated_at: activity.updated_at.to_rfc3339(), 255 })); 256 } 257 - GroupedActivity::GroupedRsvp { dids, first_activity, rsvp_statuses, count } => { 258 let group_handles: Vec<String> = dids 259 .iter() 260 .map(|did| { ··· 265 }) 266 .collect(); 267 268 - let event_url = url_from_aturi(&web_context.config.external_base, &first_activity.event_aturi) 269 - .unwrap_or_else(|_| format!("/event/{}", first_activity.event_aturi)); 270 271 let event_organizer_did = extract_did_from_aturi(&first_activity.event_aturi); 272 let event_organizer_handle = handles ··· 301 302 let params: Vec<(&str, &str)> = vec![("tab", &tab_name)]; 303 304 - let pagination_view = PaginationView::new(page_size, activity_displays.len() as i64, page, params); 305 306 if activity_displays.len() > page_size as usize { 307 activity_displays.truncate(page_size as usize);
··· 24 utils::url_from_aturi, 25 }, 26 select_template, 27 + storage::{event::activity_list_recent, identity_profile::handles_by_did}, 28 }; 29 30 #[derive(Deserialize, Serialize, PartialEq)] ··· 129 130 #[derive(Debug)] 131 enum GroupedActivity<'a> { 132 + Individual { 133 + did: String, 134 + activity: &'a crate::storage::event::model::ActivityItem, 135 }, 136 + GroupedRsvp { 137 + dids: Vec<String>, 138 first_activity: &'a crate::storage::event::model::ActivityItem, 139 rsvp_statuses: Vec<String>, 140 + count: usize, 141 }, 142 } 143 ··· 152 let mut group_dids = vec![activity.did.clone()]; 153 let mut group_statuses = vec![activity.rsvp_status.clone().unwrap_or_default()]; 154 let mut j = i + 1; 155 + 156 while j < activity_list.len() { 157 let next_activity = activity_list[j]; 158 if next_activity.activity_type == "rsvp" ··· 204 let organizer_did = extract_did_from_aturi(&activity.event_aturi); 205 needed_dids.insert(organizer_did); 206 } 207 + GroupedActivity::GroupedRsvp { 208 + dids, 209 + first_activity, 210 + .. 211 + } => { 212 for did in dids { 213 needed_dids.insert(did.clone()); 214 } ··· 233 .map(|profile| profile.handle.clone()) 234 .unwrap_or_else(|| did.clone()); 235 236 + let event_url = 237 + url_from_aturi(&web_context.config.external_base, &activity.event_aturi) 238 + .unwrap_or_else(|_| format!("/event/{}", activity.event_aturi)); 239 240 let event_organizer_did = extract_did_from_aturi(&activity.event_aturi); 241 let event_organizer_handle = handles ··· 256 updated_at: activity.updated_at.to_rfc3339(), 257 })); 258 } 259 + GroupedActivity::GroupedRsvp { 260 + dids, 261 + first_activity, 262 + rsvp_statuses, 263 + count, 264 + } => { 265 let group_handles: Vec<String> = dids 266 .iter() 267 .map(|did| { ··· 272 }) 273 .collect(); 274 275 + let event_url = url_from_aturi( 276 + &web_context.config.external_base, 277 + &first_activity.event_aturi, 278 + ) 279 + .unwrap_or_else(|_| format!("/event/{}", first_activity.event_aturi)); 280 281 let event_organizer_did = extract_did_from_aturi(&first_activity.event_aturi); 282 let event_organizer_handle = handles ··· 311 312 let params: Vec<(&str, &str)> = vec![("tab", &tab_name)]; 313 314 + let pagination_view = 315 + PaginationView::new(page_size, activity_displays.len() as i64, page, params); 316 317 if activity_displays.len() > page_size as usize { 318 activity_displays.truncate(page_size as usize);
+7 -7
src/http/handle_migrate_event.rs
··· 17 use crate::{ 18 atproto::lexicon::{ 19 community::lexicon::calendar::event::{ 20 - Event as CommunityEvent, EventLink, EventLocation as CommunityLocation, Mode, Status, 21 - NSID as COMMUNITY_NSID, 22 }, 23 community::lexicon::location, 24 events::smokesignal::calendar::event::{ 25 - Event as SmokeSignalEvent, Location as SmokeSignalLocation, PlaceLocation, 26 - NSID as SMOKESIGNAL_NSID, 27 }, 28 }, 29 contextual_error, ··· 37 identity_profile::{handle_for_did, handle_for_handle, model::IdentityProfile}, 38 }, 39 }; 40 - use atproto_client::com::atproto::repo::{put_record, PutRecordRequest, PutRecordResponse}; 41 42 pub(crate) async fn handle_migrate_event( 43 State(web_context): State<WebContext>, ··· 47 HxRequest(hx_request): HxRequest, 48 Path((handle_slug, event_rkey)): Path<(String, String)>, 49 ) -> Result<impl IntoResponse, WebError> { 50 - let current_handle = auth.require(&web_context.config, "/")?; 51 52 // Configure templates 53 let default_context = template_context! { ··· 262 error_template, 263 default_context, 264 MigrateEventError::DestinationExists, 265 - StatusCode::CONFLICT 266 ); 267 } 268
··· 17 use crate::{ 18 atproto::lexicon::{ 19 community::lexicon::calendar::event::{ 20 + Event as CommunityEvent, EventLink, EventLocation as CommunityLocation, Mode, 21 + NSID as COMMUNITY_NSID, Status, 22 }, 23 community::lexicon::location, 24 events::smokesignal::calendar::event::{ 25 + Event as SmokeSignalEvent, Location as SmokeSignalLocation, NSID as SMOKESIGNAL_NSID, 26 + PlaceLocation, 27 }, 28 }, 29 contextual_error, ··· 37 identity_profile::{handle_for_did, handle_for_handle, model::IdentityProfile}, 38 }, 39 }; 40 + use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; 41 42 pub(crate) async fn handle_migrate_event( 43 State(web_context): State<WebContext>, ··· 47 HxRequest(hx_request): HxRequest, 48 Path((handle_slug, event_rkey)): Path<(String, String)>, 49 ) -> Result<impl IntoResponse, WebError> { 50 + let current_handle = auth.require("/")?; 51 52 // Configure templates 53 let default_context = template_context! { ··· 262 error_template, 263 default_context, 264 MigrateEventError::DestinationExists, 265 + StatusCode::OK 266 ); 267 } 268
+3 -6
src/http/handle_migrate_rsvp.rs
··· 17 use crate::{ 18 atproto::lexicon::{ 19 com::atproto::repo::StrongRef, 20 - community::lexicon::calendar::rsvp::{Rsvp, RsvpStatus, NSID as RSVP_COLLECTION}, 21 events::smokesignal::calendar::event::NSID as EVENT_COLLECTION, 22 }, 23 contextual_error, ··· 33 identity_profile::{handle_for_did, handle_for_handle, model::IdentityProfile}, 34 }, 35 }; 36 - use atproto_client::com::atproto::repo::{put_record, PutRecordRequest, PutRecordResponse}; 37 38 /// Migrates a user's RSVP from a legacy event to a standard event format. 39 /// ··· 55 Path((handle_slug, event_rkey)): Path<(String, String)>, 56 ) -> Result<impl IntoResponse, WebError> { 57 // Require user to be logged in 58 - let current_handle = auth.require( 59 - &web_context.config, 60 - "/{handle_slug}/{event_rkey}/migrate-rsvp", 61 - )?; 62 63 let default_context = template_context! { 64 language => language.to_string(),
··· 17 use crate::{ 18 atproto::lexicon::{ 19 com::atproto::repo::StrongRef, 20 + community::lexicon::calendar::rsvp::{NSID as RSVP_COLLECTION, Rsvp, RsvpStatus}, 21 events::smokesignal::calendar::event::NSID as EVENT_COLLECTION, 22 }, 23 contextual_error, ··· 33 identity_profile::{handle_for_did, handle_for_handle, model::IdentityProfile}, 34 }, 35 }; 36 + use atproto_client::com::atproto::repo::{PutRecordRequest, PutRecordResponse, put_record}; 37 38 /// Migrates a user's RSVP from a legacy event to a standard event format. 39 /// ··· 55 Path((handle_slug, event_rkey)): Path<(String, String)>, 56 ) -> Result<impl IntoResponse, WebError> { 57 // Require user to be logged in 58 + let current_handle = auth.require("/{handle_slug}/{event_rkey}/migrate-rsvp")?; 59 60 let default_context = template_context! { 61 language => language.to_string(),
+18 -6
src/http/handle_oauth_aip_callback.rs
··· 1 use std::collections::HashMap; 2 3 - use crate::{config::OAuthBackendConfig, contextual_error, select_template, storage::identity_profile::{handle_for_did, identity_profile_set_email}}; 4 use anyhow::{Context, Result, anyhow}; 5 use axum::{ 6 extract::State, ··· 126 127 let identity_profile = handle_for_did(&web_context.pool, &oauth_request.did).await?; 128 129 - let maybe_email = get_email_from_userinfo(&web_context.http_client, hostname, &identity_profile.did, &token_response.access_token).await; 130 let maybe_email = match maybe_email { 131 Ok(value) => value, 132 Err(err) => { ··· 138 // Write the email address to the database if it already isn't in the database. 139 // Only set if the identity_profile's email field is None (not even an empty string) 140 if identity_profile.email.is_none() { 141 - if let Err(err) = identity_profile_set_email(&web_context.pool, &oauth_request.did, Some(&email)).await { 142 tracing::error!(error = ?err, "Failed to set email from OAuth userinfo"); 143 } 144 } ··· 163 let updated_jar = jar.add(cookie); 164 165 // Retrieve destination from OAuth request before deleting it 166 - let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 167 - web_context.pool.clone() 168 - ); 169 let destination = match postgres_storage.get_destination(&callback_state).await { 170 Ok(Some(dest)) => dest, 171 Ok(None) => "/".to_string(),
··· 1 use std::collections::HashMap; 2 3 + use crate::{ 4 + config::OAuthBackendConfig, 5 + contextual_error, select_template, 6 + storage::identity_profile::{handle_for_did, identity_profile_set_email}, 7 + }; 8 use anyhow::{Context, Result, anyhow}; 9 use axum::{ 10 extract::State, ··· 130 131 let identity_profile = handle_for_did(&web_context.pool, &oauth_request.did).await?; 132 133 + let maybe_email = get_email_from_userinfo( 134 + &web_context.http_client, 135 + hostname, 136 + &identity_profile.did, 137 + &token_response.access_token, 138 + ) 139 + .await; 140 let maybe_email = match maybe_email { 141 Ok(value) => value, 142 Err(err) => { ··· 148 // Write the email address to the database if it already isn't in the database. 149 // Only set if the identity_profile's email field is None (not even an empty string) 150 if identity_profile.email.is_none() { 151 + if let Err(err) = 152 + identity_profile_set_email(&web_context.pool, &oauth_request.did, Some(&email)) 153 + .await 154 + { 155 tracing::error!(error = ?err, "Failed to set email from OAuth userinfo"); 156 } 157 } ··· 176 let updated_jar = jar.add(cookie); 177 178 // Retrieve destination from OAuth request before deleting it 179 + let postgres_storage = 180 + crate::storage::atproto::PostgresOAuthRequestStorage::new(web_context.pool.clone()); 181 let destination = match postgres_storage.get_destination(&callback_state).await { 182 Ok(Some(dest)) => dest, 183 Ok(None) => "/".to_string(),
+4 -4
src/http/handle_oauth_aip_login.rs
··· 1 - use anyhow::{anyhow, Result}; 2 use atproto_identity::resolve::IdentityResolver; 3 use atproto_oauth::pkce::generate; 4 use atproto_oauth::workflow::OAuthRequestState as AipOAuthRequestState; 5 use atproto_oauth_aip::{ 6 resources::oauth_authorization_server, 7 - workflow::{oauth_init, OAuthClient}, 8 }; 9 use axum::response::Redirect; 10 use axum::{extract::State, response::IntoResponse}; ··· 13 use axum_template::RenderHtml; 14 use http::StatusCode; 15 use minijinja::context as template_context; 16 - use rand::{distributions::Alphanumeric, Rng}; 17 use serde::Deserialize; 18 19 use crate::{ ··· 248 if dest != "/" { 249 // Create a direct instance to access the set_destination method 250 let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 251 - web_context.pool.clone() 252 ); 253 if let Err(err) = postgres_storage.set_destination(&state, dest).await { 254 tracing::error!(?err, "set_destination");
··· 1 + use anyhow::{Result, anyhow}; 2 use atproto_identity::resolve::IdentityResolver; 3 use atproto_oauth::pkce::generate; 4 use atproto_oauth::workflow::OAuthRequestState as AipOAuthRequestState; 5 use atproto_oauth_aip::{ 6 resources::oauth_authorization_server, 7 + workflow::{OAuthClient, oauth_init}, 8 }; 9 use axum::response::Redirect; 10 use axum::{extract::State, response::IntoResponse}; ··· 13 use axum_template::RenderHtml; 14 use http::StatusCode; 15 use minijinja::context as template_context; 16 + use rand::{Rng, distributions::Alphanumeric}; 17 use serde::Deserialize; 18 19 use crate::{ ··· 248 if dest != "/" { 249 // Create a direct instance to access the set_destination method 250 let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 251 + web_context.pool.clone(), 252 ); 253 if let Err(err) = postgres_storage.set_destination(&state, dest).await { 254 tracing::error!(?err, "set_destination");
+6 -7
src/http/handle_oauth_callback.rs
··· 1 - use anyhow::{anyhow, Result}; 2 use atproto_identity::{axum::state::KeyProviderExtractor, key::identify_key}; 3 - use atproto_oauth::workflow::{oauth_complete, OAuthClient}; 4 use axum::{ 5 extract::State, 6 response::{IntoResponse, Redirect}, 7 }; 8 use axum_extra::extract::{ 9 - cookie::{Cookie, SameSite}, 10 Form, PrivateCookieJar, 11 }; 12 use minijinja::context as template_context; 13 use serde::{Deserialize, Serialize}; ··· 17 use super::{ 18 context::WebContext, 19 errors::{LoginError, WebError}, 20 - middleware_auth::{WebSession, AUTH_COOKIE_NAME}, 21 middleware_i18n::Language, 22 }; 23 ··· 164 let token_response = token_response.unwrap(); 165 166 // Retrieve destination from OAuth request before deleting it 167 - let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 168 - web_context.pool.clone() 169 - ); 170 let destination = match postgres_storage.get_destination(&callback_state).await { 171 Ok(Some(dest)) => dest, 172 Ok(None) => "/".to_string(),
··· 1 + use anyhow::{Result, anyhow}; 2 use atproto_identity::{axum::state::KeyProviderExtractor, key::identify_key}; 3 + use atproto_oauth::workflow::{OAuthClient, oauth_complete}; 4 use axum::{ 5 extract::State, 6 response::{IntoResponse, Redirect}, 7 }; 8 use axum_extra::extract::{ 9 Form, PrivateCookieJar, 10 + cookie::{Cookie, SameSite}, 11 }; 12 use minijinja::context as template_context; 13 use serde::{Deserialize, Serialize}; ··· 17 use super::{ 18 context::WebContext, 19 errors::{LoginError, WebError}, 20 + middleware_auth::{AUTH_COOKIE_NAME, WebSession}, 21 middleware_i18n::Language, 22 }; 23 ··· 164 let token_response = token_response.unwrap(); 165 166 // Retrieve destination from OAuth request before deleting it 167 + let postgres_storage = 168 + crate::storage::atproto::PostgresOAuthRequestStorage::new(web_context.pool.clone()); 169 let destination = match postgres_storage.get_destination(&callback_state).await { 170 Ok(Some(dest)) => dest, 171 Ok(None) => "/".to_string(),
+8 -5
src/http/handle_oauth_login.rs
··· 1 use anyhow::Result; 2 use atproto_identity::{ 3 - key::{generate_key, identify_key, KeyType}, 4 resolve::IdentityResolver, 5 }; 6 use atproto_oauth::{ 7 pkce::generate, 8 resources::pds_resources, 9 - workflow::{oauth_init, OAuthClient, OAuthRequest, OAuthRequestState}, 10 }; 11 use axum::{extract::State, response::IntoResponse}; 12 use axum_extra::extract::{Cached, Form, Query}; ··· 14 use axum_template::RenderHtml; 15 use http::StatusCode; 16 use minijinja::context as template_context; 17 - use rand::{distributions::Alphanumeric, Rng}; 18 use serde::Deserialize; 19 20 use crate::{ ··· 277 if dest != "/" { 278 // Create a direct instance to access the set_destination method 279 let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 280 - web_context.pool.clone() 281 ); 282 - if let Err(err) = postgres_storage.set_destination(&oauth_request_state.state, dest).await { 283 tracing::error!(?err, "set_destination"); 284 // Don't fail the login flow if we can't store the destination 285 }
··· 1 use anyhow::Result; 2 use atproto_identity::{ 3 + key::{KeyType, generate_key, identify_key}, 4 resolve::IdentityResolver, 5 }; 6 use atproto_oauth::{ 7 pkce::generate, 8 resources::pds_resources, 9 + workflow::{OAuthClient, OAuthRequest, OAuthRequestState, oauth_init}, 10 }; 11 use axum::{extract::State, response::IntoResponse}; 12 use axum_extra::extract::{Cached, Form, Query}; ··· 14 use axum_template::RenderHtml; 15 use http::StatusCode; 16 use minijinja::context as template_context; 17 + use rand::{Rng, distributions::Alphanumeric}; 18 use serde::Deserialize; 19 20 use crate::{ ··· 277 if dest != "/" { 278 // Create a direct instance to access the set_destination method 279 let postgres_storage = crate::storage::atproto::PostgresOAuthRequestStorage::new( 280 + web_context.pool.clone(), 281 ); 282 + if let Err(err) = postgres_storage 283 + .set_destination(&oauth_request_state.state, dest) 284 + .await 285 + { 286 tracing::error!(?err, "set_destination"); 287 // Don't fail the login flow if we can't store the destination 288 }
+33 -12
src/http/handle_profile.rs
··· 200 && next_activity.event_aturi == activity.event_aturi 201 { 202 group_dids.push(next_activity.did.clone()); 203 - group_statuses.push(next_activity.rsvp_status.clone().unwrap_or_default()); 204 j += 1; 205 } else { 206 break; ··· 241 let organizer_did = extract_did_from_aturi(&activity.event_aturi); 242 needed_dids.insert(organizer_did); 243 } 244 - GroupedActivity::GroupedRsvp { dids, first_activity, .. } => { 245 for did in dids { 246 needed_dids.insert(did.clone()); 247 } ··· 252 } 253 254 // Get handles for needed DIDs 255 - let handles = handles_by_did(&ctx.web_context.pool, needed_dids.into_iter().collect()).await?; 256 257 // Create ActivityDisplay objects 258 let mut activity_displays: Vec<ActivityDisplay> = Vec::new(); ··· 265 .map(|profile| profile.handle.clone()) 266 .unwrap_or_else(|| did.clone()); 267 268 - let event_url = url_from_aturi(&ctx.web_context.config.external_base, &activity.event_aturi) 269 - .unwrap_or_else(|_| format!("/event/{}", activity.event_aturi)); 270 271 let event_organizer_did = extract_did_from_aturi(&activity.event_aturi); 272 let event_organizer_handle = handles ··· 287 updated_at: activity.updated_at.to_rfc3339(), 288 })); 289 } 290 - GroupedActivity::GroupedRsvp { dids, first_activity, rsvp_statuses, count } => { 291 let group_handles: Vec<String> = dids 292 .iter() 293 .map(|did| { ··· 298 }) 299 .collect(); 300 301 - let event_url = url_from_aturi(&ctx.web_context.config.external_base, &first_activity.event_aturi) 302 - .unwrap_or_else(|_| format!("/event/{}", first_activity.event_aturi)); 303 304 - let event_organizer_did = extract_did_from_aturi(&first_activity.event_aturi); 305 let event_organizer_handle = handles 306 .get(&event_organizer_did) 307 .map(|profile| profile.handle.clone()) 308 .unwrap_or_else(|| event_organizer_did.clone()); 309 310 let first_status = &rsvp_statuses[0]; 311 - let all_same_status = rsvp_statuses.iter().all(|status| status == first_status); 312 let display_status = if all_same_status { 313 Some(first_status.clone()) 314 } else { ··· 394 } 395 }; 396 397 - let organizer_handlers = hydrate_event_organizers(&ctx.web_context.pool, &events).await?; 398 399 let mut events = events 400 .iter() ··· 410 .collect::<Vec<EventView>>(); 411 412 if let Err(err) = 413 - super::event_view::hydrate_event_rsvp_counts(&ctx.web_context.pool, &mut events).await 414 { 415 tracing::warn!("Failed to hydrate event counts: {}", err); 416 }
··· 200 && next_activity.event_aturi == activity.event_aturi 201 { 202 group_dids.push(next_activity.did.clone()); 203 + group_statuses 204 + .push(next_activity.rsvp_status.clone().unwrap_or_default()); 205 j += 1; 206 } else { 207 break; ··· 242 let organizer_did = extract_did_from_aturi(&activity.event_aturi); 243 needed_dids.insert(organizer_did); 244 } 245 + GroupedActivity::GroupedRsvp { 246 + dids, 247 + first_activity, 248 + .. 249 + } => { 250 for did in dids { 251 needed_dids.insert(did.clone()); 252 } ··· 257 } 258 259 // Get handles for needed DIDs 260 + let handles = 261 + handles_by_did(&ctx.web_context.pool, needed_dids.into_iter().collect()).await?; 262 263 // Create ActivityDisplay objects 264 let mut activity_displays: Vec<ActivityDisplay> = Vec::new(); ··· 271 .map(|profile| profile.handle.clone()) 272 .unwrap_or_else(|| did.clone()); 273 274 + let event_url = url_from_aturi( 275 + &ctx.web_context.config.external_base, 276 + &activity.event_aturi, 277 + ) 278 + .unwrap_or_else(|_| format!("/event/{}", activity.event_aturi)); 279 280 let event_organizer_did = extract_did_from_aturi(&activity.event_aturi); 281 let event_organizer_handle = handles ··· 296 updated_at: activity.updated_at.to_rfc3339(), 297 })); 298 } 299 + GroupedActivity::GroupedRsvp { 300 + dids, 301 + first_activity, 302 + rsvp_statuses, 303 + count, 304 + } => { 305 let group_handles: Vec<String> = dids 306 .iter() 307 .map(|did| { ··· 312 }) 313 .collect(); 314 315 + let event_url = url_from_aturi( 316 + &ctx.web_context.config.external_base, 317 + &first_activity.event_aturi, 318 + ) 319 + .unwrap_or_else(|_| format!("/event/{}", first_activity.event_aturi)); 320 321 + let event_organizer_did = 322 + extract_did_from_aturi(&first_activity.event_aturi); 323 let event_organizer_handle = handles 324 .get(&event_organizer_did) 325 .map(|profile| profile.handle.clone()) 326 .unwrap_or_else(|| event_organizer_did.clone()); 327 328 let first_status = &rsvp_statuses[0]; 329 + let all_same_status = 330 + rsvp_statuses.iter().all(|status| status == first_status); 331 let display_status = if all_same_status { 332 Some(first_status.clone()) 333 } else { ··· 413 } 414 }; 415 416 + let organizer_handlers = 417 + hydrate_event_organizers(&ctx.web_context.pool, &events).await?; 418 419 let mut events = events 420 .iter() ··· 430 .collect::<Vec<EventView>>(); 431 432 if let Err(err) = 433 + super::event_view::hydrate_event_rsvp_counts(&ctx.web_context.pool, &mut events) 434 + .await 435 { 436 tracing::warn!("Failed to hydrate event counts: {}", err); 437 }
+2 -2
src/http/handle_set_language.rs
··· 4 response::{IntoResponse, Redirect}, 5 }; 6 use axum_extra::extract::{ 7 - cookie::{Cookie, CookieJar, SameSite}, 8 Cached, Form, 9 }; 10 use minijinja::context as template_context; 11 use serde::Deserialize; 12 use std::{borrow::Cow, str::FromStr}; 13 use unic_langid::LanguageIdentifier; 14 15 - use crate::storage::identity_profile::{handle_update_field, HandleField}; 16 17 use super::{ 18 context::WebContext, errors::WebError, middleware_auth::Auth, middleware_i18n::COOKIE_LANG,
··· 4 response::{IntoResponse, Redirect}, 5 }; 6 use axum_extra::extract::{ 7 Cached, Form, 8 + cookie::{Cookie, CookieJar, SameSite}, 9 }; 10 use minijinja::context as template_context; 11 use serde::Deserialize; 12 use std::{borrow::Cow, str::FromStr}; 13 use unic_langid::LanguageIdentifier; 14 15 + use crate::storage::identity_profile::{HandleField, handle_update_field}; 16 17 use super::{ 18 context::WebContext, errors::WebError, middleware_auth::Auth, middleware_i18n::COOKIE_LANG,
+9 -6
src/http/handle_settings.rs
··· 16 timezones::supported_timezones, 17 }, 18 select_template, 19 - storage::identity_profile::{handle_for_did, handle_update_field, identity_profile_set_email, HandleField}, 20 }; 21 22 #[derive(Deserialize, Clone, Debug)] ··· 41 HxBoosted(hx_boosted): HxBoosted, 42 ) -> Result<impl IntoResponse, WebError> { 43 // Require authentication 44 - let current_handle = auth.require(&web_context.config, "/settings")?; 45 46 let default_context = template_context! { 47 current_handle => current_handle.clone(), ··· 251 }; 252 253 let error_template = select_template!(false, true, language); 254 - let render_template = format!("settings.{}.email.html", language.to_string().to_lowercase()); 255 256 // Update the email in the database 257 let update_result = match email_form.email { ··· 261 Some(email) => { 262 identity_profile_set_email(&web_context.pool, &current_handle.did, Some(&email)).await 263 } 264 - None => { 265 - identity_profile_set_email(&web_context.pool, &current_handle.did, Some("")).await 266 - } 267 }; 268 269 if let Err(err) = update_result {
··· 16 timezones::supported_timezones, 17 }, 18 select_template, 19 + storage::identity_profile::{ 20 + HandleField, handle_for_did, handle_update_field, identity_profile_set_email, 21 + }, 22 }; 23 24 #[derive(Deserialize, Clone, Debug)] ··· 43 HxBoosted(hx_boosted): HxBoosted, 44 ) -> Result<impl IntoResponse, WebError> { 45 // Require authentication 46 + let current_handle = auth.require("/settings")?; 47 48 let default_context = template_context! { 49 current_handle => current_handle.clone(), ··· 253 }; 254 255 let error_template = select_template!(false, true, language); 256 + let render_template = format!( 257 + "settings.{}.email.html", 258 + language.to_string().to_lowercase() 259 + ); 260 261 // Update the email in the database 262 let update_result = match email_form.email { ··· 266 Some(email) => { 267 identity_profile_set_email(&web_context.pool, &current_handle.did, Some(&email)).await 268 } 269 + None => identity_profile_set_email(&web_context.pool, &current_handle.did, Some("")).await, 270 }; 271 272 if let Err(err) = update_result {
+17 -5
src/http/handle_view_event.rs
··· 17 use crate::http::context::UserRequestContext; 18 use crate::http::errors::ViewEventError; 19 use crate::http::errors::WebError; 20 - use crate::http::event_view::hydrate_event_rsvp_counts; 21 use crate::http::event_view::EventView; 22 use crate::http::pagination::Pagination; 23 use crate::http::tab_selector::TabSelector; 24 use crate::http::utils::url_from_aturi; 25 use crate::select_template; 26 use crate::storage::event::count_event_rsvps; 27 use crate::storage::event::event_exists; 28 use crate::storage::event::event_get; ··· 32 use crate::storage::identity_profile::handle_for_did; 33 use crate::storage::identity_profile::handle_for_handle; 34 use crate::storage::identity_profile::model::IdentityProfile; 35 - use crate::storage::StoragePool; 36 37 #[derive(Debug, Deserialize, Serialize, PartialEq)] 38 pub enum RSVPTab { ··· 127 128 let profile = profile.unwrap(); 129 130 - let identity_has_email = ctx.current_handle.as_ref().is_some_and(|handle| handle.email.as_ref().is_some_and(|value| !value.is_empty())); 131 132 // We'll use TimeZoneSelector to implement the time zone selection logic 133 // The timezone selection will happen after we fetch the event ··· 284 let event_url = url_from_aturi(&ctx.web_context.config.external_base, &event.aturi)?; 285 286 // Create login URL with destination parameter for this event 287 - let login_url = format!("/oauth/login?destination={}", urlencoding::encode(&format!("/{}/{}", handle_slug, event_rkey))); 288 289 // Add Edit button link if the user is the event creator 290 let can_edit = ctx ··· 307 // Only fetch RSVP data for standard (non-legacy) events 308 // Get user's RSVP status and email sharing preference if logged in 309 let (user_rsvp, user_email_shared) = if let Some(current_entity) = &ctx.current_handle { 310 - match get_user_rsvp_with_email_shared(&ctx.web_context.pool, &lookup_aturi, &current_entity.did).await { 311 Ok(Some((status, email_shared))) => (Some(status), email_shared), 312 Ok(None) => (None, false), 313 Err(err) => {
··· 17 use crate::http::context::UserRequestContext; 18 use crate::http::errors::ViewEventError; 19 use crate::http::errors::WebError; 20 use crate::http::event_view::EventView; 21 + use crate::http::event_view::hydrate_event_rsvp_counts; 22 use crate::http::pagination::Pagination; 23 use crate::http::tab_selector::TabSelector; 24 use crate::http::utils::url_from_aturi; 25 use crate::select_template; 26 + use crate::storage::StoragePool; 27 use crate::storage::event::count_event_rsvps; 28 use crate::storage::event::event_exists; 29 use crate::storage::event::event_get; ··· 33 use crate::storage::identity_profile::handle_for_did; 34 use crate::storage::identity_profile::handle_for_handle; 35 use crate::storage::identity_profile::model::IdentityProfile; 36 37 #[derive(Debug, Deserialize, Serialize, PartialEq)] 38 pub enum RSVPTab { ··· 127 128 let profile = profile.unwrap(); 129 130 + let identity_has_email = ctx 131 + .current_handle 132 + .as_ref() 133 + .is_some_and(|handle| handle.email.as_ref().is_some_and(|value| !value.is_empty())); 134 135 // We'll use TimeZoneSelector to implement the time zone selection logic 136 // The timezone selection will happen after we fetch the event ··· 287 let event_url = url_from_aturi(&ctx.web_context.config.external_base, &event.aturi)?; 288 289 // Create login URL with destination parameter for this event 290 + let login_url = format!( 291 + "/oauth/login?destination={}", 292 + urlencoding::encode(&format!("/{}/{}", handle_slug, event_rkey)) 293 + ); 294 295 // Add Edit button link if the user is the event creator 296 let can_edit = ctx ··· 313 // Only fetch RSVP data for standard (non-legacy) events 314 // Get user's RSVP status and email sharing preference if logged in 315 let (user_rsvp, user_email_shared) = if let Some(current_entity) = &ctx.current_handle { 316 + match get_user_rsvp_with_email_shared( 317 + &ctx.web_context.pool, 318 + &lookup_aturi, 319 + &current_entity.did, 320 + ) 321 + .await 322 + { 323 Ok(Some((status, email_shared))) => (Some(status), email_shared), 324 Ok(None) => (None, false), 325 Err(err) => {
+2 -6
src/http/middleware_auth.rs
··· 1 use anyhow::Result; 2 - use atproto_oauth::jwt::{mint, Claims, Header, JoseClaims}; 3 use axum::{ 4 extract::{FromRef, FromRequestParts}, 5 http::request::Parts, ··· 10 use tracing::{debug, instrument, trace}; 11 12 use crate::{ 13 - config::Config, 14 - http::context::WebContext, 15 - http::errors::{AuthMiddlewareError, WebSessionError}, 16 storage::identity_profile::model::IdentityProfile, 17 }; 18 ··· 73 /// 74 /// This creates a redirect URL with a signed token containing the destination, 75 /// which the login handler can verify and redirect back to after successful authentication. 76 - #[instrument(level = "debug", skip(self, config), err)] 77 pub(crate) fn require( 78 &self, 79 - config: &crate::config::Config, 80 location: &str, 81 ) -> Result<IdentityProfile, MiddlewareAuthError> { 82 match self {
··· 1 use anyhow::Result; 2 use axum::{ 3 extract::{FromRef, FromRequestParts}, 4 http::request::Parts, ··· 9 use tracing::{debug, instrument, trace}; 10 11 use crate::{ 12 + config::Config, http::context::WebContext, http::errors::WebSessionError, 13 storage::identity_profile::model::IdentityProfile, 14 }; 15 ··· 70 /// 71 /// This creates a redirect URL with a signed token containing the destination, 72 /// which the login handler can verify and redirect back to after successful authentication. 73 + #[instrument(level = "debug", skip(self), err)] 74 pub(crate) fn require( 75 &self, 76 location: &str, 77 ) -> Result<IdentityProfile, MiddlewareAuthError> { 78 match self {
+1 -1
src/http/middleware_i18n.rs
··· 4 http::request::Parts, 5 response::Response, 6 }; 7 - use axum_extra::extract::{cookie::CookieJar, Cached}; 8 use std::{cmp::Ordering, str::FromStr}; 9 use tracing::{debug, instrument, trace}; 10 use unic_langid::LanguageIdentifier;
··· 4 http::request::Parts, 5 response::Response, 6 }; 7 + use axum_extra::extract::{Cached, cookie::CookieJar}; 8 use std::{cmp::Ordering, str::FromStr}; 9 use tracing::{debug, instrument, trace}; 10 use unic_langid::LanguageIdentifier;
+9 -1
src/http/pagination.rs
··· 38 (page, page_size) 39 } 40 41 - pub(crate) fn clamped_with_options(&self, page_default: i64, page_min: i64, page_max: i64, page_size_default: i64, page_size_min: i64, page_size_max: i64) -> (i64, i64) { 42 let page = self.page.unwrap_or(page_default).clamp(page_min, page_max); 43 let page_size = self 44 .page_size
··· 38 (page, page_size) 39 } 40 41 + pub(crate) fn clamped_with_options( 42 + &self, 43 + page_default: i64, 44 + page_min: i64, 45 + page_max: i64, 46 + page_size_default: i64, 47 + page_size_min: i64, 48 + page_size_max: i64, 49 + ) -> (i64, i64) { 50 let page = self.page.unwrap_or(page_default).clamp(page_min, page_max); 51 let page_size = self 52 .page_size
+1 -1
src/http/rsvp_form.rs
··· 3 use crate::{ 4 errors::expand_error, 5 i18n::Locales, 6 - storage::{event::event_get_cid, StoragePool}, 7 }; 8 9 #[allow(dead_code)]
··· 3 use crate::{ 4 errors::expand_error, 5 i18n::Locales, 6 + storage::{StoragePool, event::event_get_cid}, 7 }; 8 9 #[allow(dead_code)]
+13 -5
src/http/server.rs
··· 1 use std::time::Duration; 2 3 use axum::{ 4 http::HeaderValue, 5 routing::{get, post}, 6 - Router, 7 }; 8 use axum_htmx::AutoVaryLayer; 9 use http::{ 10 - header::{ACCEPT, ACCEPT_LANGUAGE}, 11 Method, 12 }; 13 use tower_http::trace::TraceLayer; 14 use tower_http::{classify::ServerErrorsFailureClass, timeout::TimeoutLayer}; ··· 48 }, 49 handle_profile::handle_profile_view, 50 handle_set_language::handle_set_language, 51 - handle_settings::{handle_email_update, handle_language_update, handle_settings, handle_timezone_update}, 52 handle_view_event::handle_view_event, 53 handle_view_feed::handle_view_feed, 54 handle_view_rsvp::handle_view_rsvp, ··· 128 .route("/event/links", post(handle_link_at_builder)) 129 .route("/{handle_slug}/{event_rkey}/edit", get(handle_edit_event)) 130 .route("/{handle_slug}/{event_rkey}/edit", post(handle_edit_event)) 131 - .route("/{handle_slug}/{event_rkey}/export-rsvps", get(handle_export_rsvps)) 132 - .route("/{handle_slug}/{event_rkey}/delete", post(handle_delete_event)) 133 .route( 134 "/{handle_slug}/{event_rkey}/migrate", 135 get(handle_migrate_event),
··· 1 use std::time::Duration; 2 3 use axum::{ 4 + Router, 5 http::HeaderValue, 6 routing::{get, post}, 7 }; 8 use axum_htmx::AutoVaryLayer; 9 use http::{ 10 Method, 11 + header::{ACCEPT, ACCEPT_LANGUAGE}, 12 }; 13 use tower_http::trace::TraceLayer; 14 use tower_http::{classify::ServerErrorsFailureClass, timeout::TimeoutLayer}; ··· 48 }, 49 handle_profile::handle_profile_view, 50 handle_set_language::handle_set_language, 51 + handle_settings::{ 52 + handle_email_update, handle_language_update, handle_settings, handle_timezone_update, 53 + }, 54 handle_view_event::handle_view_event, 55 handle_view_feed::handle_view_feed, 56 handle_view_rsvp::handle_view_rsvp, ··· 130 .route("/event/links", post(handle_link_at_builder)) 131 .route("/{handle_slug}/{event_rkey}/edit", get(handle_edit_event)) 132 .route("/{handle_slug}/{event_rkey}/edit", post(handle_edit_event)) 133 + .route( 134 + "/{handle_slug}/{event_rkey}/export-rsvps", 135 + get(handle_export_rsvps), 136 + ) 137 + .route( 138 + "/{handle_slug}/{event_rkey}/delete", 139 + post(handle_delete_event), 140 + ) 141 .route( 142 "/{handle_slug}/{event_rkey}/migrate", 143 get(handle_migrate_event),
+1 -1
src/http/templates.rs
··· 21 pub mod reload_env { 22 use std::path::PathBuf; 23 24 - use minijinja::{path_loader, Environment}; 25 use minijinja_autoreload::AutoReloader; 26 27 pub fn build_env(http_external: &str, version: &str) -> AutoReloader {
··· 21 pub mod reload_env { 22 use std::path::PathBuf; 23 24 + use minijinja::{Environment, path_loader}; 25 use minijinja_autoreload::AutoReloader; 26 27 pub fn build_env(http_external: &str, version: &str) -> AutoReloader {
+1 -1
src/http/timezones.rs
··· 1 - use anyhow::{anyhow, Result}; 2 use chrono::{DateTime, NaiveDateTime, Utc}; 3 use itertools::Itertools; 4
··· 1 + use anyhow::{Result, anyhow}; 2 use chrono::{DateTime, NaiveDateTime, Utc}; 3 use itertools::Itertools; 4
+3 -3
src/i18n.rs
··· 1 use anyhow::Result; 2 - use fluent::{bundle::FluentBundle, FluentArgs, FluentResource}; 3 use std::collections::HashMap; 4 use unic_langid::LanguageIdentifier; 5 ··· 105 use rust_embed::Embed; 106 use unic_langid::LanguageIdentifier; 107 108 - use crate::i18n::{errors::I18nError, Locales}; 109 110 #[derive(Embed)] 111 #[folder = "i18n/"] ··· 135 use std::path::PathBuf; 136 use unic_langid::LanguageIdentifier; 137 138 - use crate::i18n::{errors::I18nError, Locales}; 139 140 pub fn populate_locale( 141 supported_locales: &Vec<LanguageIdentifier>,
··· 1 use anyhow::Result; 2 + use fluent::{FluentArgs, FluentResource, bundle::FluentBundle}; 3 use std::collections::HashMap; 4 use unic_langid::LanguageIdentifier; 5 ··· 105 use rust_embed::Embed; 106 use unic_langid::LanguageIdentifier; 107 108 + use crate::i18n::{Locales, errors::I18nError}; 109 110 #[derive(Embed)] 111 #[folder = "i18n/"] ··· 135 use std::path::PathBuf; 136 use unic_langid::LanguageIdentifier; 137 138 + use crate::i18n::{Locales, errors::I18nError}; 139 140 pub fn populate_locale( 141 supported_locales: &Vec<LanguageIdentifier>,
+8 -11
src/storage/atproto.rs
··· 6 use sqlx::FromRow; 7 use std::sync::Arc; 8 9 - use crate::storage::{errors::StorageError, StoragePool}; 10 11 /// Database row representation of OAuthRequest 12 #[derive(FromRow)] ··· 131 destination: &str, 132 ) -> Result<(), StorageError> { 133 if oauth_state.trim().is_empty() { 134 - return Err(StorageError::UnableToExecuteQuery( 135 - sqlx::Error::Protocol("OAuth state cannot be empty".to_string()), 136 - )); 137 } 138 139 let mut tx = self ··· 161 } 162 163 /// Get the destination for an OAuth request 164 - pub async fn get_destination( 165 - &self, 166 - oauth_state: &str, 167 - ) -> Result<Option<String>, StorageError> { 168 if oauth_state.trim().is_empty() { 169 - return Err(StorageError::UnableToExecuteQuery( 170 - sqlx::Error::Protocol("OAuth state cannot be empty".to_string()), 171 - )); 172 } 173 174 let mut tx = self
··· 6 use sqlx::FromRow; 7 use std::sync::Arc; 8 9 + use crate::storage::{StoragePool, errors::StorageError}; 10 11 /// Database row representation of OAuthRequest 12 #[derive(FromRow)] ··· 131 destination: &str, 132 ) -> Result<(), StorageError> { 133 if oauth_state.trim().is_empty() { 134 + return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol( 135 + "OAuth state cannot be empty".to_string(), 136 + ))); 137 } 138 139 let mut tx = self ··· 161 } 162 163 /// Get the destination for an OAuth request 164 + pub async fn get_destination(&self, oauth_state: &str) -> Result<Option<String>, StorageError> { 165 if oauth_state.trim().is_empty() { 166 + return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol( 167 + "OAuth state cannot be empty".to_string(), 168 + ))); 169 } 170 171 let mut tx = self
+1 -1
src/storage/denylist.rs
··· 6 7 use self::model::DenylistEntry; 8 9 - use crate::storage::{errors::StorageError, StoragePool}; 10 11 pub(crate) mod model { 12 use chrono::{DateTime, Utc};
··· 6 7 use self::model::DenylistEntry; 8 9 + use crate::storage::{StoragePool, errors::StorageError}; 10 11 pub(crate) mod model { 12 use chrono::{DateTime, Utc};
+21 -10
src/storage/event.rs
··· 11 Rsvp as RsvpLexicon, RsvpStatus as RsvpStatusLexicon, 12 }; 13 14 - use super::errors::StorageError; 15 use super::StoragePool; 16 use model::{ActivityItem, Event, EventWithRole, Rsvp}; 17 18 pub mod model { ··· 1341 ORDER BY r.updated_at ASC 1342 "#; 1343 1344 - let rsvps = sqlx::query_as::<_, (String, String, String, Option<String>, String, Option<chrono::DateTime<chrono::Utc>>, Option<String>)>(query) 1345 - .bind(event_aturi) 1346 - .fetch_all(tx.as_mut()) 1347 - .await 1348 - .map_err(StorageError::UnableToExecuteQuery)?; 1349 1350 tx.commit() 1351 .await ··· 1353 1354 let export_data: Vec<RsvpExportData> = rsvps 1355 .into_iter() 1356 - .map(|(event_aturi, rsvp_aturi, did, handle, status, created_at, email)| { 1357 - RsvpExportData { 1358 event_aturi, 1359 rsvp_aturi, 1360 did, ··· 1362 status, 1363 created_at, 1364 email, 1365 - } 1366 - }) 1367 .collect(); 1368 1369 Ok(export_data)
··· 11 Rsvp as RsvpLexicon, RsvpStatus as RsvpStatusLexicon, 12 }; 13 14 use super::StoragePool; 15 + use super::errors::StorageError; 16 use model::{ActivityItem, Event, EventWithRole, Rsvp}; 17 18 pub mod model { ··· 1341 ORDER BY r.updated_at ASC 1342 "#; 1343 1344 + let rsvps = sqlx::query_as::< 1345 + _, 1346 + ( 1347 + String, 1348 + String, 1349 + String, 1350 + Option<String>, 1351 + String, 1352 + Option<chrono::DateTime<chrono::Utc>>, 1353 + Option<String>, 1354 + ), 1355 + >(query) 1356 + .bind(event_aturi) 1357 + .fetch_all(tx.as_mut()) 1358 + .await 1359 + .map_err(StorageError::UnableToExecuteQuery)?; 1360 1361 tx.commit() 1362 .await ··· 1364 1365 let export_data: Vec<RsvpExportData> = rsvps 1366 .into_iter() 1367 + .map( 1368 + |(event_aturi, rsvp_aturi, did, handle, status, created_at, email)| RsvpExportData { 1369 event_aturi, 1370 rsvp_aturi, 1371 did, ··· 1373 status, 1374 created_at, 1375 email, 1376 + }, 1377 + ) 1378 .collect(); 1379 1380 Ok(export_data)
+1 -1
src/storage/identity_profile.rs
··· 4 use cityhasher::HashMap; 5 use sqlx::{Postgres, QueryBuilder}; 6 7 use crate::storage::denylist::denylist_add_or_update; 8 use crate::storage::errors::StorageError; 9 - use crate::storage::StoragePool; 10 use model::IdentityProfile; 11 12 pub mod model {
··· 4 use cityhasher::HashMap; 5 use sqlx::{Postgres, QueryBuilder}; 6 7 + use crate::storage::StoragePool; 8 use crate::storage::denylist::denylist_add_or_update; 9 use crate::storage::errors::StorageError; 10 use model::IdentityProfile; 11 12 pub mod model {
+1 -1
src/storage/oauth.rs
··· 2 3 use chrono::{DateTime, Utc}; 4 5 - use crate::storage::{errors::StorageError, identity_profile::model::IdentityProfile, StoragePool}; 6 use model::OAuthSession; 7 8 pub async fn oauth_session_update(
··· 2 3 use chrono::{DateTime, Utc}; 4 5 + use crate::storage::{StoragePool, errors::StorageError, identity_profile::model::IdentityProfile}; 6 use model::OAuthSession; 7 8 pub async fn oauth_session_update(
+1 -1
src/task_identity_refresh.rs
··· 2 use atproto_identity::{resolve::IdentityResolver, storage::DidDocumentStorage}; 3 use chrono::Duration; 4 use sqlx::FromRow; 5 - use tokio::time::{sleep, Instant}; 6 use tokio_util::sync::CancellationToken; 7 8 use crate::storage::StoragePool;
··· 2 use atproto_identity::{resolve::IdentityResolver, storage::DidDocumentStorage}; 3 use chrono::Duration; 4 use sqlx::FromRow; 5 + use tokio::time::{Instant, sleep}; 6 use tokio_util::sync::CancellationToken; 7 8 use crate::storage::StoragePool;
+1 -1
src/task_oauth_requests_cleanup.rs
··· 1 use anyhow::Result; 2 use chrono::Duration; 3 - use tokio::time::{sleep, Instant}; 4 use tokio_util::sync::CancellationToken; 5 6 use crate::storage::StoragePool;
··· 1 use anyhow::Result; 2 use chrono::Duration; 3 + use tokio::time::{Instant, sleep}; 4 use tokio_util::sync::CancellationToken; 5 6 use crate::storage::StoragePool;
+5 -5
src/task_refresh_tokens.rs
··· 1 use anyhow::Result; 2 use atproto_identity::key::identify_key; 3 - use atproto_oauth::workflow::{oauth_refresh, OAuthClient}; 4 use chrono::{Duration, Utc}; 5 - use deadpool_redis::redis::{pipe, AsyncCommands}; 6 use std::borrow::Cow; 7 - use tokio::time::{sleep, Instant}; 8 use tokio_util::sync::CancellationToken; 9 10 use crate::{ 11 config::SigningKeys, 12 refresh_tokens_errors::RefreshError, 13 storage::{ 14 - cache::{build_worker_queue, OAUTH_REFRESH_HEARTBEATS, OAUTH_REFRESH_QUEUE}, 15 - oauth::{oauth_session_delete, oauth_session_update, web_session_lookup}, 16 CachePool, StoragePool, 17 }, 18 }; 19
··· 1 use anyhow::Result; 2 use atproto_identity::key::identify_key; 3 + use atproto_oauth::workflow::{OAuthClient, oauth_refresh}; 4 use chrono::{Duration, Utc}; 5 + use deadpool_redis::redis::{AsyncCommands, pipe}; 6 use std::borrow::Cow; 7 + use tokio::time::{Instant, sleep}; 8 use tokio_util::sync::CancellationToken; 9 10 use crate::{ 11 config::SigningKeys, 12 refresh_tokens_errors::RefreshError, 13 storage::{ 14 CachePool, StoragePool, 15 + cache::{OAUTH_REFRESH_HEARTBEATS, OAUTH_REFRESH_QUEUE, build_worker_queue}, 16 + oauth::{oauth_session_delete, oauth_session_update, web_session_lookup}, 17 }, 18 }; 19