max / makenotwork
18 files changed,
+184 insertions,
-116 deletions
| @@ -12,7 +12,9 @@ | |||
| 12 | 12 | //! on the user row. New-device login notifications are sent via Postmark | |
| 13 | 13 | //! when enabled. | |
| 14 | 14 | //! | |
| 15 | - | //! Extractors: [`AuthUser`] (required login), [`MaybeUser`] (optional), | |
| 15 | + | //! Extractors: [`AuthUser`] (required login), [`MaybeUserUnverified`] (optional, | |
| 16 | + | //! no revocation check — public read-only pages only), [`MaybeUserVerified`] | |
| 17 | + | //! (optional with revocation check — anywhere identity actually gates behavior), | |
| 16 | 18 | //! [`AdminUser`] (admin-only, hides routes with 404). | |
| 17 | 19 | ||
| 18 | 20 | use argon2::{ | |
| @@ -199,13 +201,24 @@ impl FromRequestParts<crate::AppState> for AuthUser { | |||
| 199 | 201 | ||
| 200 | 202 | /// Extractor for optional authenticated users — returns None if not logged in. | |
| 201 | 203 | /// | |
| 202 | - | /// **Security note:** Unlike `AuthUser`, this does NOT validate the session tracking ID | |
| 203 | - | /// against the database. Revoked sessions may still appear authenticated. Use only on | |
| 204 | - | /// read-only or public endpoints where this is acceptable. For any endpoint that modifies | |
| 205 | - | /// data or displays sensitive information, use `AuthUser` instead. | |
| 206 | - | pub struct MaybeUser(pub Option<SessionUser>); | |
| 207 | - | ||
| 208 | - | impl<S> FromRequestParts<S> for MaybeUser | |
| 204 | + | /// **DANGER — this extractor does NOT validate the session against the database.** | |
| 205 | + | /// A revoked session (user clicked "log out everywhere", account suspended, | |
| 206 | + | /// session row deleted) will still resolve to `Some(SessionUser)` here until | |
| 207 | + | /// the cookie naturally expires. The name carries the warning: any handler | |
| 208 | + | /// that uses this type accepts that consequence. | |
| 209 | + | /// | |
| 210 | + | /// Use ONLY for cheap anonymous-or-logged-in rendering on public read-only | |
| 211 | + | /// pages where displaying stale identity is acceptable (blog views, docs, | |
| 212 | + | /// discover feed). For any handler that: | |
| 213 | + | /// - modifies data, | |
| 214 | + | /// - gates paid content or downloads, | |
| 215 | + | /// - issues OAuth tokens / grants, | |
| 216 | + | /// - exposes account-private information, | |
| 217 | + | /// use [`AuthUser`] (required login) or [`MaybeUserVerified`] (optional login | |
| 218 | + | /// with revocation check) instead. | |
| 219 | + | pub struct MaybeUserUnverified(pub Option<SessionUser>); | |
| 220 | + | ||
| 221 | + | impl<S> FromRequestParts<S> for MaybeUserUnverified | |
| 209 | 222 | where | |
| 210 | 223 | S: Send + Sync, | |
| 211 | 224 | { | |
| @@ -222,7 +235,83 @@ where | |||
| 222 | 235 | .await | |
| 223 | 236 | .context("session error")?; | |
| 224 | 237 | ||
| 225 | - | Ok(MaybeUser(user)) | |
| 238 | + | Ok(MaybeUserUnverified(user)) | |
| 239 | + | } | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | /// Extractor for optional authenticated users WITH revocation check. | |
| 243 | + | /// | |
| 244 | + | /// Like [`MaybeUserUnverified`] but runs the same session-tracking validation | |
| 245 | + | /// as [`AuthUser`]: if the tracking row has been deleted (revoked) or the | |
| 246 | + | /// account is suspended, the session is flushed and `None` is returned (the | |
| 247 | + | /// request continues as anonymous rather than 401, since the handler chose | |
| 248 | + | /// "optional auth"). Legacy sessions without a tracking ID pass through. | |
| 249 | + | /// | |
| 250 | + | /// Costs one cached `touch_session` query per request (TTL = `SESSION_TOUCH_CACHE_SECS`). | |
| 251 | + | /// Prefer this over `MaybeUserUnverified` anywhere the identity actually gates | |
| 252 | + | /// behavior — paid content access, OAuth flows, download grants, comments, | |
| 253 | + | /// or anything that writes to the DB on behalf of the user. | |
| 254 | + | pub struct MaybeUserVerified(pub Option<SessionUser>); | |
| 255 | + | ||
| 256 | + | impl FromRequestParts<crate::AppState> for MaybeUserVerified { | |
| 257 | + | type Rejection = AppError; | |
| 258 | + | ||
| 259 | + | async fn from_request_parts( | |
| 260 | + | parts: &mut Parts, | |
| 261 | + | state: &crate::AppState, | |
| 262 | + | ) -> Result<Self, Self::Rejection> { | |
| 263 | + | let session = parts | |
| 264 | + | .extensions | |
| 265 | + | .get::<Session>() | |
| 266 | + | .ok_or(AppError::Internal(anyhow::anyhow!("Session not found")))?; | |
| 267 | + | ||
| 268 | + | let Some(mut user): Option<SessionUser> = session | |
| 269 | + | .get(USER_SESSION_KEY) | |
| 270 | + | .await | |
| 271 | + | .context("session error")? | |
| 272 | + | else { | |
| 273 | + | return Ok(MaybeUserVerified(None)); | |
| 274 | + | }; | |
| 275 | + | ||
| 276 | + | if let Ok(Some(tracking_id)) = session | |
| 277 | + | .get::<UserSessionId>(SESSION_TRACKING_KEY) | |
| 278 | + | .await | |
| 279 | + | { | |
| 280 | + | let cache_ttl = std::time::Duration::from_secs(constants::SESSION_TOUCH_CACHE_SECS); | |
| 281 | + | let cached = state.session_cache.get(&tracking_id) | |
| 282 | + | .map(|entry| entry.elapsed() < cache_ttl) | |
| 283 | + | .unwrap_or(false); | |
| 284 | + | ||
| 285 | + | if !cached { | |
| 286 | + | let result = match db::sessions::touch_session(&state.db, tracking_id).await { | |
| 287 | + | Ok(r) => r, | |
| 288 | + | Err(e) => { | |
| 289 | + | tracing::warn!(error = ?e, "session touch failed in MaybeUserVerified, treating as anonymous"); | |
| 290 | + | db::sessions::TouchResult { valid: false, suspended: false, can_create_projects: false, is_fan_plus: false, creator_tier: None } | |
| 291 | + | } | |
| 292 | + | }; | |
| 293 | + | if !result.valid { | |
| 294 | + | state.session_cache.remove(&tracking_id); | |
| 295 | + | let _ = session.flush().await; | |
| 296 | + | return Ok(MaybeUserVerified(None)); | |
| 297 | + | } | |
| 298 | + | let live_tier: Option<db::CreatorTier> = result.creator_tier.as_deref().and_then(|s| s.parse().ok()); | |
| 299 | + | if user.suspended != result.suspended || user.is_fan_plus != result.is_fan_plus || user.can_create_projects != result.can_create_projects || user.creator_tier != live_tier { | |
| 300 | + | user.suspended = result.suspended; | |
| 301 | + | user.is_fan_plus = result.is_fan_plus; | |
| 302 | + | user.can_create_projects = result.can_create_projects; | |
| 303 | + | user.creator_tier = live_tier; | |
| 304 | + | if let Err(e) = session.insert(USER_SESSION_KEY, user.clone()).await { | |
| 305 | + | tracing::warn!(user_id = %user.id, error = ?e, "failed to update session with refreshed user state"); | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | state.session_cache.insert(tracking_id, Instant::now()); | |
| 309 | + | } | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | tracing::Span::current().record("user_id", tracing::field::display(&user.id)); | |
| 313 | + | ||
| 314 | + | Ok(MaybeUserVerified(Some(user))) | |
| 226 | 315 | } | |
| 227 | 316 | } | |
| 228 | 317 |
| @@ -12,7 +12,7 @@ use axum::{ | |||
| 12 | 12 | use tower_sessions::Session; | |
| 13 | 13 | ||
| 14 | 14 | use crate::{ | |
| 15 | - | auth::{MaybeUser, SessionUser}, | |
| 15 | + | auth::{MaybeUserUnverified, SessionUser}, | |
| 16 | 16 | db::{self, Slug}, | |
| 17 | 17 | error::{AppError, Result}, | |
| 18 | 18 | helpers::get_csrf_token, | |
| @@ -100,7 +100,7 @@ pub async fn custom_domain_fallback( | |||
| 100 | 100 | headers: HeaderMap, | |
| 101 | 101 | uri: Uri, | |
| 102 | 102 | session: Session, | |
| 103 | - | MaybeUser(maybe_user): MaybeUser, | |
| 103 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 104 | 104 | ) -> Response { | |
| 105 | 105 | let Some(host) = extract_host(&headers) else { | |
| 106 | 106 | return StatusCode::NOT_FOUND.into_response(); |
| @@ -9,7 +9,7 @@ use serde::Deserialize; | |||
| 9 | 9 | use tower_sessions::Session; | |
| 10 | 10 | ||
| 11 | 11 | use crate::{ | |
| 12 | - | auth::MaybeUser, | |
| 12 | + | auth::MaybeUserUnverified, | |
| 13 | 13 | constants, | |
| 14 | 14 | db, | |
| 15 | 15 | error::{AppError, Result}, | |
| @@ -28,7 +28,7 @@ use super::{ | |||
| 28 | 28 | pub(super) async fn repo_overview( | |
| 29 | 29 | State(state): State<AppState>, | |
| 30 | 30 | session: Session, | |
| 31 | - | MaybeUser(maybe_user): MaybeUser, | |
| 31 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 32 | 32 | Path((owner, repo_name)): Path<(String, String)>, | |
| 33 | 33 | ) -> Result<impl IntoResponse> { | |
| 34 | 34 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -76,7 +76,7 @@ pub(super) async fn repo_overview( | |||
| 76 | 76 | pub(super) async fn tree_root( | |
| 77 | 77 | State(state): State<AppState>, | |
| 78 | 78 | session: Session, | |
| 79 | - | MaybeUser(maybe_user): MaybeUser, | |
| 79 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 80 | 80 | Path((owner, repo_name, git_ref)): Path<(String, String, String)>, | |
| 81 | 81 | ) -> Result<impl IntoResponse> { | |
| 82 | 82 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -122,7 +122,7 @@ pub(super) async fn tree_root( | |||
| 122 | 122 | pub(super) async fn tree_or_file( | |
| 123 | 123 | State(state): State<AppState>, | |
| 124 | 124 | session: Session, | |
| 125 | - | MaybeUser(maybe_user): MaybeUser, | |
| 125 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 126 | 126 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 127 | 127 | ) -> Result<Response> { | |
| 128 | 128 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -220,7 +220,7 @@ pub(super) struct CommitQuery { | |||
| 220 | 220 | pub(super) async fn commit_log( | |
| 221 | 221 | State(state): State<AppState>, | |
| 222 | 222 | session: Session, | |
| 223 | - | MaybeUser(maybe_user): MaybeUser, | |
| 223 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 224 | 224 | Path((owner, repo_name, git_ref)): Path<(String, String, String)>, | |
| 225 | 225 | Query(query): Query<CommitQuery>, | |
| 226 | 226 | ) -> Result<impl IntoResponse> { | |
| @@ -262,7 +262,7 @@ pub(super) async fn commit_log( | |||
| 262 | 262 | pub(super) async fn commit_detail_page( | |
| 263 | 263 | State(state): State<AppState>, | |
| 264 | 264 | session: Session, | |
| 265 | - | MaybeUser(maybe_user): MaybeUser, | |
| 265 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 266 | 266 | Path((owner, repo_name, oid_str)): Path<(String, String, String)>, | |
| 267 | 267 | ) -> Result<impl IntoResponse> { | |
| 268 | 268 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -311,7 +311,7 @@ pub(super) async fn commit_detail_page( | |||
| 311 | 311 | pub(super) async fn blame_view( | |
| 312 | 312 | State(state): State<AppState>, | |
| 313 | 313 | session: Session, | |
| 314 | - | MaybeUser(maybe_user): MaybeUser, | |
| 314 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 315 | 315 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 316 | 316 | ) -> Result<impl IntoResponse> { | |
| 317 | 317 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -348,7 +348,7 @@ pub(super) async fn blame_view( | |||
| 348 | 348 | pub(super) async fn user_repos( | |
| 349 | 349 | State(state): State<AppState>, | |
| 350 | 350 | session: Session, | |
| 351 | - | MaybeUser(maybe_user): MaybeUser, | |
| 351 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 352 | 352 | Path(owner): Path<String>, | |
| 353 | 353 | ) -> Result<impl IntoResponse> { | |
| 354 | 354 | let username = db::Username::new(&owner).map_err(|_| AppError::NotFound)?; | |
| @@ -386,7 +386,7 @@ pub(super) struct ExploreQuery { | |||
| 386 | 386 | pub(super) async fn git_landing( | |
| 387 | 387 | State(state): State<AppState>, | |
| 388 | 388 | session: Session, | |
| 389 | - | MaybeUser(maybe_user): MaybeUser, | |
| 389 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 390 | 390 | Query(query): Query<ExploreQuery>, | |
| 391 | 391 | ) -> Result<impl IntoResponse> { | |
| 392 | 392 | let page = query.page.unwrap_or(1).clamp(1, 10_000); | |
| @@ -415,7 +415,7 @@ pub(super) async fn git_landing( | |||
| 415 | 415 | pub(super) async fn file_log( | |
| 416 | 416 | State(state): State<AppState>, | |
| 417 | 417 | session: Session, | |
| 418 | - | MaybeUser(maybe_user): MaybeUser, | |
| 418 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 419 | 419 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 420 | 420 | Query(query): Query<CommitQuery>, | |
| 421 | 421 | ) -> Result<impl IntoResponse> { |
| @@ -9,7 +9,7 @@ use axum::{ | |||
| 9 | 9 | use serde::Deserialize; | |
| 10 | 10 | ||
| 11 | 11 | use crate::{ | |
| 12 | - | auth::MaybeUser, | |
| 12 | + | auth::MaybeUserUnverified, | |
| 13 | 13 | constants, | |
| 14 | 14 | error::{AppError, Result, ResultExt}, | |
| 15 | 15 | git, | |
| @@ -22,7 +22,7 @@ use super::{repos_root, resolve_repo, resolve_repo_name}; | |||
| 22 | 22 | #[tracing::instrument(skip_all, name = "git::raw_file")] | |
| 23 | 23 | pub(super) async fn raw_file( | |
| 24 | 24 | State(state): State<AppState>, | |
| 25 | - | MaybeUser(maybe_user): MaybeUser, | |
| 25 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 26 | 26 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 27 | 27 | ) -> Result<Response> { | |
| 28 | 28 | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| @@ -93,7 +93,7 @@ pub(super) struct InfoRefsQuery { | |||
| 93 | 93 | #[tracing::instrument(skip_all, name = "git::smart_http_info_refs")] | |
| 94 | 94 | pub(super) async fn smart_http_info_refs( | |
| 95 | 95 | State(state): State<AppState>, | |
| 96 | - | MaybeUser(maybe_user): MaybeUser, | |
| 96 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 97 | 97 | Path((owner, repo_name)): Path<(String, String)>, | |
| 98 | 98 | Query(query): Query<InfoRefsQuery>, | |
| 99 | 99 | ) -> Result<Response> { | |
| @@ -146,7 +146,7 @@ pub(super) async fn smart_http_info_refs( | |||
| 146 | 146 | #[tracing::instrument(skip_all, name = "git::smart_http_upload_pack")] | |
| 147 | 147 | pub(super) async fn smart_http_upload_pack( | |
| 148 | 148 | State(state): State<AppState>, | |
| 149 | - | MaybeUser(maybe_user): MaybeUser, | |
| 149 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 150 | 150 | Path((owner, repo_name)): Path<(String, String)>, | |
| 151 | 151 | body: axum::body::Bytes, | |
| 152 | 152 | ) -> Result<Response> { |
| @@ -8,7 +8,7 @@ use serde::Deserialize; | |||
| 8 | 8 | use tower_sessions::Session; | |
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | - | auth::MaybeUser, | |
| 11 | + | auth::MaybeUserVerified, | |
| 12 | 12 | db::{self, IssueStatus}, | |
| 13 | 13 | error::{AppError, Result}, | |
| 14 | 14 | helpers::get_csrf_token, | |
| @@ -32,7 +32,7 @@ pub(super) struct IssueListQuery { | |||
| 32 | 32 | pub(super) async fn issue_list( | |
| 33 | 33 | State(state): State<AppState>, | |
| 34 | 34 | session: Session, | |
| 35 | - | MaybeUser(maybe_user): MaybeUser, | |
| 35 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 36 | 36 | Path((owner, repo_name)): Path<(String, String)>, | |
| 37 | 37 | Query(query): Query<IssueListQuery>, | |
| 38 | 38 | ) -> Result<impl IntoResponse> { | |
| @@ -84,7 +84,7 @@ pub(super) async fn issue_list( | |||
| 84 | 84 | pub(super) async fn issue_detail( | |
| 85 | 85 | State(state): State<AppState>, | |
| 86 | 86 | session: Session, | |
| 87 | - | MaybeUser(maybe_user): MaybeUser, | |
| 87 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 88 | 88 | Path((owner, repo_name, number)): Path<(String, String, i32)>, | |
| 89 | 89 | ) -> Result<impl IntoResponse> { | |
| 90 | 90 | let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; |
| @@ -18,10 +18,10 @@ use tower_governor::GovernorLayer; | |||
| 18 | 18 | use tower_sessions::Session; | |
| 19 | 19 | ||
| 20 | 20 | use crate::{ | |
| 21 | - | auth::{verify_password, MaybeUser, SESSION_TRACKING_KEY}, | |
| 21 | + | auth::{verify_password, MaybeUserVerified}, | |
| 22 | 22 | constants::{self, LOCKOUT_MINUTES, MAX_LOGIN_ATTEMPTS}, | |
| 23 | 23 | csrf, | |
| 24 | - | db::{self, CreatorTier, SyncAppId, UserId, UserSessionId, Username}, | |
| 24 | + | db::{self, CreatorTier, SyncAppId, UserId, Username}, | |
| 25 | 25 | error::{AppError, Result}, | |
| 26 | 26 | synckit_auth, | |
| 27 | 27 | templates::OAuthAuthorizeTemplate, | |
| @@ -143,7 +143,7 @@ fn render_authorize_error( | |||
| 143 | 143 | #[tracing::instrument(skip_all, name = "oauth::authorize_get")] | |
| 144 | 144 | async fn authorize_get( | |
| 145 | 145 | State(state): State<AppState>, | |
| 146 | - | MaybeUser(session_user): MaybeUser, | |
| 146 | + | MaybeUserVerified(session_user): MaybeUserVerified, | |
| 147 | 147 | session: Session, | |
| 148 | 148 | Query(params): Query<AuthorizeQuery>, | |
| 149 | 149 | ) -> Result<Response> { | |
| @@ -197,7 +197,7 @@ async fn authorize_get( | |||
| 197 | 197 | #[tracing::instrument(skip_all, name = "oauth::authorize_post")] | |
| 198 | 198 | async fn authorize_post( | |
| 199 | 199 | State(state): State<AppState>, | |
| 200 | - | MaybeUser(session_user): MaybeUser, | |
| 200 | + | MaybeUserVerified(session_user): MaybeUserVerified, | |
| 201 | 201 | session: Session, | |
| 202 | 202 | Form(form): Form<AuthorizeForm>, | |
| 203 | 203 | ) -> Result<Response> { | |
| @@ -222,49 +222,16 @@ async fn authorize_post( | |||
| 222 | 222 | ||
| 223 | 223 | let csrf_token = csrf::get_or_create_token(&session).await?; | |
| 224 | 224 | ||
| 225 | - | // Determine the authenticated user. | |
| 226 | - | // When a session user is present, validate the session tracking row (mirrors | |
| 227 | - | // AuthUser logic) so that revoked sessions cannot authorize OAuth grants. | |
| 228 | - | let validated_session_user = if let Some(ref user) = session_user { | |
| 229 | - | // Check session tracking: if the tracked session has been revoked, discard it | |
| 230 | - | let still_valid = if let Ok(Some(tracking_id)) = session | |
| 231 | - | .get::<UserSessionId>(SESSION_TRACKING_KEY) | |
| 232 | - | .await | |
| 233 | - | { | |
| 234 | - | let cache_ttl = std::time::Duration::from_secs(constants::SESSION_TOUCH_CACHE_SECS); | |
| 235 | - | let cached = state.session_cache.get(&tracking_id) | |
| 236 | - | .map(|entry| entry.elapsed() < cache_ttl) | |
| 237 | - | .unwrap_or(false); | |
| 238 | - | ||
| 239 | - | if cached { | |
| 240 | - | true | |
| 241 | - | } else { | |
| 242 | - | let result = match db::sessions::touch_session(&state.db, tracking_id).await { | |
| 243 | - | Ok(r) => r, | |
| 244 | - | Err(e) => { | |
| 245 | - | tracing::warn!(error = ?e, "oauth session touch failed, invalidating"); | |
| 246 | - | db::sessions::TouchResult { valid: false, suspended: false, can_create_projects: false, is_fan_plus: false, creator_tier: None } | |
| 247 | - | } | |
| 248 | - | }; | |
| 249 | - | if result.valid && !result.suspended { | |
| 250 | - | state.session_cache.insert(tracking_id, std::time::Instant::now()); | |
| 251 | - | true | |
| 252 | - | } else { | |
| 253 | - | state.session_cache.remove(&tracking_id); | |
| 254 | - | let _ = session.flush().await; | |
| 255 | - | false | |
| 256 | - | } | |
| 257 | - | } | |
| 258 | - | } else { | |
| 259 | - | // Legacy session without tracking ID — require re-authentication | |
| 260 | - | // for OAuth grants (stale session data could mask suspension) | |
| 261 | - | false | |
| 262 | - | }; | |
| 263 | - | ||
| 264 | - | if still_valid { Some(user) } else { None } | |
| 265 | - | } else { | |
| 266 | - | None | |
| 267 | - | }; | |
| 225 | + | // Session revocation/suspension is checked by MaybeUserVerified at extraction. | |
| 226 | + | // For OAuth grants specifically, also require a tracking ID — legacy | |
| 227 | + | // sessions predating session tracking must re-authenticate via password. | |
| 228 | + | let has_tracking = session | |
| 229 | + | .get::<crate::db::UserSessionId>(crate::auth::SESSION_TRACKING_KEY) | |
| 230 | + | .await | |
| 231 | + | .ok() | |
| 232 | + | .flatten() | |
| 233 | + | .is_some(); | |
| 234 | + | let validated_session_user = session_user.as_ref().filter(|_| has_tracking); | |
| 268 | 235 | ||
| 269 | 236 | let user_id = if let Some(user) = validated_session_user { | |
| 270 | 237 | // Already logged in via validated MNW session — skip password check |
| @@ -9,7 +9,7 @@ use axum::{ | |||
| 9 | 9 | use tower_sessions::Session; | |
| 10 | 10 | ||
| 11 | 11 | use crate::{ | |
| 12 | - | auth::MaybeUser, | |
| 12 | + | auth::MaybeUserUnverified, | |
| 13 | 13 | constants, | |
| 14 | 14 | db::{self, Slug}, | |
| 15 | 15 | error::{AppError, Result}, | |
| @@ -33,7 +33,7 @@ pub fn blog_routes() -> Router<AppState> { | |||
| 33 | 33 | async fn project_blog_page( | |
| 34 | 34 | State(state): State<AppState>, | |
| 35 | 35 | session: Session, | |
| 36 | - | MaybeUser(maybe_user): MaybeUser, | |
| 36 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 37 | 37 | Path(slug): Path<String>, | |
| 38 | 38 | ) -> Result<impl IntoResponse> { | |
| 39 | 39 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -68,7 +68,7 @@ async fn project_blog_page( | |||
| 68 | 68 | async fn blog_post_page( | |
| 69 | 69 | State(state): State<AppState>, | |
| 70 | 70 | session: Session, | |
| 71 | - | MaybeUser(maybe_user): MaybeUser, | |
| 71 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 72 | 72 | Path((slug, post_slug)): Path<(String, String)>, | |
| 73 | 73 | ) -> Result<impl IntoResponse> { | |
| 74 | 74 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -132,7 +132,7 @@ async fn blog_post_page( | |||
| 132 | 132 | async fn changelog_index( | |
| 133 | 133 | State(state): State<AppState>, | |
| 134 | 134 | session: Session, | |
| 135 | - | MaybeUser(maybe_user): MaybeUser, | |
| 135 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 136 | 136 | ) -> Result<impl IntoResponse> { | |
| 137 | 137 | let csrf_token = get_csrf_token(&session).await; | |
| 138 | 138 | ||
| @@ -170,7 +170,7 @@ async fn changelog_index( | |||
| 170 | 170 | async fn changelog_post( | |
| 171 | 171 | State(state): State<AppState>, | |
| 172 | 172 | session: Session, | |
| 173 | - | MaybeUser(maybe_user): MaybeUser, | |
| 173 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 174 | 174 | Path(post_slug): Path<String>, | |
| 175 | 175 | ) -> Result<impl IntoResponse> { | |
| 176 | 176 | let csrf_token = get_csrf_token(&session).await; |
| @@ -7,7 +7,7 @@ use axum::{ | |||
| 7 | 7 | use tower_sessions::Session; | |
| 8 | 8 | ||
| 9 | 9 | use crate::{ | |
| 10 | - | auth::{MaybeUser, SessionUser}, | |
| 10 | + | auth::{MaybeUserUnverified, SessionUser}, | |
| 11 | 11 | db::{self, ContentData, ItemId, ItemType}, | |
| 12 | 12 | error::{AppError, Result}, | |
| 13 | 13 | helpers::{fetch_discussion_info, get_csrf_token, get_initials}, | |
| @@ -22,7 +22,7 @@ use crate::{ | |||
| 22 | 22 | pub(in crate::routes::pages::public) async fn item_page( | |
| 23 | 23 | State(state): State<AppState>, | |
| 24 | 24 | session: Session, | |
| 25 | - | MaybeUser(maybe_user): MaybeUser, | |
| 25 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 26 | 26 | Path(item_id): Path<String>, | |
| 27 | 27 | ) -> Result<Response> { | |
| 28 | 28 | let csrf_token = get_csrf_token(&session).await; |
| @@ -10,7 +10,7 @@ use axum::{ | |||
| 10 | 10 | use tower_sessions::Session; | |
| 11 | 11 | ||
| 12 | 12 | use crate::{ | |
| 13 | - | auth::MaybeUser, | |
| 13 | + | auth::MaybeUserVerified, | |
| 14 | 14 | constants, | |
| 15 | 15 | db::{self, ContentData, ItemId, ItemType}, | |
| 16 | 16 | error::{AppError, Result}, | |
| @@ -27,7 +27,7 @@ pub(in crate::routes::pages::public) async fn library_page( | |||
| 27 | 27 | State(state): State<AppState>, | |
| 28 | 28 | session: Session, | |
| 29 | 29 | headers: axum::http::HeaderMap, | |
| 30 | - | MaybeUser(maybe_user): MaybeUser, | |
| 30 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 31 | 31 | Path(item_id): Path<String>, | |
| 32 | 32 | ) -> Result<Response> { | |
| 33 | 33 | let csrf_token = get_csrf_token(&session).await; |
| @@ -18,7 +18,7 @@ use serde::Deserialize; | |||
| 18 | 18 | use tower_sessions::Session; | |
| 19 | 19 | ||
| 20 | 20 | use crate::{ | |
| 21 | - | auth::{MaybeUser, SessionUser}, | |
| 21 | + | auth::{MaybeUserUnverified, MaybeUserVerified, SessionUser}, | |
| 22 | 22 | db::{self, FollowTargetType, ItemId, Username}, | |
| 23 | 23 | error::{AppError, Result}, | |
| 24 | 24 | helpers::get_csrf_token, | |
| @@ -66,7 +66,7 @@ pub(super) async fn user_page( | |||
| 66 | 66 | State(state): State<AppState>, | |
| 67 | 67 | session: Session, | |
| 68 | 68 | headers: axum::http::HeaderMap, | |
| 69 | - | MaybeUser(maybe_user): MaybeUser, | |
| 69 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 70 | 70 | Path(username): Path<String>, | |
| 71 | 71 | ) -> Result<Response> { | |
| 72 | 72 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -150,7 +150,7 @@ pub(crate) async fn render_user_profile( | |||
| 150 | 150 | pub(super) async fn purchase_page( | |
| 151 | 151 | State(state): State<AppState>, | |
| 152 | 152 | session: Session, | |
| 153 | - | MaybeUser(maybe_user): MaybeUser, | |
| 153 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 154 | 154 | Path(item_id): Path<String>, | |
| 155 | 155 | Query(query): Query<PurchaseQuery>, | |
| 156 | 156 | ) -> Result<impl IntoResponse> { | |
| @@ -239,7 +239,7 @@ fn format_relative_ago(ts: chrono::DateTime<chrono::Utc>) -> String { | |||
| 239 | 239 | pub(super) async fn receipt_page( | |
| 240 | 240 | State(state): State<AppState>, | |
| 241 | 241 | session: Session, | |
| 242 | - | MaybeUser(maybe_user): MaybeUser, | |
| 242 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 243 | 243 | Path(transaction_id): Path<String>, | |
| 244 | 244 | ) -> Result<impl IntoResponse> { | |
| 245 | 245 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -293,7 +293,7 @@ pub(super) async fn receipt_page( | |||
| 293 | 293 | pub(super) async fn collection_page( | |
| 294 | 294 | State(state): State<AppState>, | |
| 295 | 295 | session: Session, | |
| 296 | - | MaybeUser(maybe_user): MaybeUser, | |
| 296 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 297 | 297 | Path((username, slug)): Path<(String, String)>, | |
| 298 | 298 | ) -> Result<impl IntoResponse> { | |
| 299 | 299 | let csrf_token = get_csrf_token(&session).await; |
| @@ -7,7 +7,7 @@ use axum::{ | |||
| 7 | 7 | use tower_sessions::Session; | |
| 8 | 8 | ||
| 9 | 9 | use crate::{ | |
| 10 | - | auth::{MaybeUser, SessionUser}, | |
| 10 | + | auth::{MaybeUserVerified, SessionUser}, | |
| 11 | 11 | db::{self, FollowTargetType, ItemId, ItemType, Slug}, | |
| 12 | 12 | error::{AppError, Result}, | |
| 13 | 13 | helpers::get_csrf_token, | |
| @@ -23,7 +23,7 @@ pub(in crate::routes::pages::public) async fn project_page( | |||
| 23 | 23 | State(state): State<AppState>, | |
| 24 | 24 | session: Session, | |
| 25 | 25 | headers: axum::http::HeaderMap, | |
| 26 | - | MaybeUser(maybe_user): MaybeUser, | |
| 26 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 27 | 27 | Path(slug): Path<String>, | |
| 28 | 28 | ) -> Result<Response> { | |
| 29 | 29 | let csrf_token = get_csrf_token(&session).await; |
| @@ -8,7 +8,7 @@ use sqlx::PgPool; | |||
| 8 | 8 | use tower_sessions::Session; | |
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | - | auth::MaybeUser, | |
| 11 | + | auth::MaybeUserUnverified, | |
| 12 | 12 | constants, | |
| 13 | 13 | db::{self, discover::DiscoverFilters, DiscoverSort, ItemType}, | |
| 14 | 14 | error::Result, | |
| @@ -221,7 +221,7 @@ pub struct TagTreeQuery { | |||
| 221 | 221 | pub(super) async fn tag_tree( | |
| 222 | 222 | State(state): State<AppState>, | |
| 223 | 223 | session: Session, | |
| 224 | - | MaybeUser(maybe_user): MaybeUser, | |
| 224 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 225 | 225 | Query(query): Query<TagTreeQuery>, | |
| 226 | 226 | ) -> Result<impl IntoResponse> { | |
| 227 | 227 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -291,7 +291,7 @@ pub(super) async fn tag_tree( | |||
| 291 | 291 | pub(super) async fn discover( | |
| 292 | 292 | State(state): State<AppState>, | |
| 293 | 293 | session: Session, | |
| 294 | - | MaybeUser(maybe_user): MaybeUser, | |
| 294 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 295 | 295 | Query(query): Query<DiscoverQuery>, | |
| 296 | 296 | ) -> Result<impl IntoResponse> { | |
| 297 | 297 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -488,7 +488,7 @@ pub(super) async fn discover( | |||
| 488 | 488 | #[tracing::instrument(skip_all, name = "discover::discover_results")] | |
| 489 | 489 | pub(super) async fn discover_results( | |
| 490 | 490 | State(state): State<AppState>, | |
| 491 | - | MaybeUser(maybe_user): MaybeUser, | |
| 491 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 492 | 492 | Query(query): Query<DiscoverQuery>, | |
| 493 | 493 | ) -> Result<impl IntoResponse> { | |
| 494 | 494 | let data = fetch_discover_data(&state.db, &query).await?; |
| @@ -8,7 +8,7 @@ use axum::{ | |||
| 8 | 8 | use tower_sessions::Session; | |
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | - | auth::MaybeUser, | |
| 11 | + | auth::MaybeUserUnverified, | |
| 12 | 12 | error::{AppError, Result}, | |
| 13 | 13 | helpers::get_csrf_token, | |
| 14 | 14 | templates::{DocIndexTemplate, DocSection, DocSectionEntry, DocSubsection, DocTemplate}, | |
| @@ -20,7 +20,7 @@ use crate::{ | |||
| 20 | 20 | pub async fn docs_index( | |
| 21 | 21 | State(state): State<AppState>, | |
| 22 | 22 | session: Session, | |
| 23 | - | MaybeUser(maybe_user): MaybeUser, | |
| 23 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 24 | 24 | ) -> Result<impl IntoResponse> { | |
| 25 | 25 | let csrf_token = get_csrf_token(&session).await; | |
| 26 | 26 | ||
| @@ -99,7 +99,7 @@ pub async fn docs_search_index( | |||
| 99 | 99 | pub async fn doc_page( | |
| 100 | 100 | State(state): State<AppState>, | |
| 101 | 101 | session: Session, | |
| 102 | - | MaybeUser(maybe_user): MaybeUser, | |
| 102 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 103 | 103 | Path(slug): Path<String>, | |
| 104 | 104 | ) -> Result<impl IntoResponse> { | |
| 105 | 105 | let page = state.docs.get(&slug).ok_or(AppError::NotFound)?; |
| @@ -14,8 +14,8 @@ use serde::Deserialize; | |||
| 14 | 14 | use tower_sessions::Session; | |
| 15 | 15 | ||
| 16 | 16 | use crate::{ | |
| 17 | - | auth::{hash_password, login_user, track_session, AuthUser, MaybeUser, SessionUser}, | |
| 18 | - | db::{self, Username}, | |
| 17 | + | auth::{hash_password, login_user, track_session, AuthUser, MaybeUserUnverified, SessionUser}, | |
| 18 | + | db::{self}, | |
| 19 | 19 | email, | |
| 20 | 20 | error::{AppError, Result}, | |
| 21 | 21 | helpers::{get_csrf_token, is_htmx_request}, | |
| @@ -38,7 +38,7 @@ pub struct JoinQuery { | |||
| 38 | 38 | #[tracing::instrument(skip_all, name = "join_wizard::page")] | |
| 39 | 39 | pub async fn wizard_page( | |
| 40 | 40 | session: Session, | |
| 41 | - | MaybeUser(maybe_user): MaybeUser, | |
| 41 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 42 | 42 | Query(query): Query<JoinQuery>, | |
| 43 | 43 | ) -> Response { | |
| 44 | 44 | if maybe_user.is_some() { | |
| @@ -55,7 +55,7 @@ pub async fn wizard_page( | |||
| 55 | 55 | /// Form input for account creation (step 1). | |
| 56 | 56 | #[derive(Debug, Deserialize)] | |
| 57 | 57 | pub struct AccountForm { | |
| 58 | - | pub username: Username, | |
| 58 | + | pub username: String, | |
| 59 | 59 | pub email: String, | |
| 60 | 60 | pub password: String, | |
| 61 | 61 | pub invite_code: Option<String>, | |
| @@ -94,6 +94,18 @@ pub async fn step_account_create( | |||
| 94 | 94 | } | |
| 95 | 95 | }; | |
| 96 | 96 | ||
| 97 | + | // Validate username in-handler so field-aware error UX is reachable. If | |
| 98 | + | // `username` were typed as `Username` on `AccountForm`, axum would 400 in | |
| 99 | + | // form extraction and the form would be blanked out before this handler | |
| 100 | + | // ever ran. | |
| 101 | + | let username = match db::Username::new(&form.username) { | |
| 102 | + | Ok(u) => u, | |
| 103 | + | Err(e) => { | |
| 104 | + | let msg = e.to_string(); | |
| 105 | + | return return_error(&msg, &[("username", msg.as_str())]); | |
| 106 | + | } | |
| 107 | + | }; | |
| 108 | + | ||
| 97 | 109 | // Validate and normalize email | |
| 98 | 110 | let email = match db::Email::new(&form.email) { | |
| 99 | 111 | Ok(e) => e, | |
| @@ -104,7 +116,7 @@ pub async fn step_account_create( | |||
| 104 | 116 | }; | |
| 105 | 117 | ||
| 106 | 118 | // Check uniqueness | |
| 107 | - | let username_taken = db::users::get_user_by_username(&state.db, &form.username) | |
| 119 | + | let username_taken = db::users::get_user_by_username(&state.db, &username) | |
| 108 | 120 | .await? | |
| 109 | 121 | .is_some(); | |
| 110 | 122 | let email_taken = db::users::get_user_by_email(&state.db, &email) | |
| @@ -163,7 +175,7 @@ pub async fn step_account_create( | |||
| 163 | 175 | // Hash password and create user | |
| 164 | 176 | let password_hash = hash_password(&form.password)?; | |
| 165 | 177 | let user = | |
| 166 | - | db::users::create_user(&state.db, &form.username, &email, &password_hash).await?; | |
| 178 | + | db::users::create_user(&state.db, &username, &email, &password_hash).await?; | |
| 167 | 179 | ||
| 168 | 180 | // Process invite code (if provided and valid) | |
| 169 | 181 | if let Some(ref code_raw) = form.invite_code { |
| @@ -9,7 +9,7 @@ use serde::Deserialize; | |||
| 9 | 9 | use tower_sessions::Session; | |
| 10 | 10 | ||
| 11 | 11 | use crate::{ | |
| 12 | - | auth::{AuthUser, MaybeUser}, | |
| 12 | + | auth::{AuthUser, MaybeUserUnverified}, | |
| 13 | 13 | constants, | |
| 14 | 14 | db, | |
| 15 | 15 | error::{AppError, Result}, | |
| @@ -30,7 +30,7 @@ pub(super) async fn index( | |||
| 30 | 30 | State(state): State<AppState>, | |
| 31 | 31 | headers: HeaderMap, | |
| 32 | 32 | session: Session, | |
| 33 | - | MaybeUser(maybe_user): MaybeUser, | |
| 33 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 34 | 34 | ) -> Result<Response> { | |
| 35 | 35 | // Check for custom domain — delegate to the custom domain handler | |
| 36 | 36 | if let Some(response) = | |
| @@ -386,7 +386,7 @@ pub(super) async fn checkout_complete() -> impl IntoResponse { | |||
| 386 | 386 | #[tracing::instrument(skip_all, name = "landing::use_cases_page")] | |
| 387 | 387 | pub(super) async fn use_cases_page( | |
| 388 | 388 | session: Session, | |
| 389 | - | MaybeUser(maybe_user): MaybeUser, | |
| 389 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 390 | 390 | ) -> impl IntoResponse { | |
| 391 | 391 | UseCasesTemplate { | |
| 392 | 392 | csrf_token: get_csrf_token(&session).await, | |
| @@ -398,7 +398,7 @@ pub(super) async fn use_cases_page( | |||
| 398 | 398 | #[tracing::instrument(skip_all, name = "landing::team_page")] | |
| 399 | 399 | pub(super) async fn team_page( | |
| 400 | 400 | session: Session, | |
| 401 | - | MaybeUser(maybe_user): MaybeUser, | |
| 401 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 402 | 402 | ) -> impl IntoResponse { | |
| 403 | 403 | TeamTemplate { | |
| 404 | 404 | csrf_token: get_csrf_token(&session).await, | |
| @@ -410,7 +410,7 @@ pub(super) async fn team_page( | |||
| 410 | 410 | #[tracing::instrument(skip_all, name = "landing::policy_page")] | |
| 411 | 411 | pub(super) async fn policy_page( | |
| 412 | 412 | session: Session, | |
| 413 | - | MaybeUser(maybe_user): MaybeUser, | |
| 413 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 414 | 414 | ) -> impl IntoResponse { | |
| 415 | 415 | let csrf_token = get_csrf_token(&session).await; | |
| 416 | 416 | PolicyTemplate { | |
| @@ -431,7 +431,7 @@ pub(super) struct FanPlusQuery { | |||
| 431 | 431 | pub(super) async fn fan_plus_page( | |
| 432 | 432 | State(state): State<AppState>, | |
| 433 | 433 | session: Session, | |
| 434 | - | MaybeUser(maybe_user): MaybeUser, | |
| 434 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 435 | 435 | Query(query): Query<FanPlusQuery>, | |
| 436 | 436 | ) -> Result<impl IntoResponse> { | |
| 437 | 437 | let csrf_token = get_csrf_token(&session).await; |
| @@ -18,7 +18,7 @@ use axum::{ | |||
| 18 | 18 | use tower_sessions::Session; | |
| 19 | 19 | ||
| 20 | 20 | use crate::{ | |
| 21 | - | auth::MaybeUser, | |
| 21 | + | auth::MaybeUserUnverified, | |
| 22 | 22 | constants, | |
| 23 | 23 | db, | |
| 24 | 24 | error::Result, | |
| @@ -96,7 +96,7 @@ pub fn public_routes() -> Router<AppState> { | |||
| 96 | 96 | async fn creators_page( | |
| 97 | 97 | State(state): State<AppState>, | |
| 98 | 98 | session: Session, | |
| 99 | - | MaybeUser(maybe_user): MaybeUser, | |
| 99 | + | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 100 | 100 | ) -> Result<impl IntoResponse> { | |
| 101 | 101 | let csrf_token = get_csrf_token(&session).await; | |
| 102 | 102 |
| @@ -8,7 +8,7 @@ use axum::{ | |||
| 8 | 8 | use serde::Serialize; | |
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | - | auth::MaybeUser, | |
| 11 | + | auth::MaybeUserVerified, | |
| 12 | 12 | db::{self, ContentData, ItemId, VersionId}, | |
| 13 | 13 | error::{AppError, Result, ResultExt}, | |
| 14 | 14 | pricing, | |
| @@ -59,7 +59,7 @@ async fn resolve_content_url( | |||
| 59 | 59 | #[tracing::instrument(skip_all, name = "storage::stream_url", fields(item_id))] | |
| 60 | 60 | pub(super) async fn stream_url( | |
| 61 | 61 | State(state): State<AppState>, | |
| 62 | - | MaybeUser(maybe_user): MaybeUser, | |
| 62 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 63 | 63 | Path(item_id): Path<ItemId>, | |
| 64 | 64 | ) -> Result<impl IntoResponse> { | |
| 65 | 65 | tracing::Span::current().record("item_id", tracing::field::display(&item_id)); | |
| @@ -146,7 +146,7 @@ pub(super) async fn stream_url( | |||
| 146 | 146 | #[tracing::instrument(skip_all, name = "storage::version_download", fields(version_id))] | |
| 147 | 147 | pub(super) async fn version_download( | |
| 148 | 148 | State(state): State<AppState>, | |
| 149 | - | MaybeUser(maybe_user): MaybeUser, | |
| 149 | + | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 150 | 150 | Path(version_id): Path<VersionId>, | |
| 151 | 151 | ) -> Result<impl IntoResponse> { | |
| 152 | 152 | tracing::Span::current().record("version_id", tracing::field::display(&version_id)); |
| @@ -58,7 +58,7 @@ pub struct CancelQuery { | |||
| 58 | 58 | pub(super) async fn checkout_success( | |
| 59 | 59 | State(state): State<AppState>, | |
| 60 | 60 | session: Session, | |
| 61 | - | crate::auth::MaybeUser(maybe_user): crate::auth::MaybeUser, | |
| 61 | + | crate::auth::MaybeUserVerified(maybe_user): crate::auth::MaybeUserVerified, | |
| 62 | 62 | Query(query): Query<SuccessQuery>, | |
| 63 | 63 | ) -> impl IntoResponse { | |
| 64 | 64 | if let Some(session_id) = &query.session_id { |