···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")]
00134 AipConfigurationIncomplete,
135136 /// 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,
137138 /// Error when oauth_backend has an invalid value.
···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'.")]
0015 InvalidRsvpStatus(String),
1617 /// 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),
1819 /// Error when a user is not authorized to migrate an RSVP.
+1-1
src/http/errors/mod.rs
···21pub(crate) use event_view_errors::EventViewError;
22pub(crate) use import_error::ImportError;
23pub(crate) use login_error::LoginError;
24-pub(crate) use middleware_errors::{AuthMiddlewareError, WebSessionError};
25pub(crate) use migrate_event_error::MigrateEventError;
26pub(crate) use migrate_rsvp_error::MigrateRsvpError;
27pub(crate) use rsvp_error::RSVPError;
···21pub(crate) use event_view_errors::EventViewError;
22pub(crate) use import_error::ImportError;
23pub(crate) use login_error::LoginError;
24+pub(crate) use middleware_errors::WebSessionError;
25pub(crate) use migrate_event_error::MigrateEventError;
26pub(crate) use migrate_rsvp_error::MigrateRsvpError;
27pub(crate) use rsvp_error::RSVPError;
···17use 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};
3738/// 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- )?;
6263 let default_context = template_context! {
64 language => language.to_string(),
···17use 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};
3738/// 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")?;
0005960 let default_context = template_context! {
61 language => language.to_string(),
+18-6
src/http/handle_oauth_aip_callback.rs
···1use std::collections::HashMap;
23-use crate::{config::OAuthBackendConfig, contextual_error, select_template, storage::identity_profile::{handle_for_did, identity_profile_set_email}};
00004use anyhow::{Context, Result, anyhow};
5use axum::{
6 extract::State,
···126127 let identity_profile = handle_for_did(&web_context.pool, &oauth_request.did).await?;
128129- let maybe_email = get_email_from_userinfo(&web_context.http_client, hostname, &identity_profile.did, &token_response.access_token).await;
000000130 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 {
000142 tracing::error!(error = ?err, "Failed to set email from OAuth userinfo");
143 }
144 }
···163 let updated_jar = jar.add(cookie);
164165 // 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(),
···1use std::collections::HashMap;
23+use crate::{
4+ config::OAuthBackendConfig,
5+ contextual_error, select_template,
6+ storage::identity_profile::{handle_for_did, identity_profile_set_email},
7+};
8use anyhow::{Context, Result, anyhow};
9use axum::{
10 extract::State,
···130131 let identity_profile = handle_for_did(&web_context.pool, &oauth_request.did).await?;
132133+ 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);
177178 // Retrieve destination from OAuth request before deleting it
179+ let postgres_storage =
180+ crate::storage::atproto::PostgresOAuthRequestStorage::new(web_context.pool.clone());
0181 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};
2use atproto_identity::resolve::IdentityResolver;
3use atproto_oauth::pkce::generate;
4use atproto_oauth::workflow::OAuthRequestState as AipOAuthRequestState;
5use atproto_oauth_aip::{
6 resources::oauth_authorization_server,
7- workflow::{oauth_init, OAuthClient},
8};
9use axum::response::Redirect;
10use axum::{extract::State, response::IntoResponse};
···13use axum_template::RenderHtml;
14use http::StatusCode;
15use minijinja::context as template_context;
16-use rand::{distributions::Alphanumeric, Rng};
17use serde::Deserialize;
1819use 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};
2use atproto_identity::resolve::IdentityResolver;
3use atproto_oauth::pkce::generate;
4use atproto_oauth::workflow::OAuthRequestState as AipOAuthRequestState;
5use atproto_oauth_aip::{
6 resources::oauth_authorization_server,
7+ workflow::{OAuthClient, oauth_init},
8};
9use axum::response::Redirect;
10use axum::{extract::State, response::IntoResponse};
···13use axum_template::RenderHtml;
14use http::StatusCode;
15use minijinja::context as template_context;
16+use rand::{Rng, distributions::Alphanumeric};
17use serde::Deserialize;
1819use 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");
···16 timezones::supported_timezones,
17 },
18 select_template,
19- storage::identity_profile::{handle_for_did, handle_update_field, identity_profile_set_email, HandleField},
0020};
2122#[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")?;
4546 let default_context = template_context! {
47 current_handle => current_handle.clone(),
···251 };
252253 let error_template = select_template!(false, true, language);
254- let render_template = format!("settings.{}.email.html", language.to_string().to_lowercase());
000255256 // 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, ¤t_handle.did, Some(&email)).await
263 }
264- None => {
265- identity_profile_set_email(&web_context.pool, ¤t_handle.did, Some("")).await
266- }
267 };
268269 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};
2324#[derive(Deserialize, Clone, Debug)]
···43 HxBoosted(hx_boosted): HxBoosted,
44) -> Result<impl IntoResponse, WebError> {
45 // Require authentication
46+ let current_handle = auth.require("/settings")?;
4748 let default_context = template_context! {
49 current_handle => current_handle.clone(),
···253 };
254255 let error_template = select_template!(false, true, language);
256+ let render_template = format!(
257+ "settings.{}.email.html",
258+ language.to_string().to_lowercase()
259+ );
260261 // 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, ¤t_handle.did, Some(&email)).await
268 }
269+ None => identity_profile_set_email(&web_context.pool, ¤t_handle.did, Some("")).await,
00270 };
271272 if let Err(err) = update_result {
+17-5
src/http/handle_view_event.rs
···17use crate::http::context::UserRequestContext;
18use crate::http::errors::ViewEventError;
19use crate::http::errors::WebError;
20-use crate::http::event_view::hydrate_event_rsvp_counts;
21use crate::http::event_view::EventView;
022use crate::http::pagination::Pagination;
23use crate::http::tab_selector::TabSelector;
24use crate::http::utils::url_from_aturi;
25use crate::select_template;
026use crate::storage::event::count_event_rsvps;
27use crate::storage::event::event_exists;
28use crate::storage::event::event_get;
···32use crate::storage::identity_profile::handle_for_did;
33use crate::storage::identity_profile::handle_for_handle;
34use crate::storage::identity_profile::model::IdentityProfile;
35-use crate::storage::StoragePool;
3637#[derive(Debug, Deserialize, Serialize, PartialEq)]
38pub enum RSVPTab {
···127128 let profile = profile.unwrap();
129130- let identity_has_email = ctx.current_handle.as_ref().is_some_and(|handle| handle.email.as_ref().is_some_and(|value| !value.is_empty()));
000131132 // 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)?;
285286 // Create login URL with destination parameter for this event
287- let login_url = format!("/oauth/login?destination={}", urlencoding::encode(&format!("/{}/{}", handle_slug, event_rkey)));
000288289 // 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, ¤t_entity.did).await {
000000311 Ok(Some((status, email_shared))) => (Some(status), email_shared),
312 Ok(None) => (None, false),
313 Err(err) => {
···17use crate::http::context::UserRequestContext;
18use crate::http::errors::ViewEventError;
19use crate::http::errors::WebError;
020use crate::http::event_view::EventView;
21+use crate::http::event_view::hydrate_event_rsvp_counts;
22use crate::http::pagination::Pagination;
23use crate::http::tab_selector::TabSelector;
24use crate::http::utils::url_from_aturi;
25use crate::select_template;
26+use crate::storage::StoragePool;
27use crate::storage::event::count_event_rsvps;
28use crate::storage::event::event_exists;
29use crate::storage::event::event_get;
···33use crate::storage::identity_profile::handle_for_did;
34use crate::storage::identity_profile::handle_for_handle;
35use crate::storage::identity_profile::model::IdentityProfile;
03637#[derive(Debug, Deserialize, Serialize, PartialEq)]
38pub enum RSVPTab {
···127128 let profile = profile.unwrap();
129130+ 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()));
134135 // 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)?;
288289 // 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+ );
294295 // 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+ ¤t_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
···1use anyhow::Result;
2-use atproto_oauth::jwt::{mint, Claims, Header, JoseClaims};
3use axum::{
4 extract::{FromRef, FromRequestParts},
5 http::request::Parts,
···10use tracing::{debug, instrument, trace};
1112use 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 {
···1use anyhow::Result;
02use axum::{
3 extract::{FromRef, FromRequestParts},
4 http::request::Parts,
···9use tracing::{debug, instrument, trace};
1011use crate::{
12+ config::Config, http::context::WebContext, http::errors::WebSessionError,
0013 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,
076 location: &str,
77 ) -> Result<IdentityProfile, MiddlewareAuthError> {
78 match self {
···1use anyhow::Result;
2-use fluent::{bundle::FluentBundle, FluentArgs, FluentResource};
3use std::collections::HashMap;
4use unic_langid::LanguageIdentifier;
5···105 use rust_embed::Embed;
106 use unic_langid::LanguageIdentifier;
107108- use crate::i18n::{errors::I18nError, Locales};
109110 #[derive(Embed)]
111 #[folder = "i18n/"]
···135 use std::path::PathBuf;
136 use unic_langid::LanguageIdentifier;
137138- use crate::i18n::{errors::I18nError, Locales};
139140 pub fn populate_locale(
141 supported_locales: &Vec<LanguageIdentifier>,
···1use anyhow::Result;
2+use fluent::{FluentArgs, FluentResource, bundle::FluentBundle};
3use std::collections::HashMap;
4use unic_langid::LanguageIdentifier;
5···105 use rust_embed::Embed;
106 use unic_langid::LanguageIdentifier;
107108+ use crate::i18n::{Locales, errors::I18nError};
109110 #[derive(Embed)]
111 #[folder = "i18n/"]
···135 use std::path::PathBuf;
136 use unic_langid::LanguageIdentifier;
137138+ use crate::i18n::{Locales, errors::I18nError};
139140 pub fn populate_locale(
141 supported_locales: &Vec<LanguageIdentifier>,
+8-11
src/storage/atproto.rs
···6use sqlx::FromRow;
7use std::sync::Arc;
89-use crate::storage::{errors::StorageError, StoragePool};
1011/// 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 }
138139 let mut tx = self
···161 }
162163 /// 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 }
173174 let mut tx = self
···6use sqlx::FromRow;
7use std::sync::Arc;
89+use crate::storage::{StoragePool, errors::StorageError};
1011/// 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 }
138139 let mut tx = self
···161 }
162163 /// Get the destination for an OAuth request
164+ pub async fn get_destination(&self, oauth_state: &str) -> Result<Option<String>, StorageError> {
000165 if oauth_state.trim().is_empty() {
166+ return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol(
167+ "OAuth state cannot be empty".to_string(),
168+ )));
169 }
170171 let mut tx = self
+1-1
src/storage/denylist.rs
···67use self::model::DenylistEntry;
89-use crate::storage::{errors::StorageError, StoragePool};
1011pub(crate) mod model {
12 use chrono::{DateTime, Utc};
···67use self::model::DenylistEntry;
89+use crate::storage::{StoragePool, errors::StorageError};
1011pub(crate) mod model {
12 use chrono::{DateTime, Utc};