feat: implement email-optional authentication

- Make email field optional in database schema
- Update user creation to handle null emails
- Enhance notification service to gracefully skip email for users without email addresses
- Add validation in email service for null/empty emails
- Create comprehensive documentation for Clerk configuration
- Add test script for email-optional functionality
- Update all user creation points (userService, eventController, reportController)

Users can now sign up with just username and password. Email notifications
are gracefully skipped for users without email addresses, while in-app
notifications continue to work for all users.

Closes: Email-optional authentication requirement
This commit is contained in:
2025-11-18 18:02:10 +00:00
parent eed129347b
commit e2ff6f04f3
13 changed files with 1390 additions and 7 deletions

View File

@@ -23,7 +23,7 @@ async function findOrCreateUser(clerkId: string): Promise<string> {
user = await prisma.user.create({
data: {
clerkId,
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
email: clerkUser.emailAddresses[0]?.emailAddress || null,
username: clerkUser.username || `user_${clerkId.substring(0, 8)}`,
firstName: clerkUser.firstName,
lastName: clerkUser.lastName,

View File

@@ -29,7 +29,7 @@ export class ReportController {
user = await prisma.user.create({
data: {
clerkId,
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
email: clerkUser.emailAddresses[0]?.emailAddress || null,
username: defaultUsername,
firstName: clerkUser.firstName,
lastName: clerkUser.lastName,

View File

@@ -0,0 +1,118 @@
/**
* Test script for email-optional functionality
*
* This script tests:
* 1. Creating users without email addresses
* 2. Notification delivery for users without email
* 3. Email service handling of null emails
*
* Run with: npx ts-node src/scripts/testEmailOptional.ts
*/
import prisma from '../config/database';
import { notificationService } from '../services/notificationService';
import { emailService } from '../services/emailService';
async function testEmailOptional() {
console.log('🧪 Testing Email-Optional Functionality\n');
try {
// Test 1: Create a test user without email
console.log('1️⃣ Creating test user without email...');
const testUser = await prisma.user.create({
data: {
clerkId: `test_${Date.now()}`,
email: null, // No email
username: `testuser_${Date.now()}`,
firstName: 'Test',
lastName: 'User',
notificationEmail: true, // Wants email notifications but has no email
notificationInApp: true,
},
});
console.log(`✅ Created user: ${testUser.username} (ID: ${testUser.id})`);
console.log(` Email: ${testUser.email || 'NULL'}`);
console.log(` Notification preferences: email=${testUser.notificationEmail}, inApp=${testUser.notificationInApp}\n`);
// Test 2: Create a notification for this user
console.log('2️⃣ Creating notification for user without email...');
const notification = await notificationService.createNotification({
userId: testUser.id,
type: 'new_report',
title: 'Test Notification',
message: 'This is a test notification for a user without email.',
});
console.log(`✅ Created notification: ${notification.id}`);
console.log(` Email sent: ${notification.emailSent}\n`);
// Test 3: Try to send email notification (should be skipped gracefully)
console.log('3️⃣ Attempting to send email notification...');
const result = await emailService.sendNotificationEmail(
testUser.email || '',
testUser.firstName,
'Test Email',
'This should not be sent because user has no email.'
);
console.log(`✅ Email send result: ${result}`);
console.log(` Expected: false (no email address)\n`);
// Test 4: Create a user WITH email for comparison
console.log('4️⃣ Creating test user WITH email...');
const testUserWithEmail = await prisma.user.create({
data: {
clerkId: `test_email_${Date.now()}`,
email: `test_${Date.now()}@example.com`,
username: `testuser_email_${Date.now()}`,
firstName: 'Test',
lastName: 'WithEmail',
notificationEmail: true,
notificationInApp: true,
},
});
console.log(`✅ Created user: ${testUserWithEmail.username} (ID: ${testUserWithEmail.id})`);
console.log(` Email: ${testUserWithEmail.email}\n`);
// Test 5: Query users for email notifications
console.log('5️⃣ Querying users eligible for email notifications...');
const usersForEmail = await prisma.user.findMany({
where: {
notificationEmail: true,
email: { not: null },
},
select: {
id: true,
username: true,
email: true,
},
});
console.log(`✅ Found ${usersForEmail.length} users eligible for email notifications`);
console.log(` Users with email: ${usersForEmail.filter(u => u.email).length}`);
console.log(` Users without email: ${usersForEmail.filter(u => !u.email).length}\n`);
// Cleanup
console.log('🧹 Cleaning up test data...');
await prisma.notification.deleteMany({
where: { userId: { in: [testUser.id, testUserWithEmail.id] } },
});
await prisma.user.deleteMany({
where: { id: { in: [testUser.id, testUserWithEmail.id] } },
});
console.log('✅ Cleanup complete\n');
console.log('✅ All tests passed!\n');
console.log('Summary:');
console.log('- Users can be created without email addresses');
console.log('- Notifications are created for users without email');
console.log('- Email sending is gracefully skipped for users without email');
console.log('- Database queries correctly filter users by email presence');
} catch (error: any) {
console.error('❌ Test failed:', error.message);
console.error(error);
} finally {
await prisma.$disconnect();
}
}
// Run the test
testEmailOptional();

View File

@@ -76,6 +76,12 @@ export class EmailService {
relatedType?: 'report' | 'event',
relatedId?: string
): Promise<boolean> {
// Validate email address exists
if (!to || to.trim() === '') {
console.log('[Email] Skipping notification - no email address provided');
return false;
}
const greeting = firstName ? `Hi ${firstName}` : 'Hi';
const text = `

View File

@@ -416,7 +416,8 @@ export class NotificationService {
}
/**
* Send email notifications using Mailgun
* Send email notifications using email service
* Gracefully handles users without email addresses
*/
private async sendEmailNotifications(notifications: any[]) {
// Get user emails for notifications
@@ -426,6 +427,7 @@ export class NotificationService {
where: {
id: { in: userIds },
notificationEmail: true,
email: { not: null }, // Only get users with email addresses
},
select: {
id: true,
@@ -438,10 +440,21 @@ export class NotificationService {
for (const notification of notifications) {
const user = userMap.get(notification.userId);
if (!user) continue;
if (!user) {
// User doesn't have email or has email notifications disabled
console.log(`[Notification] Skipping email for user ${notification.userId} (no email address or disabled)`);
continue;
}
if (!user.email) {
// Extra safety check - should not happen due to query filter
console.log(`[Notification] Skipping email for user ${user.id} (email is null)`);
continue;
}
try {
// Send email using Mailgun
// Send email using email service
const success = await emailService.sendNotificationEmail(
user.email,
user.firstName,

View File

@@ -43,7 +43,7 @@ export class UserService {
user = await prisma.user.create({
data: {
clerkId,
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
email: clerkUser.emailAddresses[0]?.emailAddress || null,
username: defaultUsername,
firstName: clerkUser.firstName,
lastName: clerkUser.lastName,