import { z } from "zod"; import prisma from "~/server/utils/prisma"; // Input validation schema for updating templates const updateTemplateSchema = z.object({ title: z.string().min(1, "Title is required").max(100, "Title must be less than 100 characters"), description: z.string().optional(), subject: z.string().min(1, "Subject is required").max(255, "Subject must be less than 255 characters"), preheader: z.string().max(255, "Preheader must be less than 255 characters").optional(), category: z.string().min(1, "Category is required"), channels: z.array(z.enum(["email", "sms", "push"])).min(1, "At least one channel is required"), status: z.enum(["Draft", "Active", "Archived"]).default("Draft"), version: z.string().default("1.0"), content: z.string().min(1, "Content is required"), tags: z.string().optional(), isPersonal: z.boolean().default(false), // Email specific settings fromName: z.string().optional(), replyTo: z.string().email("Invalid reply-to email format").optional().or(z.literal("")), trackOpens: z.boolean().default(true), // Push notification specific settings pushTitle: z.string().optional(), pushIcon: z.string().url("Invalid push icon URL").optional().or(z.literal("")), pushUrl: z.string().url("Invalid push URL").optional().or(z.literal("")), // SMS specific settings smsContent: z.string().max(160, "SMS content must be 160 characters or less").optional(), }); // Helper function to generate unique value from title function generateUniqueValue(title) { return title .toLowerCase() .replace(/[^a-z0-9\s]/g, '') .replace(/\s+/g, '_') .substring(0, 50); } // Helper function to validate template content function validateTemplateContent(content, channels) { const errors = []; // Check for balanced variable syntax const variableMatches = content.match(/\{\{[^}]*\}\}/g) || []; for (const match of variableMatches) { if (!match.endsWith('}}')) { errors.push(`Unmatched variable syntax: ${match}`); } } return errors; } export default defineEventHandler(async (event) => { try { // Get template ID from route parameters const templateId = getRouterParam(event, "id"); if (!templateId) { throw createError({ statusCode: 400, statusMessage: "Template ID is required", }); } // Read and validate request body const body = await readBody(event); console.log("Template update request for ID:", templateId, body); // Validate input data const validationResult = updateTemplateSchema.safeParse(body); if (!validationResult.success) { const errors = validationResult.error.errors.map(err => `${err.path.join('.')}: ${err.message}` ); throw createError({ statusCode: 400, statusMessage: "Validation failed", data: { errors: errors } }); } const validatedData = validationResult.data; // Get current user (assuming auth middleware provides this) const user = event.context.user; if (!user || !user.userID) { throw createError({ statusCode: 401, statusMessage: "Authentication required", }); } console.log("User authenticated:", user.userID); // Check if template exists const existingTemplate = await prisma.notification_templates.findUnique({ where: { id: templateId } }); if (!existingTemplate) { throw createError({ statusCode: 404, statusMessage: "Template not found", }); } console.log("Template found for update:", existingTemplate.name); // Validate template content const contentErrors = validateTemplateContent(validatedData.content, validatedData.channels); if (contentErrors.length > 0) { throw createError({ statusCode: 400, statusMessage: "Content validation failed", data: { errors: contentErrors } }); } // Generate unique value for the template if title changed let finalValue = existingTemplate.value; if (validatedData.title !== existingTemplate.name) { let uniqueValue = generateUniqueValue(validatedData.title); // Check if value already exists and make it unique let counter = 1; finalValue = uniqueValue; while (true) { const existingValueTemplate = await prisma.notification_templates.findUnique({ where: { value: finalValue, NOT: { id: templateId } // Exclude current template } }); if (!existingValueTemplate) { break; } finalValue = `${uniqueValue}_${counter}`; counter++; // Safety check to prevent infinite loop if (counter > 100) { finalValue = `${uniqueValue}_${Date.now()}`; break; } } } // Prepare data for database update const templateData = { name: validatedData.title, value: finalValue, description: validatedData.description || null, subject: validatedData.subject, preheader: validatedData.preheader || null, email_content: validatedData.content, push_title: validatedData.pushTitle || null, push_body: validatedData.content ? validatedData.content.replace(/<[^>]*>/g, '').substring(0, 300) : null, push_icon: validatedData.pushIcon || null, push_url: validatedData.pushUrl || null, sms_content: validatedData.smsContent || null, category: validatedData.category, channels: validatedData.channels, status: validatedData.status, version: validatedData.version, tags: validatedData.tags || null, is_personal: validatedData.isPersonal, from_name: validatedData.fromName || null, reply_to: validatedData.replyTo || null, track_opens: validatedData.trackOpens, is_active: validatedData.status === "Active", updated_by: user.userID.toString(), updated_at: new Date(), }; console.log("Updating template with data:", templateData); // Create version history entry before updating the template await prisma.notification_template_versions.create({ data: { template_id: templateId, version: existingTemplate.version || "1.0", name: existingTemplate.name, description: existingTemplate.description, subject: existingTemplate.subject, preheader: existingTemplate.preheader, email_content: existingTemplate.email_content, push_title: existingTemplate.push_title, push_body: existingTemplate.push_body, push_icon: existingTemplate.push_icon, push_url: existingTemplate.push_url, sms_content: existingTemplate.sms_content, category: existingTemplate.category, channels: existingTemplate.channels, status: existingTemplate.status, tags: existingTemplate.tags, is_personal: existingTemplate.is_personal, from_name: existingTemplate.from_name, reply_to: existingTemplate.reply_to, track_opens: existingTemplate.track_opens, variables: existingTemplate.variables, is_active: existingTemplate.is_active, change_description: `Template updated - version ${existingTemplate.version}`, is_current: false, created_by: user.userID.toString(), } }); // Update the template const updatedTemplate = await prisma.notification_templates.update({ where: { id: templateId }, data: templateData }); console.log("Template updated successfully:", updatedTemplate.id); // Return success response return { success: true, data: { id: updatedTemplate.id, message: "Template updated successfully", template: { id: updatedTemplate.id, name: updatedTemplate.name, value: updatedTemplate.value, status: updatedTemplate.status, category: updatedTemplate.category, channels: updatedTemplate.channels, version: updatedTemplate.version, updated_at: updatedTemplate.updated_at } }, }; } catch (error) { console.error("Template update error:", error); console.error("Error details:", { message: error.message, stack: error.stack, cause: error.cause, code: error.code, statusCode: error.statusCode }); // Handle Prisma errors if (error.code && error.code.startsWith('P')) { console.error("Prisma error code:", error.code); if (error.code === 'P2002') { throw createError({ statusCode: 400, statusMessage: "Template with this name already exists", data: { error: "A template with this name already exists. Please choose a different name.", code: error.code } }); } if (error.code === 'P2025') { throw createError({ statusCode: 404, statusMessage: "Template not found", data: { error: "The template you're trying to update does not exist.", code: error.code } }); } throw createError({ statusCode: 400, statusMessage: "Database operation failed", data: { error: error.message, code: error.code } }); } // Handle known errors with status codes if (error.statusCode) { throw error; } // Generic server error throw createError({ statusCode: 500, statusMessage: "Failed to update template", data: { error: error.message } }); } finally { } });