Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
231
server/api/notifications/templates/create.post.js
Normal file
231
server/api/notifications/templates/create.post.js
Normal file
@@ -0,0 +1,231 @@
|
||||
import { z } from "zod";
|
||||
import prisma from "~/server/utils/prisma";
|
||||
|
||||
// Input validation schema
|
||||
const createTemplateSchema = 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 {
|
||||
// Read and validate request body
|
||||
const body = await readBody(event);
|
||||
|
||||
console.log("Template creation request:", body);
|
||||
|
||||
// Validate input data
|
||||
const validationResult = createTemplateSchema.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);
|
||||
|
||||
// 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
|
||||
let uniqueValue = generateUniqueValue(validatedData.title);
|
||||
|
||||
// Check if value already exists and make it unique
|
||||
let counter = 1;
|
||||
let finalValue = uniqueValue;
|
||||
while (true) {
|
||||
const existingTemplate = await prisma.notification_templates.findUnique({
|
||||
where: { value: finalValue }
|
||||
});
|
||||
|
||||
if (!existingTemplate) {
|
||||
break;
|
||||
}
|
||||
|
||||
finalValue = `${uniqueValue}_${counter}`;
|
||||
counter++;
|
||||
|
||||
// Safety check to prevent infinite loop
|
||||
if (counter > 100) {
|
||||
finalValue = `${uniqueValue}_${Date.now()}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for database insertion
|
||||
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" ? 1 : validatedData.status === "Draft" ? 0 : 2,
|
||||
created_by: user.userID.toString(),
|
||||
updated_by: user.userID.toString(),
|
||||
};
|
||||
|
||||
console.log("Creating template with data:", templateData);
|
||||
|
||||
// Create the template
|
||||
const newTemplate = await prisma.notification_templates.create({
|
||||
data: templateData
|
||||
});
|
||||
|
||||
console.log("Template created successfully:", newTemplate.id);
|
||||
|
||||
// Return success response
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: newTemplate.id,
|
||||
message: "Template created successfully",
|
||||
template: {
|
||||
id: newTemplate.id,
|
||||
name: newTemplate.name,
|
||||
value: newTemplate.value,
|
||||
status: newTemplate.status,
|
||||
category: newTemplate.category,
|
||||
channels: newTemplate.channels,
|
||||
version: newTemplate.version,
|
||||
created_at: newTemplate.created_at
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Template creation 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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 create template",
|
||||
data: {
|
||||
error: error.message
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user