diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index ee487de..c54ab50 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -17,6 +17,7 @@ import type * as emailVerification from "../emailVerification.js"; import type * as eventStaff from "../eventStaff.js"; import type * as events from "../events.js"; import type * as http from "../http.js"; +import type * as migrations from "../migrations.js"; import type * as payments from "../payments.js"; import type * as platformPricing from "../platformPricing.js"; import type * as roles from "../roles.js"; @@ -39,6 +40,7 @@ declare const fullApi: ApiFromModules<{ eventStaff: typeof eventStaff; events: typeof events; http: typeof http; + migrations: typeof migrations; payments: typeof payments; platformPricing: typeof platformPricing; roles: typeof roles; diff --git a/convex/auth.ts b/convex/auth.ts index db997f9..b1eaabd 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,7 +1,6 @@ import {convexAuth} from '@convex-dev/auth/server'; import {Password} from '@convex-dev/auth/providers/Password'; import {ConvexError, type Value} from 'convex/values'; -import {internal} from './_generated/api'; export const {auth, signIn, signOut, store, isAuthenticated} = convexAuth({ providers: [ @@ -14,8 +13,8 @@ export const {auth, signIn, signOut, store, isAuthenticated} = convexAuth({ // not a valid Convex Value and would break the index-signature check. profile(params) { const result: Record & {email: string} = { - email: params.email as string, - status: 'pending_verification', + email: (params.email as string).toLowerCase().trim(), + status: 'active', createdAt: 0, }; if (typeof params.name === 'string') result.name = params.name; @@ -36,17 +35,10 @@ export const {auth, signIn, signOut, store, isAuthenticated} = convexAuth({ const userId = await ctx.db.insert('users', { email: profile.email, name: profile.name, - status: 'pending_verification', + status: 'active', createdAt: Date.now(), }); - // Fire-and-forget: send verification email via Node.js action. - await ctx.scheduler.runAfter( - 0, - internal.emailVerification.sendVerificationEmail, - {userId}, - ); - return userId; }, diff --git a/convex/eventStaff.ts b/convex/eventStaff.ts index 189f012..60e200c 100644 --- a/convex/eventStaff.ts +++ b/convex/eventStaff.ts @@ -168,9 +168,10 @@ export const addStaff = mutation({ } // Look up the target user by email + const normEmail = args.email.toLowerCase().trim(); const target = await ctx.db .query('users') - .withIndex('by_email', q => q.eq('email', args.email)) + .withIndex('by_email', q => q.eq('email', normEmail)) .unique(); if (!target) throw new Error('No user found with that email address'); if (target._id === event.ownerId) { diff --git a/convex/migrations.ts b/convex/migrations.ts new file mode 100644 index 0000000..471ceb3 --- /dev/null +++ b/convex/migrations.ts @@ -0,0 +1,76 @@ +import {internalMutation} from './_generated/server'; + +/** + * One-time migration: lowercase all emails in the `users` table and the + * `authAccounts` table so that case-insensitive auth works for existing accounts. + * + * Run once via: npx convex run migrations:normalizeEmails + */ +export const normalizeEmails = internalMutation({ + args: {}, + handler: async ctx => { + // 1. Normalize users.email + const users = await ctx.db.query('users').collect(); + let usersPatched = 0; + for (const user of users) { + const normalized = user.email.toLowerCase().trim(); + if (normalized !== user.email) { + await ctx.db.patch(user._id, {email: normalized}); + usersPatched++; + } + } + + // 2. Normalize authAccounts.providerAccountId (and emailVerified) for the + // Password provider — that is where the email is used as the account key. + const accounts = await ctx.db.query('authAccounts').collect(); + let accountsPatched = 0; + for (const account of accounts) { + if (account.provider !== 'password') continue; + + const normalizedId = account.providerAccountId.toLowerCase().trim(); + const normalizedVerified = + account.emailVerified != null + ? account.emailVerified.toLowerCase().trim() + : undefined; + + const needsPatch = + normalizedId !== account.providerAccountId || + (account.emailVerified != null && + normalizedVerified !== account.emailVerified); + + if (needsPatch) { + await ctx.db.patch(account._id, { + providerAccountId: normalizedId, + ...(account.emailVerified != null + ? {emailVerified: normalizedVerified} + : {}), + }); + accountsPatched++; + } + } + + return {usersPatched, accountsPatched}; + }, +}); + +/** + * One-time migration: activate all accounts that are still pending_verification. + * Use while email verification is not yet operational. + * + * Run once via: npx convex run migrations:activateAllUsers + */ +export const activateAllUsers = internalMutation({ + args: {}, + handler: async ctx => { + const users = await ctx.db + .query('users') + .filter(q => q.eq(q.field('status'), 'pending_verification')) + .collect(); + + for (const user of users) { + await ctx.db.patch(user._id, {status: 'active'}); + } + + return {activated: users.length}; + }, +}); diff --git a/convex/roles.ts b/convex/roles.ts index 3e3204b..15cc66f 100644 --- a/convex/roles.ts +++ b/convex/roles.ts @@ -118,9 +118,10 @@ export const bootstrapSuperAdmin = mutation({ } // Find the target user by email + const normEmail = args.email.toLowerCase().trim(); const user = await ctx.db .query('users') - .withIndex('by_email', q => q.eq('email', args.email)) + .withIndex('by_email', q => q.eq('email', normEmail)) .unique(); if (!user) { return {ok: false, message: `No user found with email "${args.email}". Register first, then run this again.`};