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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
118
server/src/scripts/testEmailOptional.ts
Normal file
118
server/src/scripts/testEmailOptional.ts
Normal 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();
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user