diff --git a/Cargo.toml b/Cargo.toml index 41a5208..663f0f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,78 +34,63 @@ atproto-oauth-axum = { git = "https://tangled.org/@smokesignal.events/atproto-id atproto-record = { git = "https://tangled.org/@smokesignal.events/atproto-identity-rs" } atproto-xrpcs = { git = "https://tangled.org/@smokesignal.events/atproto-identity-rs" } +ammonia = "4" anyhow = "1.0" async-trait = "0.1" -axum-extra = { version = "0.10", features = ["cookie", "cookie-private", "form", "query", "cookie-key-expansion", "typed-header", "typed-routing"] } axum = { version = "0.8", features = ["http2", "macros", "multipart"] } +axum-extra = { version = "0.12", features = ["cookie", "cookie-private", "form", "query", "cookie-key-expansion", "typed-header", "typed-routing", "cached"] } +axum-htmx = { version = "0.8", features = ["auto-vary"] } axum-template = { version = "3.0", features = ["minijinja-autoreload", "minijinja"] } base64 = "0.22" -chrono-tz = { version = "0.10", features = ["serde"] } +bloomfilter = "3" +bytes = "1.10" chrono = { version = "0.4", default-features = false, features = ["std", "alloc", "now", "serde"] } -futures-util = { version = "0.3", features = ["sink"] } -headers = "0.4" +chrono-tz = { version = "0.10", features = ["serde"] } +cookie = "0.18" +crockford = "1.2" +css-inline = "0.18" +deadpool-redis = {version = "0.22", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } +elliptic-curve = { version = "0.13", features = ["pem", "pkcs8", "sec1", "std", "alloc", "digest", "ecdh", "jwk", "bits"] } +fluent = "0.17" +fluent-bundle = "0.16" +fluent-syntax = "0.12" +hex = "0.4.3" +hmac = "0.12" http = "1.1" -serde_json = { version = "1.0", features = ["alloc"] } -serde = { version = "1.0", features = ["alloc", "derive"] } -thiserror = "2.0" -tokio-util = { version = "0.7", features = ["net", "rt", "tracing"] } -tokio = { version = "1.41", features = ["bytes", "macros", "net", "rt", "rt-multi-thread", "signal", "sync"] } -tower-http = { version = "0.6", features = ["cors", "fs", "timeout", "trace", "tracing"] } -tower = { version = "0.5", features = ["limit", "timeout", "tokio", "tracing"] } -tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono", "json"] } -tracing = { version = "0.1", features = ["async-await", "log", "valuable"] } -reqwest = { version = "0.12", features = ["json", "zstd", "rustls-tls"] } -reqwest-chain = "1" -reqwest-middleware = { version = "0.4", features = ["http2", "json", "multipart"] } -reqwest-retry = "0.7" -regex = "1" -duration-str = "0.11" +icalendar = "0.17" +image = "0.25" +intl-memoizer = "0.5" +itertools = "0.14" +lettre = {version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "rustls-tls", "tokio1-rustls-tls"]} +lru = "0.16" +metrohash = "1.0" +minify-html-onepass = "0.18" minijinja = { version = "2.7", features = ["builtins", "json", "urlencode"] } minijinja-autoreload = { version = "2.7", optional = true } minijinja-embed = { version = "2.7", optional = true } -axum-htmx = { version = "0.7", features = ["auto-vary"] } -hickory-resolver = { version = "0.24", features = ["dns-over-https-rustls", "dns-over-rustls", "rustls", "tokio-rustls"] } -rand = "0.8" -async-stream = "0.3" -tokio-stream = "0.1" -url = "2.5" -cookie = "0.18" -ammonia = "4" +minio = { version = "0.3", optional = true } +once_cell = "1.19" +opensearch = { version = "2.3", default-features = false, features = ["rustls-tls"] } +ordermap = "1" +p256 = { version = "0.13", features = ["ecdsa-core", "jwk", "serde", "ecdh"] } +rand = "0.9" +regex = "1" +reqwest = { version = "0.12", features = ["json", "zstd", "rustls-tls"] } rust-embed = "8.5" +serde = { version = "1.0", features = ["alloc", "derive"] } +serde_json = { version = "1.0", features = ["alloc"] } +sha2 = "0.10" sqlx = { version = "0.8", default-features = false, features = ["derive", "macros", "migrate", "json", "runtime-tokio", "postgres", "chrono", "tls-rustls-ring-native-roots"] } -elliptic-curve = { version = "0.13.8", features = ["pem", "pkcs8", "sec1", "std", "alloc", "digest", "ecdh", "jwk", "bits"] } -p256 = { version = "0.13.2", features = ["ecdsa-core", "jwk", "serde", "ecdh"] } -ordermap = "0.5" -urlencoding = "2.1" +thiserror = "2.0" +tokio = { version = "1.41", features = ["bytes", "macros", "net", "rt", "rt-multi-thread", "signal", "sync"] } +tokio-util = { version = "0.7", features = ["net", "rt", "tracing"] } +tower-http = { version = "0.6", features = ["cors", "fs", "timeout", "trace", "tracing"] } +tracing = { version = "0.1", features = ["async-await", "log", "valuable"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono", "json"] } ulid = { version = "1.1", features = ["serde"] } unic-langid = "0.9" -intl-memoizer = "0.5" -fluent = "0.16" -fluent-bundle = "0.15" -fluent-syntax = "0.11" -sha2 = "0.10.8" -redis = { version = "0.28", features = ["tokio-comp", "tokio-rustls-comp"] } -itertools = "0.14.0" -deadpool = "0.12.2" -deadpool-redis = {version = "0.20.0", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } -crockford = "1.2.1" -tokio-websockets = { version = "0.11.3", features = ["client", "rand", "ring", "rustls-native-roots"] } -zstd = "0.13.3" -once_cell = "1.19" -parking_lot = "0.12" -metrohash = "1.0.7" -minio = { version = "0.3", optional = true } -bytes = "1.10.1" -bloomfilter = "1.0.15" -image = "0.25" -opensearch = { version = "2.3.0", default-features = false, features = ["rustls-tls"] } -icalendar = "0.16" -lru = "0.16.0" -lettre = {version = "0.11.19", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "rustls-tls", "tokio1-rustls-tls"]} -hmac = "0.12.1" -hex = "0.4.3" -css-inline = "0.18.0" -minify-html-onepass = "0.18.1" +url = "2.5" +urlencoding = "2.1" [profile.release] opt-level = 3 diff --git a/migrations/20251105000002_private_event_content.sql b/migrations/20251105000002_private_event_content.sql new file mode 100644 index 0000000..34e76a8 --- /dev/null +++ b/migrations/20251105000002_private_event_content.sql @@ -0,0 +1,11 @@ +-- Create private_event_content table for conditional event content +CREATE TABLE private_event_content ( + aturi TEXT PRIMARY KEY, + display_criteria JSONB NOT NULL DEFAULT '[]', + content TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create index for faster lookups +CREATE INDEX idx_private_event_content_aturi ON private_event_content(aturi); diff --git a/src/bin/crypto.rs b/src/bin/crypto.rs index 1dee7de..ee19f55 100644 --- a/src/bin/crypto.rs +++ b/src/bin/crypto.rs @@ -4,7 +4,7 @@ use base64::{Engine as _, engine::general_purpose}; use rand::RngCore; fn main() { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); env::args().for_each(|arg| { if arg.as_str() == "key" { diff --git a/src/http/errors/create_rsvp_errors.rs b/src/http/errors/create_rsvp_errors.rs index f0fefba..9428a06 100644 --- a/src/http/errors/create_rsvp_errors.rs +++ b/src/http/errors/create_rsvp_errors.rs @@ -17,6 +17,8 @@ pub(crate) enum CreateRsvpError { /// /// This error occurs when a user attempts to RSVP to an event that requires /// confirmed email addresses, but the user has not confirmed their email address. - #[error("error-smokesignal-create-rsvp-2 Email confirmation required: This event requires a confirmed email address to RSVP. Please confirm your email address in your settings.")] + #[error( + "error-smokesignal-create-rsvp-2 Email confirmation required: This event requires a confirmed email address to RSVP. Please confirm your email address in your settings." + )] EmailConfirmationRequired, } diff --git a/src/http/event_form.rs b/src/http/event_form.rs index 155d739..0c130bc 100644 --- a/src/http/event_form.rs +++ b/src/http/event_form.rs @@ -158,6 +158,11 @@ pub(crate) struct BuildEventForm { pub send_notifications: Option, pub require_confirmed_email: Option, + + pub private_content: Option, + pub private_content_criteria_going_confirmed: Option, + pub private_content_criteria_going: Option, + pub private_content_criteria_interested: Option, } impl From for BuildLocationForm { diff --git a/src/http/handle_create_rsvp.rs b/src/http/handle_create_rsvp.rs index f128b60..ce39068 100644 --- a/src/http/handle_create_rsvp.rs +++ b/src/http/handle_create_rsvp.rs @@ -118,7 +118,8 @@ pub(crate) async fn handle_create_rsvp( if !found_errors { // Check if the event requires confirmed email let event_aturi = build_rsvp_form.subject_aturi.as_ref().unwrap(); - let event = crate::storage::event::event_get(&web_context.pool, event_aturi).await?; + let event = + crate::storage::event::event_get(&web_context.pool, event_aturi).await?; if event.require_confirmed_email { // Check if user has confirmed email diff --git a/src/http/handle_edit_event.rs b/src/http/handle_edit_event.rs index b9db8fb..b70cb46 100644 --- a/src/http/handle_edit_event.rs +++ b/src/http/handle_edit_event.rs @@ -198,6 +198,31 @@ pub(crate) async fn handle_edit_event( build_event_form.description = Some(description.clone()); build_event_form.require_confirmed_email = Some(event.require_confirmed_email); + // Load private event content if it exists + if let Ok(Some(private_content)) = + crate::storage::private_event_content::private_event_content_get( + &ctx.web_context.pool, + &lookup_aturi, + ) + .await + { + build_event_form.private_content = Some(private_content.content); + + // Set checkboxes based on criteria + for criterion in private_content.display_criteria.0.iter() { + match criterion.as_str() { + "going_confirmed" => { + build_event_form.private_content_criteria_going_confirmed = Some(true) + } + "going" => build_event_form.private_content_criteria_going = Some(true), + "interested" => { + build_event_form.private_content_criteria_interested = Some(true) + } + _ => {} + } + } + } + // If we have a single address location, populate the form fields with its data if let LocationEditStatus::Editable(Address { country, @@ -650,10 +675,60 @@ pub(crate) async fn handle_edit_event( error_template, default_context, err, - StatusCode::OK + StatusCode::INTERNAL_SERVER_ERROR ); } + // Save private event content if provided + let private_content = build_event_form.private_content.as_deref().unwrap_or(""); + + // Build display criteria array from checkboxes + let mut display_criteria = Vec::new(); + if build_event_form + .private_content_criteria_going_confirmed + .unwrap_or(false) + { + display_criteria.push("going_confirmed".to_string()); + } + if build_event_form + .private_content_criteria_going + .unwrap_or(false) + { + display_criteria.push("going".to_string()); + } + if build_event_form + .private_content_criteria_interested + .unwrap_or(false) + { + display_criteria.push("interested".to_string()); + } + + // Only save if there's content or criteria, otherwise delete + if !private_content.is_empty() || !display_criteria.is_empty() { + if let Err(err) = + crate::storage::private_event_content::private_event_content_upsert( + &ctx.web_context.pool, + &lookup_aturi, + &display_criteria, + private_content, + ) + .await + { + tracing::error!("Failed to save private event content: {:?}", err); + } + } else { + // Delete private content if both content and criteria are empty + if let Err(err) = + crate::storage::private_event_content::private_event_content_delete( + &ctx.web_context.pool, + &lookup_aturi, + ) + .await + { + tracing::error!("Failed to delete private event content: {:?}", err); + } + } + // Send email notifications to RSVP holders if checkbox is checked and emailer is enabled if build_event_form.send_notifications.unwrap_or(false) && let Some(ref emailer) = ctx.web_context.emailer diff --git a/src/http/handle_oauth_aip_login.rs b/src/http/handle_oauth_aip_login.rs index 08c9d0a..d0c665e 100644 --- a/src/http/handle_oauth_aip_login.rs +++ b/src/http/handle_oauth_aip_login.rs @@ -12,7 +12,7 @@ use axum_htmx::{HxBoosted, HxRedirect, HxRequest}; use axum_template::RenderHtml; use http::StatusCode; use minijinja::context as template_context; -use rand::{Rng, distributions::Alphanumeric}; +use rand::{Rng, distr::Alphanumeric}; use serde::Deserialize; use crate::{ @@ -86,12 +86,12 @@ pub(crate) async fn handle_oauth_aip_login( if let Some(subject) = validated_subject { // Generate OAuth parameters - let state: String = rand::thread_rng() + let state: String = rand::rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) .collect(); - let nonce: String = rand::thread_rng() + let nonce: String = rand::rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) @@ -218,7 +218,9 @@ pub(crate) async fn handle_oauth_aip_login( stringify(oauth_args) ); - if hx_request && let Ok(hx_redirect) = HxRedirect::try_from(destination.as_str()) { + if hx_request { + let hx_redirect = HxRedirect::try_from(destination.as_str()) + .expect("HxRedirect construction should not fail"); return Ok((StatusCode::OK, hx_redirect, "").into_response()); } diff --git a/src/http/handle_oauth_login.rs b/src/http/handle_oauth_login.rs index f819cb7..b68c293 100644 --- a/src/http/handle_oauth_login.rs +++ b/src/http/handle_oauth_login.rs @@ -16,7 +16,7 @@ use axum_htmx::{HxBoosted, HxRedirect, HxRequest}; use axum_template::RenderHtml; use http::StatusCode; use minijinja::context as template_context; -use rand::{Rng, distributions::Alphanumeric}; +use rand::{Rng, distr::Alphanumeric}; use serde::Deserialize; use crate::{ @@ -180,12 +180,12 @@ pub(crate) async fn handle_oauth_login( return contextual_error!(web_context, language, error_template, default_context, err); } - let state: String = rand::thread_rng() + let state: String = rand::rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) .collect(); - let nonce: String = rand::thread_rng() + let nonce: String = rand::rng() .sample_iter(&Alphanumeric) .take(30) .map(char::from) diff --git a/src/http/handle_view_event.rs b/src/http/handle_view_event.rs index 85ab699..a317392 100644 --- a/src/http/handle_view_event.rs +++ b/src/http/handle_view_event.rs @@ -28,7 +28,7 @@ use crate::storage::event::count_event_rsvps; use crate::storage::event::event_exists; use crate::storage::event::event_get; use crate::storage::event::get_event_rsvps_with_validation; -use crate::storage::event::get_user_rsvp_with_email_shared; +use crate::storage::event::get_user_rsvp_status_and_validation; use crate::storage::identity_profile::handle_for_did; use crate::storage::identity_profile::handle_for_handle; use crate::storage::identity_profile::handles_by_did; @@ -285,20 +285,25 @@ pub(crate) async fn handle_view_event( .is_some_and(|current_entity| current_entity.did == profile.did); // Get user's RSVP status if logged in - let user_rsvp_status = if let Some(current_entity) = &ctx.current_handle { - match get_user_rsvp_with_email_shared(&ctx.web_context.pool, &aturi, ¤t_entity.did) + let (user_rsvp_status, user_rsvp_is_validated) = + if let Some(current_entity) = &ctx.current_handle { + match get_user_rsvp_status_and_validation( + &ctx.web_context.pool, + &aturi, + ¤t_entity.did, + ) .await - { - Ok(Some((status, _email_shared))) => Some(status), - Ok(None) => None, - Err(err) => { - tracing::error!("Error getting user RSVP status: {:?}", err); - None + { + Ok(Some((status, is_validated))) => (Some(status), is_validated), + Ok(None) => (None, false), + Err(err) => { + tracing::error!("Error getting user RSVP status: {:?}", err); + (None, false) + } } - } - } else { - None - }; + } else { + (None, false) + }; // Check if there's a pending acceptance ticket for the current user's RSVP let pending_acceptance = if let Some(current_entity) = &ctx.current_handle { @@ -398,6 +403,66 @@ pub(crate) async fn handle_view_event( let interested_tab_url = format!("?tab=interested&collection={}", encoded_collection); let notgoing_tab_url = format!("?tab=notgoing&collection={}", encoded_collection); + // Check if private content should be displayed + let private_content = if ctx.current_handle.is_some() { + // Load private event content + if let Ok(Some(private_event_content)) = + crate::storage::private_event_content::private_event_content_get( + &ctx.web_context.pool, + &aturi, + ) + .await + { + // Check if viewer meets any of the display criteria + let mut meets_criteria = false; + + // If display_criteria is empty, show to anyone who has RSVP'd + if private_event_content.display_criteria.0.is_empty() { + meets_criteria = user_rsvp_status.is_some(); + } else { + // Check specific criteria + for criterion in private_event_content.display_criteria.0.iter() { + match criterion.as_str() { + "going_confirmed" => { + // Check if user has "going" status AND has a validated RSVP + if user_rsvp_status.as_deref() == Some("going") + && user_rsvp_is_validated + { + meets_criteria = true; + break; + } + } + "going" => { + // Check if user has "going" status + if user_rsvp_status.as_deref() == Some("going") { + meets_criteria = true; + break; + } + } + "interested" => { + // Check if user has "interested" status + if user_rsvp_status.as_deref() == Some("interested") { + meets_criteria = true; + break; + } + } + _ => {} + } + } + } + + if meets_criteria { + Some(private_event_content.content) + } else { + None + } + } else { + None + } + } else { + None + }; + Ok(( StatusCode::OK, RenderHtml( @@ -424,6 +489,8 @@ pub(crate) async fn handle_view_event( notgoing_tab_url, // Email confirmation status user_has_confirmed_email, + // Private event content + private_content, }, ), ) diff --git a/src/storage/content.rs b/src/storage/content.rs index 62c2b3e..9bc20ca 100644 --- a/src/storage/content.rs +++ b/src/storage/content.rs @@ -319,7 +319,8 @@ impl CachedContentStorage { bloom_filter_capacity: usize, bloom_filter_error_rate: f64, ) -> Self { - let bloom_filter = Bloom::new_for_fp_rate(bloom_filter_capacity, bloom_filter_error_rate); + let bloom_filter = Bloom::new_for_fp_rate(bloom_filter_capacity, bloom_filter_error_rate) + .expect("TODO fix this"); Self { underlying_storage, @@ -381,7 +382,7 @@ impl CachedContentStorage { // This is a limitation of bloom filters - in production you might want // to use a counting bloom filter or periodically reset the filter let mut filter = self.not_found_filter.write().await; - *filter = Bloom::new_for_fp_rate(10000, 0.01); + *filter = Bloom::new_for_fp_rate(10000, 0.01).expect("TODO fix this"); } } diff --git a/src/storage/event.rs b/src/storage/event.rs index 824c20e..a8e60b6 100644 --- a/src/storage/event.rs +++ b/src/storage/event.rs @@ -589,7 +589,7 @@ pub async fn get_user_rsvp( Ok(status) } -pub async fn get_user_rsvp_with_email_shared( +pub async fn get_user_rsvp_status_and_validation( pool: &StoragePool, event_aturi: &str, did: &str, @@ -614,7 +614,7 @@ pub async fn get_user_rsvp_with_email_shared( .map_err(StorageError::CannotBeginDatabaseTransaction)?; let result = sqlx::query_as::<_, (String, bool)>( - "SELECT status, email_shared FROM rsvps WHERE event_aturi = $1 AND did = $2", + "SELECT status, validated_at IS NOT NULL FROM rsvps WHERE event_aturi = $1 AND did = $2", ) .bind(event_aturi) .bind(did) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 156b290..92e5215 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -9,6 +9,7 @@ pub mod event; pub mod identity_profile; pub mod notification; pub mod oauth; +pub mod private_event_content; pub mod profile; pub mod types; pub mod webhook; diff --git a/src/storage/private_event_content.rs b/src/storage/private_event_content.rs new file mode 100644 index 0000000..690b1cb --- /dev/null +++ b/src/storage/private_event_content.rs @@ -0,0 +1,119 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sqlx::FromRow; + +use crate::storage::StoragePool; +use crate::storage::errors::StorageError; + +#[derive(Clone, FromRow, Deserialize, Serialize, Debug)] +pub struct PrivateEventContent { + pub aturi: String, + pub display_criteria: sqlx::types::Json>, + pub content: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +/// Upsert private event content for an event +pub async fn private_event_content_upsert( + pool: &StoragePool, + aturi: &str, + display_criteria: &[String], + content: &str, +) -> Result<(), StorageError> { + // Validate aturi is not empty + if aturi.trim().is_empty() { + return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol( + "Event URI cannot be empty".into(), + ))); + } + + let mut tx = pool + .begin() + .await + .map_err(StorageError::CannotBeginDatabaseTransaction)?; + + let now = Utc::now(); + + sqlx::query( + "INSERT INTO private_event_content (aturi, display_criteria, content, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (aturi) DO UPDATE SET + display_criteria = $2, + content = $3, + updated_at = $5", + ) + .bind(aturi) + .bind(json!(display_criteria)) + .bind(content) + .bind(now) + .bind(now) + .execute(tx.as_mut()) + .await + .map_err(StorageError::UnableToExecuteQuery)?; + + tx.commit() + .await + .map_err(StorageError::CannotCommitDatabaseTransaction) +} + +/// Get private event content for an event by AT-URI +pub async fn private_event_content_get( + pool: &StoragePool, + aturi: &str, +) -> Result, StorageError> { + // Validate aturi is not empty + if aturi.trim().is_empty() { + return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol( + "Event URI cannot be empty".into(), + ))); + } + + let mut tx = pool + .begin() + .await + .map_err(StorageError::CannotBeginDatabaseTransaction)?; + + let content = sqlx::query_as::<_, PrivateEventContent>( + "SELECT * FROM private_event_content WHERE aturi = $1", + ) + .bind(aturi) + .fetch_optional(tx.as_mut()) + .await + .map_err(StorageError::UnableToExecuteQuery)?; + + tx.commit() + .await + .map_err(StorageError::CannotCommitDatabaseTransaction)?; + + Ok(content) +} + +/// Delete private event content for an event +pub async fn private_event_content_delete( + pool: &StoragePool, + aturi: &str, +) -> Result<(), StorageError> { + // Validate aturi is not empty + if aturi.trim().is_empty() { + return Err(StorageError::UnableToExecuteQuery(sqlx::Error::Protocol( + "Event URI cannot be empty".into(), + ))); + } + + let mut tx = pool + .begin() + .await + .map_err(StorageError::CannotBeginDatabaseTransaction)?; + + sqlx::query("DELETE FROM private_event_content WHERE aturi = $1") + .bind(aturi) + .execute(tx.as_mut()) + .await + .map_err(StorageError::UnableToExecuteQuery)?; + + tx.commit() + .await + .map_err(StorageError::CannotCommitDatabaseTransaction) +} diff --git a/src/task_webhooks.rs b/src/task_webhooks.rs index 5f41824..df4789c 100644 --- a/src/task_webhooks.rs +++ b/src/task_webhooks.rs @@ -2,7 +2,7 @@ use anyhow::Result; use atproto_identity::traits::DidDocumentStorage; use atproto_oauth::jwt::{Claims, Header, mint}; use chrono::Utc; -use rand::distributions::{Alphanumeric, DistString}; +use rand::distr::{Alphanumeric, SampleString}; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; @@ -197,7 +197,7 @@ impl WebhookProcessor { let header: Header = self.service_key.0.clone().try_into()?; let now = Utc::now(); - let jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); + let jti = Alphanumeric.sample_string(&mut rand::rng(), 30); // Create base claims let mut claims = Claims::default(); diff --git a/templates/en-us/edit_event.common.html b/templates/en-us/edit_event.common.html index 54ead87..334a936 100644 --- a/templates/en-us/edit_event.common.html +++ b/templates/en-us/edit_event.common.html @@ -68,6 +68,80 @@ + + {% if delete_event_url %} {% include 'en-us/delete_event.partial.html' %} {% endif %} diff --git a/templates/en-us/view_event.common.html b/templates/en-us/view_event.common.html index df19960..4afcf6b 100644 --- a/templates/en-us/view_event.common.html +++ b/templates/en-us/view_event.common.html @@ -279,6 +279,26 @@ +{% if private_content %} +
+
+
+
+

+ + + + Private Event Information +

+
+
+ {{ private_content }} +
+
+
+
+{% endif %} +