From 859c441cda011b980799979b6cbcf04f13c56a3a Mon Sep 17 00:00:00 2001 From: Paul van Brenk <5273975+paulvanbrenk@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:50:50 -0500 Subject: [PATCH 1/3] feat: Add email change via Stytch magic link verification Users can now change their email address from the Settings page. The flow sends a magic link to the new address, verifies ownership via a dedicated /confirm-email page, then cleans up the old email from Stytch and syncs to the database. Co-Authored-By: Claude Opus 4.6 (1M context) --- PatchNotes.Api/Routes/UserRoutes.cs | 95 +++++++++++++++ PatchNotes.Api/Stytch/IStytchClient.cs | 22 ++++ PatchNotes.Api/Stytch/StytchClient.cs | 11 ++ patchnotes-web/openapi.json | 25 +++- .../generated/admin-git-hub/admin-git-hub.ts | 4 +- .../admin-git-hub/admin-git-hub.zod.ts | 2 +- .../email-templates/email-templates.ts | 8 +- .../email-templates/email-templates.zod.ts | 2 +- patchnotes-web/src/api/generated/feed/feed.ts | 4 +- .../src/api/generated/feed/feed.zod.ts | 2 +- .../git-hub-search/git-hub-search.ts | 4 +- .../git-hub-search/git-hub-search.zod.ts | 2 +- .../generated/model/addFromGitHubResponse.ts | 2 +- .../api/generated/model/addPackageRequest.ts | 2 +- .../api/generated/model/bulkAddPackageItem.ts | 2 +- .../generated/model/bulkAddPackageResult.ts | 2 +- .../model/bulkAddPackageResultItem.ts | 2 +- .../generated/model/emailPreferencesDto.ts | 2 +- .../api/generated/model/emailTemplateDto.ts | 2 +- .../src/api/generated/model/feedGroupDto.ts | 2 +- .../src/api/generated/model/feedReleaseDto.ts | 2 +- .../api/generated/model/feedResponseDto.ts | 2 +- .../src/api/generated/model/getFeedParams.ts | 2 +- .../model/getPackageSummariesParams.ts | 2 +- .../model/getPackagesByOwnerParams.ts | 2 +- .../api/generated/model/getPackagesParams.ts | 2 +- .../api/generated/model/getReleasesParams.ts | 2 +- .../api/generated/model/getSummariesParams.ts | 2 +- .../model/gitHubRepoSearchResultDto.ts | 2 +- .../src/api/generated/model/index.ts | 2 +- .../api/generated/model/ownerPackageDto.ts | 2 +- .../api/generated/model/packageDetailDto.ts | 2 +- .../generated/model/packageDetailGroupDto.ts | 2 +- .../generated/model/packageDetailInfoDto.ts | 2 +- .../model/packageDetailReleaseDto.ts | 2 +- .../model/packageDetailResponseDto.ts | 2 +- .../src/api/generated/model/packageDto.ts | 2 +- .../api/generated/model/packageReleaseDto.ts | 2 +- .../model/packageReleasePackageDto.ts | 2 +- .../paginatedResponseOfOwnerPackageDto.ts | 2 +- .../model/paginatedResponseOfPackageDto.ts | 2 +- .../src/api/generated/model/releaseDto.ts | 2 +- .../api/generated/model/releasePackageDto.ts | 2 +- .../api/generated/model/releaseSummaryDto.ts | 2 +- .../model/searchGitHubRepositoriesParams.ts | 2 +- .../searchGitHubRepositoriesUserParams.ts | 2 +- .../generated/model/setWatchlistRequest.ts | 2 +- .../generated/model/subscriptionStatusDto.ts | 2 +- .../model/updateEmailPreferencesRequest.ts | 2 +- .../model/updateEmailTemplateRequest.ts | 2 +- .../generated/model/updatePackageRequest.ts | 2 +- .../api/generated/model/updateUserRequest.ts | 2 +- .../src/api/generated/model/userDto.ts | 2 +- .../generated/model/watchlistPackageDto.ts | 2 +- .../src/api/generated/packages/packages.ts | 20 ++-- .../api/generated/packages/packages.zod.ts | 2 +- .../patchnotes-api/patchnotes-api.ts | 6 +- .../src/api/generated/releases/releases.ts | 6 +- .../api/generated/releases/releases.zod.ts | 2 +- .../generated/subscription/subscription.ts | 8 +- .../subscription/subscription.zod.ts | 2 +- .../src/api/generated/summaries/summaries.ts | 6 +- .../api/generated/summaries/summaries.zod.ts | 2 +- .../src/api/generated/users/users.ts | 94 ++++++++++++++- .../src/api/generated/users/users.zod.ts | 13 +- .../src/api/generated/watchlist/watchlist.ts | 12 +- .../api/generated/watchlist/watchlist.zod.ts | 2 +- patchnotes-web/src/pages/ConfirmEmail.tsx | 113 ++++++++++++++++++ patchnotes-web/src/pages/Settings.tsx | 93 +++++++++++++- patchnotes-web/src/routes/confirm-email.tsx | 6 + 70 files changed, 553 insertions(+), 99 deletions(-) create mode 100644 patchnotes-web/src/pages/ConfirmEmail.tsx create mode 100644 patchnotes-web/src/routes/confirm-email.tsx diff --git a/PatchNotes.Api/Routes/UserRoutes.cs b/PatchNotes.Api/Routes/UserRoutes.cs index 0e005aea..00a4f728 100644 --- a/PatchNotes.Api/Routes/UserRoutes.cs +++ b/PatchNotes.Api/Routes/UserRoutes.cs @@ -195,6 +195,101 @@ public static WebApplication MapUserRoutes(this WebApplication app) .Produces(StatusCodes.Status404NotFound) .WithName("UpdateCurrentUser"); + // POST /api/users/me/confirm-email-change - Clean up old emails after magic link verification + group.MapPost("/me/confirm-email-change", async (HttpContext httpContext, PatchNotesDbContext db, IStytchClient stytchClient, ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("PatchNotes.Api.Routes.UserRoutes"); + var stytchUserId = httpContext.Items["StytchUserId"] as string; + var sessionEmail = httpContext.Items["StytchEmail"] as string; + + // Fetch user from Stytch to get the full emails array + StytchUser? stytchUser; + try + { + stytchUser = await stytchClient.GetUserAsync(stytchUserId!); + } + catch (Exception ex) + { + logger.LogError(ex, "Stytch API call failed for user {StytchUserId}", stytchUserId); + return Results.Json(new ApiError("Stytch API call failed"), statusCode: 502); + } + + if (stytchUser == null) + { + return Results.Json(new ApiError("Could not fetch user from Stytch"), statusCode: 502); + } + + // If there's only one email, nothing to clean up + if (stytchUser.Emails.Count <= 1) + { + var existingUser = await db.Users.FirstOrDefaultAsync(u => u.StytchUserId == stytchUserId); + if (existingUser == null) return Results.NotFound(new ApiError("User not found")); + + var session = httpContext.Items["StytchSession"] as StytchSessionResult; + return Results.Ok(new UserDto + { + Id = existingUser.Id, + StytchUserId = existingUser.StytchUserId, + Email = existingUser.Email, + Name = existingUser.Name, + CreatedAt = existingUser.CreatedAt, + LastLoginAt = existingUser.LastLoginAt, + IsPro = existingUser.IsPro || (session?.IsAdmin ?? false), + IsAdmin = session?.IsAdmin ?? false + }); + } + + // The session email is the new email (the one the user just verified via magic link). + // Delete all other emails from Stytch. + var newEmail = sessionEmail ?? stytchUser.Emails.Last().Email; + var emailsToRemove = stytchUser.Emails.Where(e => e.Email != newEmail).ToList(); + + foreach (var oldEmail in emailsToRemove) + { + try + { + await stytchClient.DeleteEmailAsync(oldEmail.EmailId); + logger.LogInformation("Removed old email {EmailId} ({Email}) from Stytch user {StytchUserId}", + oldEmail.EmailId, oldEmail.Email, stytchUserId); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete old email {EmailId} from Stytch user {StytchUserId}", + oldEmail.EmailId, stytchUserId); + return Results.Json(new ApiError("Failed to remove old email from Stytch"), statusCode: 502); + } + } + + // Update our DB with the new email + var user = await db.Users.FirstOrDefaultAsync(u => u.StytchUserId == stytchUserId); + if (user == null) + { + return Results.NotFound(new ApiError("User not found")); + } + + user.Email = newEmail; + await db.SaveChangesAsync(); + + var sess = httpContext.Items["StytchSession"] as StytchSessionResult; + var isAdmin = sess?.IsAdmin ?? false; + + return Results.Ok(new UserDto + { + Id = user.Id, + StytchUserId = user.StytchUserId, + Email = user.Email, + Name = user.Name, + CreatedAt = user.CreatedAt, + LastLoginAt = user.LastLoginAt, + IsPro = user.IsPro || isAdmin, + IsAdmin = isAdmin + }); + }) + .AddEndpointFilterFactory(RouteUtils.CreateAuthFilter()) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .WithName("ConfirmEmailChange"); + // GET /api/users/me/email-preferences - Get current email preferences group.MapGet("/me/email-preferences", async (HttpContext httpContext, PatchNotesDbContext db) => { diff --git a/PatchNotes.Api/Stytch/IStytchClient.cs b/PatchNotes.Api/Stytch/IStytchClient.cs index 09430b37..3a395b8b 100644 --- a/PatchNotes.Api/Stytch/IStytchClient.cs +++ b/PatchNotes.Api/Stytch/IStytchClient.cs @@ -20,6 +20,13 @@ public interface IStytchClient /// Cancellation token. /// The user info, or null if not found. Task GetUserAsync(string userId, CancellationToken cancellationToken = default); + + /// + /// Deletes an email address from a Stytch user by email ID. + /// + /// The Stytch email ID to delete. + /// Cancellation token. + Task DeleteEmailAsync(string emailId, CancellationToken cancellationToken = default); } /// @@ -73,6 +80,11 @@ public class StytchUser /// public string? Email { get; set; } + /// + /// All email addresses associated with the user. + /// + public List Emails { get; set; } = []; + /// /// The user's name, if available. /// @@ -83,3 +95,13 @@ public class StytchUser /// public string? Status { get; set; } } + +/// +/// A Stytch email address with its ID and verification status. +/// +public class StytchEmail +{ + public required string EmailId { get; set; } + public required string Email { get; set; } + public bool Verified { get; set; } +} diff --git a/PatchNotes.Api/Stytch/StytchClient.cs b/PatchNotes.Api/Stytch/StytchClient.cs index 975bb1b9..db4d0bd2 100644 --- a/PatchNotes.Api/Stytch/StytchClient.cs +++ b/PatchNotes.Api/Stytch/StytchClient.cs @@ -68,6 +68,12 @@ public StytchClient(IConfiguration configuration, ILogger logger) { UserId = response.UserId, Email = response.Emails?.FirstOrDefault()?.Email, + Emails = response.Emails?.Select(e => new StytchEmail + { + EmailId = e.EmailId, + Email = e.Email, + Verified = e.Verified + }).ToList() ?? [], Name = name, Status = response.Status }; @@ -78,4 +84,9 @@ public StytchClient(IConfiguration configuration, ILogger logger) return null; } } + + public async Task DeleteEmailAsync(string emailId, CancellationToken cancellationToken = default) + { + await _client.Users.DeleteEmail(new UsersDeleteEmailRequest(emailId)); + } } diff --git a/patchnotes-web/openapi.json b/patchnotes-web/openapi.json index c8ecf6cc..e6bfa151 100644 --- a/patchnotes-web/openapi.json +++ b/patchnotes-web/openapi.json @@ -6,7 +6,7 @@ }, "servers": [ { - "url": "http://localhost:5000/" + "url": "http://localhost:5099/" } ], "paths": { @@ -578,6 +578,29 @@ } } }, + "/api/users/me/confirm-email-change": { + "post": { + "tags": [ + "Users" + ], + "operationId": "ConfirmEmailChange", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, "/api/users/me/email-preferences": { "get": { "tags": [ diff --git a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts index 6e8712c8..cddf8cd7 100644 --- a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts +++ b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.2 🍺 + * Generated by orval v8.4.0 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 @@ -75,7 +75,7 @@ export const searchGitHubRepositories = async (params?: SearchGitHubRepositories } );} - + diff --git a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts index df2fcb19..aac6835a 100644 --- a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts +++ b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.2 🍺 + * Generated by orval v8.4.0 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 diff --git a/patchnotes-web/src/api/generated/email-templates/email-templates.ts b/patchnotes-web/src/api/generated/email-templates/email-templates.ts index 87c39426..dc60fe2e 100644 --- a/patchnotes-web/src/api/generated/email-templates/email-templates.ts +++ b/patchnotes-web/src/api/generated/email-templates/email-templates.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.2 🍺 + * Generated by orval v8.4.0 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 @@ -65,7 +65,7 @@ export const getEmailTemplates = async ( options?: RequestInit): Promise => { return useMutation(getLoginUserMutationOptions(options), queryClient); } + export type confirmEmailChangeResponse200 = { + data: UserDto + status: 200 +} + +export type confirmEmailChangeResponse404 = { + data: void + status: 404 +} + +export type confirmEmailChangeResponseSuccess = (confirmEmailChangeResponse200) & { + headers: Headers; +}; +export type confirmEmailChangeResponseError = (confirmEmailChangeResponse404) & { + headers: Headers; +}; + +export type confirmEmailChangeResponse = (confirmEmailChangeResponseSuccess | confirmEmailChangeResponseError) + +export const getConfirmEmailChangeUrl = () => { + + + + + return `/api/users/me/confirm-email-change` +} + +export const confirmEmailChange = async ( options?: RequestInit): Promise => { + + return customFetch(getConfirmEmailChangeUrl(), + { + ...options, + method: 'POST' + + + } +);} + + + + +export const getConfirmEmailChangeMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,void, TContext> => { + +const mutationKey = ['confirmEmailChange']; +const {mutation: mutationOptions, request: requestOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, request: undefined}; + + + + + const mutationFn: MutationFunction>, void> = () => { + + + return confirmEmailChange(requestOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type ConfirmEmailChangeMutationResult = NonNullable>> + + export type ConfirmEmailChangeMutationError = void + + export const useConfirmEmailChange = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + void, + TContext + > => { + return useMutation(getConfirmEmailChangeMutationOptions(options), queryClient); + } export type getEmailPreferencesResponse200 = { data: EmailPreferencesDto status: 200 @@ -343,7 +425,7 @@ export const getEmailPreferences = async ( options?: RequestInit): Promise { + return new URLSearchParams(window.location.search).get('token') + }, []) + + const [error, setError] = useState( + !token ? 'Invalid email confirmation link' : null + ) + const [done, setDone] = useState(false) + + useEffect(() => { + if (!isInitialized || error || done) return + + // User must be logged in to change their email + if (!user) { + navigate({ to: '/login' }) + return + } + + const confirm = async () => { + if (!token) return + + try { + // Authenticate the magic link token — this appends the new email to the Stytch user + await stytch.magicLinks.authenticate(token, { + session_duration_minutes: 43200, // 30 days + }) + + // Sync session email to our DB + await api.post('/users/login') + + // Remove old email(s) from Stytch and update DB + await api.post('/users/me/confirm-email-change') + + setDone(true) + } catch (err) { + console.error('Email confirmation failed:', err) + setError('Email confirmation failed. The link may have expired.') + } + } + + confirm() + }, [stytch, token, user, isInitialized, navigate, error, done]) + + if (error) { + return ( + +
+
+

+ Email Change Failed +

+

{error}

+ +
+
+
+ ) + } + + if (done) { + return ( + +
+
+

+ Email Updated +

+

+ Your email address has been changed successfully. +

+ +
+
+
+ ) + } + + return ( + +
+
+

+ Confirming email change... +

+

+ Please wait while we verify your new email address. +

+
+
+
+ ) +} diff --git a/patchnotes-web/src/pages/Settings.tsx b/patchnotes-web/src/pages/Settings.tsx index 306ed434..acb9f177 100644 --- a/patchnotes-web/src/pages/Settings.tsx +++ b/patchnotes-web/src/pages/Settings.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { Link, useNavigate } from '@tanstack/react-router' -import { useStytchUser } from '@stytch/react' +import { useStytch, useStytchUser } from '@stytch/react' import { Header, HeaderTitle, Container, Button, Input } from '../components/ui' import { ThemeToggle } from '../components/theme' import { UserMenu } from '../components/auth' @@ -90,6 +90,7 @@ function SettingsSkeleton() { } export function Settings() { + const stytch = useStytch() const { user, isInitialized } = useStytchUser() const navigate = useNavigate() const queryClient = useQueryClient() @@ -110,6 +111,13 @@ export function Settings() { const name = nameOverride ?? serverName + // Email change state + const [showEmailInput, setShowEmailInput] = useState(false) + const [newEmail, setNewEmail] = useState('') + const [emailSending, setEmailSending] = useState(false) + const [emailSent, setEmailSent] = useState(false) + const [emailError, setEmailError] = useState(null) + // Redirect if not authenticated useEffect(() => { if (isInitialized && !user) { @@ -152,6 +160,27 @@ export function Settings() { ) } + const handleSendVerification = async () => { + if (!newEmail.trim()) return + setEmailSending(true) + setEmailError(null) + setEmailSent(false) + + try { + await stytch.magicLinks.email.send(newEmail.trim(), { + login_magic_link_url: `${window.location.origin}/confirm-email`, + }) + setEmailSent(true) + } catch (err) { + console.error('Failed to send verification email:', err) + setEmailError( + err instanceof Error ? err.message : 'Failed to send verification email' + ) + } finally { + setEmailSending(false) + } + } + // Email preferences const isPro = userData?.isPro ?? false @@ -286,6 +315,68 @@ export function Settings() { ) : ( <>
+
+ +

+ {userData?.email ?? 'No email set'} +

+ + {!showEmailInput && !emailSent && ( + + )} + + {showEmailInput && !emailSent && ( +
+ { + setNewEmail(e.target.value) + setEmailError(null) + }} + placeholder="New email address" + /> + + +
+ )} + + {emailSent && ( +

+ Verification email sent to {newEmail}. Check your inbox. +

+ )} + + {emailError != null && ( +

+ {emailError} +

+ )} +
+ Date: Tue, 24 Feb 2026 15:53:58 -0500 Subject: [PATCH 2/3] fix: Add DeleteEmailAsync to mock, regenerate orval with v8.4.2 Co-Authored-By: Claude Opus 4.6 (1M context) --- PatchNotes.Tests/PatchNotesApiFixture.cs | 5 +++++ .../generated/admin-git-hub/admin-git-hub.ts | 4 ++-- .../admin-git-hub/admin-git-hub.zod.ts | 2 +- .../email-templates/email-templates.ts | 8 ++++---- .../email-templates/email-templates.zod.ts | 2 +- patchnotes-web/src/api/generated/feed/feed.ts | 4 ++-- .../src/api/generated/feed/feed.zod.ts | 2 +- .../git-hub-search/git-hub-search.ts | 4 ++-- .../git-hub-search/git-hub-search.zod.ts | 2 +- .../generated/model/addFromGitHubResponse.ts | 2 +- .../api/generated/model/addPackageRequest.ts | 2 +- .../api/generated/model/bulkAddPackageItem.ts | 2 +- .../generated/model/bulkAddPackageResult.ts | 2 +- .../model/bulkAddPackageResultItem.ts | 2 +- .../generated/model/emailPreferencesDto.ts | 2 +- .../api/generated/model/emailTemplateDto.ts | 2 +- .../src/api/generated/model/feedGroupDto.ts | 2 +- .../src/api/generated/model/feedReleaseDto.ts | 2 +- .../api/generated/model/feedResponseDto.ts | 2 +- .../src/api/generated/model/getFeedParams.ts | 2 +- .../model/getPackageSummariesParams.ts | 2 +- .../model/getPackagesByOwnerParams.ts | 2 +- .../api/generated/model/getPackagesParams.ts | 2 +- .../api/generated/model/getReleasesParams.ts | 2 +- .../api/generated/model/getSummariesParams.ts | 2 +- .../model/gitHubRepoSearchResultDto.ts | 2 +- .../src/api/generated/model/index.ts | 2 +- .../api/generated/model/ownerPackageDto.ts | 2 +- .../api/generated/model/packageDetailDto.ts | 2 +- .../generated/model/packageDetailGroupDto.ts | 2 +- .../generated/model/packageDetailInfoDto.ts | 2 +- .../model/packageDetailReleaseDto.ts | 2 +- .../model/packageDetailResponseDto.ts | 2 +- .../src/api/generated/model/packageDto.ts | 2 +- .../api/generated/model/packageReleaseDto.ts | 2 +- .../model/packageReleasePackageDto.ts | 2 +- .../paginatedResponseOfOwnerPackageDto.ts | 2 +- .../model/paginatedResponseOfPackageDto.ts | 2 +- .../src/api/generated/model/releaseDto.ts | 2 +- .../api/generated/model/releasePackageDto.ts | 2 +- .../api/generated/model/releaseSummaryDto.ts | 2 +- .../model/searchGitHubRepositoriesParams.ts | 2 +- .../searchGitHubRepositoriesUserParams.ts | 2 +- .../generated/model/setWatchlistRequest.ts | 2 +- .../generated/model/subscriptionStatusDto.ts | 2 +- .../model/updateEmailPreferencesRequest.ts | 2 +- .../model/updateEmailTemplateRequest.ts | 2 +- .../generated/model/updatePackageRequest.ts | 2 +- .../api/generated/model/updateUserRequest.ts | 2 +- .../src/api/generated/model/userDto.ts | 2 +- .../generated/model/watchlistPackageDto.ts | 2 +- .../src/api/generated/packages/packages.ts | 20 +++++++++---------- .../api/generated/packages/packages.zod.ts | 2 +- .../patchnotes-api/patchnotes-api.ts | 6 +++--- .../src/api/generated/releases/releases.ts | 6 +++--- .../api/generated/releases/releases.zod.ts | 2 +- .../generated/subscription/subscription.ts | 8 ++++---- .../subscription/subscription.zod.ts | 2 +- .../src/api/generated/summaries/summaries.ts | 6 +++--- .../api/generated/summaries/summaries.zod.ts | 2 +- .../src/api/generated/users/users.ts | 14 ++++++------- .../src/api/generated/users/users.zod.ts | 2 +- .../src/api/generated/watchlist/watchlist.ts | 12 +++++------ .../api/generated/watchlist/watchlist.zod.ts | 2 +- 64 files changed, 103 insertions(+), 98 deletions(-) diff --git a/PatchNotes.Tests/PatchNotesApiFixture.cs b/PatchNotes.Tests/PatchNotesApiFixture.cs index 79d8f735..a2d0d639 100644 --- a/PatchNotes.Tests/PatchNotesApiFixture.cs +++ b/PatchNotes.Tests/PatchNotesApiFixture.cs @@ -290,6 +290,11 @@ public void RegisterSession(string sessionToken, string userId, string email, Li _users.TryGetValue(userId, out var user); return Task.FromResult(user); } + + public Task DeleteEmailAsync(string emailId, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } } /// diff --git a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts index cddf8cd7..6e8712c8 100644 --- a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts +++ b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.0 🍺 + * Generated by orval v8.4.2 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 @@ -75,7 +75,7 @@ export const searchGitHubRepositories = async (params?: SearchGitHubRepositories } );} - + diff --git a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts index aac6835a..df2fcb19 100644 --- a/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts +++ b/patchnotes-web/src/api/generated/admin-git-hub/admin-git-hub.zod.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.0 🍺 + * Generated by orval v8.4.2 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 diff --git a/patchnotes-web/src/api/generated/email-templates/email-templates.ts b/patchnotes-web/src/api/generated/email-templates/email-templates.ts index dc60fe2e..87c39426 100644 --- a/patchnotes-web/src/api/generated/email-templates/email-templates.ts +++ b/patchnotes-web/src/api/generated/email-templates/email-templates.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.4.0 🍺 + * Generated by orval v8.4.2 🍺 * Do not edit manually. * PatchNotes.Api | v1 * OpenAPI spec version: 1.0.0 @@ -65,7 +65,7 @@ export const getEmailTemplates = async ( options?: RequestInit): Promise Date: Tue, 24 Feb 2026 15:56:57 -0500 Subject: [PATCH 3/3] fix: Regenerate route tree to include /confirm-email Co-Authored-By: Claude Opus 4.6 (1M context) --- patchnotes-web/src/routeTree.gen.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/patchnotes-web/src/routeTree.gen.ts b/patchnotes-web/src/routeTree.gen.ts index 4444a3b7..8e75232b 100644 --- a/patchnotes-web/src/routeTree.gen.ts +++ b/patchnotes-web/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as SettingsRouteImport } from './routes/settings' import { Route as PrivacyRouteImport } from './routes/privacy' import { Route as PricingRouteImport } from './routes/pricing' import { Route as LoginRouteImport } from './routes/login' +import { Route as ConfirmEmailRouteImport } from './routes/confirm-email' import { Route as AuthenticateRouteImport } from './routes/authenticate' import { Route as AdminRouteImport } from './routes/admin' import { Route as AboutRouteImport } from './routes/about' @@ -61,6 +62,11 @@ const LoginRoute = LoginRouteImport.update({ path: '/login', getParentRoute: () => rootRouteImport, } as any) +const ConfirmEmailRoute = ConfirmEmailRouteImport.update({ + id: '/confirm-email', + path: '/confirm-email', + getParentRoute: () => rootRouteImport, +} as any) const AuthenticateRoute = AuthenticateRouteImport.update({ id: '/authenticate', path: '/authenticate', @@ -116,6 +122,7 @@ export interface FileRoutesByFullPath { '/about': typeof AboutRoute '/admin': typeof AdminRoute '/authenticate': typeof AuthenticateRoute + '/confirm-email': typeof ConfirmEmailRoute '/login': typeof LoginRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute @@ -134,6 +141,7 @@ export interface FileRoutesByTo { '/about': typeof AboutRoute '/admin': typeof AdminRoute '/authenticate': typeof AuthenticateRoute + '/confirm-email': typeof ConfirmEmailRoute '/login': typeof LoginRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute @@ -152,6 +160,7 @@ export interface FileRoutesById { '/about': typeof AboutRoute '/admin': typeof AdminRoute '/authenticate': typeof AuthenticateRoute + '/confirm-email': typeof ConfirmEmailRoute '/login': typeof LoginRoute '/pricing': typeof PricingRoute '/privacy': typeof PrivacyRoute @@ -172,6 +181,7 @@ export interface FileRouteTypes { | '/about' | '/admin' | '/authenticate' + | '/confirm-email' | '/login' | '/pricing' | '/privacy' @@ -190,6 +200,7 @@ export interface FileRouteTypes { | '/about' | '/admin' | '/authenticate' + | '/confirm-email' | '/login' | '/pricing' | '/privacy' @@ -207,6 +218,7 @@ export interface FileRouteTypes { | '/about' | '/admin' | '/authenticate' + | '/confirm-email' | '/login' | '/pricing' | '/privacy' @@ -226,6 +238,7 @@ export interface RootRouteChildren { AboutRoute: typeof AboutRoute AdminRoute: typeof AdminRoute AuthenticateRoute: typeof AuthenticateRoute + ConfirmEmailRoute: typeof ConfirmEmailRoute LoginRoute: typeof LoginRoute PricingRoute: typeof PricingRoute PrivacyRoute: typeof PrivacyRoute @@ -289,6 +302,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } + '/confirm-email': { + id: '/confirm-email' + path: '/confirm-email' + fullPath: '/confirm-email' + preLoaderRoute: typeof ConfirmEmailRouteImport + parentRoute: typeof rootRouteImport + } '/authenticate': { id: '/authenticate' path: '/authenticate' @@ -374,6 +394,7 @@ const rootRouteChildren: RootRouteChildren = { AboutRoute: AboutRoute, AdminRoute: AdminRoute, AuthenticateRoute: AuthenticateRoute, + ConfirmEmailRoute: ConfirmEmailRoute, LoginRoute: LoginRoute, PricingRoute: PricingRoute, PrivacyRoute: PrivacyRoute,