Skip to main content

max / makenotwork

server: audit MaybeUser sites; introduce MaybeUserVerified Rename MaybeUser → MaybeUserUnverified to make the lack of session revocation check visible at every call site. Add MaybeUserVerified extractor that runs touch_session (cached) and refreshes suspended / tier / can_create_projects flags during extraction, matching AuthUser's guarantees for the optional-login case. Convert identity-gated handlers to MaybeUserVerified: - oauth.rs authorize_{get,post} — grant issuance; deletes 40-line inline touch_session block in POST, keeps explicit tracking-ID gate so legacy untracked sessions still must re-auth for grants - storage/downloads.rs stream_url, version_download — paid stream/ download access checks - pages/public/content/library.rs library_page — paid content reader - pages/public/content/project.rs project_page — project paywall - pages/public/content/mod.rs receipt_page, collection_page — buyer/seller-only transactions; private collection visibility - stripe/checkout/mod.rs checkout_success — cart-queue checkout - git_issues/issues.rs issue_list, issue_detail — private repo visibility via resolve_repo Public read-only surfaces (blog, docs, discover, landing, raw git browsing, store item_page, user profile, purchase_page, join wizard) stay on MaybeUserUnverified per the documented happy path.
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-25 23:31 UTC
Commit: 2a7414caa6ef33a51129e3284f78dc889adc5055
Parent: 87e3e66
18 files changed, +183 insertions, -115 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 @@
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>);
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>);
207 220
208 - impl<S> FromRequestParts<S> for MaybeUser
221 + impl<S> FromRequestParts<S> for MaybeUserUnverified
209 222 where
210 223 S: Send + Sync,
211 224 {
@@ -222,7 +235,83 @@
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 @@
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 @@
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();
@@ -18,10 +18,10 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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?;
@@ -9,7 +9,7 @@
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 @@
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 @@
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 @@
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 @@
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;
@@ -8,7 +8,7 @@
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 @@
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 @@
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));
@@ -8,7 +8,7 @@
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 @@
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 @@
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 @@
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?;