Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down
14 changes: 3 additions & 11 deletions convex/auth.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -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<string, Value> & {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;
Expand All @@ -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;
},

Expand Down
3 changes: 2 additions & 1 deletion convex/eventStaff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
76 changes: 76 additions & 0 deletions convex/migrations.ts
Original file line number Diff line number Diff line change
@@ -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};
},
});
3 changes: 2 additions & 1 deletion convex/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`};
Expand Down
Loading