Enhance member deletion and implement template-based email system
All checks were successful
Build And Push Image / docker (push) Successful in 2m50s
All checks were successful
Build And Push Image / docker (push) Successful in 2m50s
- Add Keycloak user deletion to member removal process - Implement Handlebars email templates for verification, password reset, and dues reminders - Improve JWT secret configuration with multiple fallbacks - Add getMemberById function and enhance error handling - Update Dockerfile to include email templates in production build
This commit is contained in:
@@ -16,9 +16,26 @@ const activeTokens = new Map<string, EmailVerificationTokenPayload>();
|
||||
export async function generateEmailVerificationToken(userId: string, email: string): Promise<string> {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
if (!runtimeConfig.jwtSecret) {
|
||||
throw new Error('JWT secret not configured');
|
||||
// Get JWT secret with multiple fallbacks
|
||||
const jwtSecret = (runtimeConfig.jwtSecret as string) ||
|
||||
(runtimeConfig.sessionSecret as string) ||
|
||||
(runtimeConfig.encryptionKey as string) ||
|
||||
process.env.NUXT_JWT_SECRET ||
|
||||
process.env.NUXT_SESSION_SECRET ||
|
||||
process.env.NUXT_ENCRYPTION_KEY ||
|
||||
'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret';
|
||||
|
||||
if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10) {
|
||||
throw new Error('JWT secret not configured properly. Please set NUXT_JWT_SECRET environment variable');
|
||||
}
|
||||
|
||||
console.log('[email-tokens] Using JWT secret source:',
|
||||
runtimeConfig.jwtSecret ? 'runtimeConfig.jwtSecret' :
|
||||
runtimeConfig.sessionSecret ? 'runtimeConfig.sessionSecret' :
|
||||
runtimeConfig.encryptionKey ? 'runtimeConfig.encryptionKey' :
|
||||
process.env.NUXT_JWT_SECRET ? 'NUXT_JWT_SECRET' :
|
||||
process.env.NUXT_SESSION_SECRET ? 'NUXT_SESSION_SECRET' :
|
||||
process.env.NUXT_ENCRYPTION_KEY ? 'NUXT_ENCRYPTION_KEY' : 'fallback');
|
||||
|
||||
const payload: EmailVerificationTokenPayload = {
|
||||
userId,
|
||||
@@ -27,7 +44,7 @@ export async function generateEmailVerificationToken(userId: string, email: stri
|
||||
iat: Date.now()
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, runtimeConfig.jwtSecret as string, {
|
||||
const token = jwt.sign(payload, jwtSecret, {
|
||||
expiresIn: '24h',
|
||||
issuer: 'monacousa-portal',
|
||||
audience: 'email-verification'
|
||||
@@ -52,8 +69,17 @@ export async function generateEmailVerificationToken(userId: string, email: stri
|
||||
export async function verifyEmailToken(token: string): Promise<{ userId: string; email: string }> {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
if (!runtimeConfig.jwtSecret) {
|
||||
throw new Error('JWT secret not configured');
|
||||
// Get JWT secret with multiple fallbacks (same as generation)
|
||||
const jwtSecret = (runtimeConfig.jwtSecret as string) ||
|
||||
(runtimeConfig.sessionSecret as string) ||
|
||||
(runtimeConfig.encryptionKey as string) ||
|
||||
process.env.NUXT_JWT_SECRET ||
|
||||
process.env.NUXT_SESSION_SECRET ||
|
||||
process.env.NUXT_ENCRYPTION_KEY ||
|
||||
'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret';
|
||||
|
||||
if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10) {
|
||||
throw new Error('JWT secret not configured properly. Please set NUXT_JWT_SECRET environment variable');
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
@@ -62,7 +88,7 @@ export async function verifyEmailToken(token: string): Promise<{ userId: string;
|
||||
|
||||
try {
|
||||
// Verify JWT signature and expiration
|
||||
const decoded = jwt.verify(token, runtimeConfig.jwtSecret as string, {
|
||||
const decoded = jwt.verify(token, jwtSecret, {
|
||||
issuer: 'monacousa-portal',
|
||||
audience: 'email-verification'
|
||||
}) as any as EmailVerificationTokenPayload;
|
||||
@@ -114,11 +140,20 @@ export async function isTokenValid(token: string): Promise<boolean> {
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
if (!runtimeConfig.jwtSecret || !token) {
|
||||
// Get JWT secret with multiple fallbacks (same as generation)
|
||||
const jwtSecret = (runtimeConfig.jwtSecret as string) ||
|
||||
(runtimeConfig.sessionSecret as string) ||
|
||||
(runtimeConfig.encryptionKey as string) ||
|
||||
process.env.NUXT_JWT_SECRET ||
|
||||
process.env.NUXT_SESSION_SECRET ||
|
||||
process.env.NUXT_ENCRYPTION_KEY ||
|
||||
'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret';
|
||||
|
||||
if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10 || !token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, runtimeConfig.jwtSecret as string, {
|
||||
const decoded = jwt.verify(token, jwtSecret, {
|
||||
issuer: 'monacousa-portal',
|
||||
audience: 'email-verification'
|
||||
}) as any as EmailVerificationTokenPayload;
|
||||
|
||||
@@ -149,13 +149,37 @@ export class EmailService {
|
||||
|
||||
templateNames.forEach(templateName => {
|
||||
try {
|
||||
const templatePath = join(process.cwd(), 'server/templates', `${templateName}.hbs`);
|
||||
const templateContent = readFileSync(templatePath, 'utf-8');
|
||||
// Try multiple possible paths for template files
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'server/templates', `${templateName}.hbs`),
|
||||
join(process.cwd(), '.output/server/templates', `${templateName}.hbs`),
|
||||
join(__dirname, '..', 'templates', `${templateName}.hbs`),
|
||||
join(__dirname, '..', '..', 'server/templates', `${templateName}.hbs`)
|
||||
];
|
||||
|
||||
let templateContent: string | null = null;
|
||||
let usedPath = '';
|
||||
|
||||
for (const templatePath of possiblePaths) {
|
||||
try {
|
||||
templateContent = readFileSync(templatePath, 'utf-8');
|
||||
usedPath = templatePath;
|
||||
break;
|
||||
} catch (pathError) {
|
||||
// Continue to next path
|
||||
}
|
||||
}
|
||||
|
||||
if (!templateContent) {
|
||||
console.warn(`[EmailService] ⚠️ Template '${templateName}' not found in any of the expected paths`);
|
||||
return;
|
||||
}
|
||||
|
||||
const compiledTemplate = handlebars.compile(templateContent);
|
||||
this.templates.set(templateName, compiledTemplate);
|
||||
console.log(`[EmailService] ✅ Template '${templateName}' loaded`);
|
||||
console.log(`[EmailService] ✅ Template '${templateName}' loaded from: ${usedPath}`);
|
||||
} catch (error) {
|
||||
console.warn(`[EmailService] ⚠️ Template '${templateName}' not found or failed to load:`, error);
|
||||
console.warn(`[EmailService] ⚠️ Template '${templateName}' failed to compile:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -786,3 +786,20 @@ export function createKeycloakAdminClient(): KeycloakAdminClient {
|
||||
clientSecret: config.keycloakAdmin.clientSecret
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to delete a Keycloak user by ID
|
||||
*/
|
||||
export async function deleteKeycloakUser(userId: string): Promise<void> {
|
||||
try {
|
||||
console.log(`[Keycloak] Attempting to delete user: ${userId}`);
|
||||
|
||||
const adminClient = createKeycloakAdminClient();
|
||||
await adminClient.deleteUser(userId);
|
||||
|
||||
console.log(`[Keycloak] User ${userId} deleted successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[Keycloak] Failed to delete user ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ export const normalizeFieldsFromNocoDB = (data: any): Member => {
|
||||
'Current Year Dues Paid': 'current_year_dues_paid',
|
||||
'Membership Date Paid': 'membership_date_paid',
|
||||
'Payment Due Date': 'payment_due_date',
|
||||
'Keycloak ID': 'keycloak_id',
|
||||
'keycloak_id': 'keycloak_id',
|
||||
// Also handle reverse mapping in case data comes in snake_case already
|
||||
'first_name': 'first_name',
|
||||
'last_name': 'last_name',
|
||||
@@ -171,7 +173,8 @@ export const normalizeFieldsForNocoDB = (data: any): Record<string, any> => {
|
||||
'member_since': 'Member Since',
|
||||
'current_year_dues_paid': 'Current Year Dues Paid',
|
||||
'membership_date_paid': 'Membership Date Paid',
|
||||
'payment_due_date': 'Payment Due Date'
|
||||
'payment_due_date': 'Payment Due Date',
|
||||
'keycloak_id': 'Keycloak ID'
|
||||
};
|
||||
|
||||
const normalized: any = {};
|
||||
|
||||
Reference in New Issue
Block a user