From f02c3fe293d17c9279230f8a575cc8f1c57bf2c4 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 10 Aug 2026 20:10:02 +0200 Subject: [PATCH] Add a `registrationToken` GraphQL query for anonymous token lookup The registration form needs to know, before the user submits anything, whether the code they were handed is usable and which identity it pins. The query only returns whether the token is valid and the username and email it was issued for, never the admin-only details. --- crates/handlers/src/graphql/query/user.rs | 41 +++++++++++++++++++++++ frontend/schema.graphql | 29 ++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/crates/handlers/src/graphql/query/user.rs b/crates/handlers/src/graphql/query/user.rs index adf8b75dc..98d575560 100644 --- a/crates/handlers/src/graphql/query/user.rs +++ b/crates/handlers/src/graphql/query/user.rs @@ -28,6 +28,18 @@ enum UsernameUnavailableReason { Invalid, } +/// Public information about a registration token, returned by anonymous lookup. +/// Does not expose admin details like usage counts or expiration. +#[derive(SimpleObject)] +struct RegistrationTokenInfo { + /// Whether the token is currently valid for registration. + valid: bool, + /// A username imposed by this token, if any. + username: Option, + /// An email imposed by this token, if any. + email: Option, +} + /// The result of a username availability check. #[derive(SimpleObject)] struct UsernameAvailability { @@ -168,6 +180,35 @@ impl UserQuery { }) } + /// Look up a registration token by its string value. + /// + /// Returns public information about the token (validity, imposed + /// username/email). Returns `null` if the token does not exist. + /// + /// This query is accessible to anonymous users, as it is used during + /// the registration flow. + async fn registration_token( + &self, + ctx: &Context<'_>, + token: String, + ) -> Result, async_graphql::Error> { + let state = ctx.state(); + let mut repo = state.repository().await?; + + let registration_token = repo.user_registration_token().find_by_token(&token).await?; + repo.cancel().await?; + + let Some(registration_token) = registration_token else { + return Ok(None); + }; + + Ok(Some(RegistrationTokenInfo { + valid: registration_token.is_valid(state.clock().now()), + username: registration_token.username, + email: registration_token.email, + })) + } + /// Fetch a user by its username. async fn user_by_username( &self, diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 9aca097e2..7a16b0157 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -1194,6 +1194,16 @@ type Query { """ usernameAvailable(username: String!): UsernameAvailability! """ + Look up a registration token by its string value. + + Returns public information about the token (validity, imposed + username/email). Returns `null` if the token does not exist. + + This query is accessible to anonymous users, as it is used during + the registration flow. + """ + registrationToken(token: String!): RegistrationTokenInfo + """ Fetch a user by its username. """ userByUsername(username: String!): User @@ -1271,6 +1281,25 @@ type Query { viewerSession: ViewerSession! } +""" +Public information about a registration token, returned by anonymous lookup. +Does not expose admin details like usage counts or expiration. +""" +type RegistrationTokenInfo { + """ + Whether the token is currently valid for registration. + """ + valid: Boolean! + """ + A username imposed by this token, if any. + """ + username: String + """ + An email imposed by this token, if any. + """ + email: String +} + """ The input for the `removeEmail` mutation """