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:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
import prisma from "~/server/utils/prisma";
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",
});
}
console.log("Deleting template:", templateId);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Check if template exists and user has permission
const existingTemplate = await prisma.notification_templates.findFirst({
where: {
id: templateId,
// Optionally restrict to user's own templates
// created_by: user.userID.toString()
}
});
if (!existingTemplate) {
throw createError({
statusCode: 404,
statusMessage: "Template not found or you don't have permission to delete it",
});
}
// Check if template is being used by any notifications
const templatesInUse = await prisma.notifications.findFirst({
where: {
template_id: templateId
}
});
if (templatesInUse) {
throw createError({
statusCode: 400,
statusMessage: "Cannot delete template that is currently being used by notifications. Please remove it from notifications first.",
});
}
console.log("Template found and can be deleted:", existingTemplate.name);
// Delete the template
await prisma.notification_templates.delete({
where: {
id: templateId
}
});
console.log("Template deleted successfully:", templateId);
// Return success response
return {
success: true,
data: {
message: `Template "${existingTemplate.name}" has been deleted successfully`,
deletedTemplate: {
id: existingTemplate.id,
name: existingTemplate.name,
value: existingTemplate.value
}
}
};
} catch (error) {
console.error("Template deletion 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 === 'P2025') {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
data: {
error: "The template you're trying to delete 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 delete template",
data: {
error: error.message
}
});
} finally {
}
});

View File

@@ -0,0 +1,134 @@
import prisma from "~/server/utils/prisma";
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",
});
}
console.log("Fetching template:", templateId);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Fetch the template
const template = await prisma.notification_templates.findUnique({
where: {
id: templateId,
},
});
if (!template) {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
});
}
console.log("Template found:", template.name);
// Format the response data to match frontend expectations
const formattedTemplate = {
id: template.id,
title: template.name,
value: template.value,
description: template.description || "",
subject: template.subject || "",
preheader: template.preheader || "",
category: template.category || "",
channels: template.channels || [],
status: template.status || "Draft",
version: template.version || "1.0",
content: template.email_content || "",
tags: template.tags || "",
isPersonal: template.is_personal || false,
// Email specific settings
fromName: template.from_name || "",
replyTo: template.reply_to || "",
trackOpens: template.track_opens !== false, // Default to true
// Push notification specific settings
pushTitle: template.push_title || "",
pushIcon: template.push_icon || "",
pushUrl: template.push_url || "",
// SMS specific settings
smsContent: template.sms_content || "",
// Metadata
isActive: template.is_active || false,
variables: template.variables || null,
createdBy: template.created_by,
updatedBy: template.updated_by,
createdAt: template.created_at,
updatedAt: template.updated_at,
};
// Return success response
return {
success: true,
message: "wowowowow",
data: {
template: formattedTemplate,
},
};
} catch (error) {
console.error("Template fetch 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 === "P2025") {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
data: {
error: "The template you're looking for 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 fetch template",
data: {
error: error.message,
},
});
} finally {
}
});

View File

@@ -0,0 +1,308 @@
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 {
}
});

View File

@@ -0,0 +1,185 @@
import prisma from "~/server/utils/prisma";
// 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);
}
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",
});
}
console.log("Duplicating template:", templateId);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Find the original template
const originalTemplate = await prisma.notification_templates.findUnique({
where: {
id: templateId
}
});
if (!originalTemplate) {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
});
}
console.log("Original template found:", originalTemplate.name);
// Generate new name and value for the duplicate
const newName = `${originalTemplate.name} (Copy)`;
let uniqueValue = generateUniqueValue(newName);
// 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}_copy_${counter}`;
counter++;
// Safety check to prevent infinite loop
if (counter > 100) {
finalValue = `${uniqueValue}_copy_${Date.now()}`;
break;
}
}
// Prepare duplicate template data
const duplicateData = {
name: newName,
value: finalValue,
description: originalTemplate.description,
subject: originalTemplate.subject,
preheader: originalTemplate.preheader,
email_content: originalTemplate.email_content,
push_title: originalTemplate.push_title,
push_body: originalTemplate.push_body,
push_icon: originalTemplate.push_icon,
push_url: originalTemplate.push_url,
sms_content: originalTemplate.sms_content,
category: originalTemplate.category,
channels: originalTemplate.channels,
status: "Draft", // Always start as draft
version: "1.0", // Reset version for new template
tags: originalTemplate.tags,
is_personal: originalTemplate.is_personal,
from_name: originalTemplate.from_name,
reply_to: originalTemplate.reply_to,
track_opens: originalTemplate.track_opens,
variables: originalTemplate.variables,
is_active: false, // Inactive by default
created_by: user.userID.toString(),
updated_by: user.userID.toString(),
};
console.log("Creating duplicate with data:", duplicateData);
// Create the duplicate template
const duplicateTemplate = await prisma.notification_templates.create({
data: duplicateData
});
console.log("Template duplicated successfully:", duplicateTemplate.id);
// Return success response
return {
success: true,
data: {
message: `Template "${originalTemplate.name}" has been duplicated successfully`,
originalTemplate: {
id: originalTemplate.id,
name: originalTemplate.name,
value: originalTemplate.value
},
duplicateTemplate: {
id: duplicateTemplate.id,
name: duplicateTemplate.name,
value: duplicateTemplate.value,
status: duplicateTemplate.status,
version: duplicateTemplate.version,
created_at: duplicateTemplate.created_at
}
}
};
} catch (error) {
console.error("Template duplication 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: "Duplicate value conflict",
data: {
error: "Unable to generate unique identifier for the duplicate template. Please try again.",
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 duplicate template",
data: {
error: error.message
}
});
} finally {
}
});

View File

@@ -0,0 +1,149 @@
import prisma from "~/server/utils/prisma";
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",
});
}
console.log("Fetching version history for template:", templateId);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Check if template exists
const template = await prisma.notification_templates.findUnique({
where: { id: templateId }
});
if (!template) {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
});
}
// Check if the version history table exists
let versions = [];
try {
// Fetch version history for the template
versions = await prisma.notification_template_versions.findMany({
where: {
template_id: templateId
},
orderBy: {
created_at: 'desc'
}
});
} catch (dbError) {
console.error("Database error when fetching versions:", dbError);
// If table doesn't exist, return empty array
if (dbError.code === 'P2021' || dbError.message.includes('doesn\'t exist')) {
console.log("Version history table doesn't exist, returning empty array");
versions = [];
} else {
throw dbError;
}
}
// Format the response data
const formattedVersions = versions.map(version => ({
id: version.id,
version: version.version,
name: version.name,
description: version.description,
subject: version.subject,
content: version.email_content,
changeDescription: version.change_description,
isCurrent: version.is_current,
status: version.status,
createdBy: version.created_by,
createdAt: version.created_at,
formattedCreatedAt: version.created_at
? new Date(version.created_at).toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "",
}));
console.log(`Found ${formattedVersions.length} versions for template ${templateId}`);
// Return success response
return {
success: true,
data: {
templateId,
templateName: template.name,
versions: formattedVersions,
totalCount: formattedVersions.length
}
};
} catch (error) {
console.error("Version history fetch 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 === 'P2025') {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
data: {
error: "The template you're looking for 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 fetch version history",
data: {
error: error.message
}
});
} finally {
}
});

View File

@@ -0,0 +1,145 @@
import prisma from "~/server/utils/prisma";
export default defineEventHandler(async (event) => {
try {
// Get template ID and version ID from route parameters
const templateId = getRouterParam(event, "id");
const versionId = getRouterParam(event, "versionId");
if (!templateId || !versionId) {
throw createError({
statusCode: 400,
statusMessage: "Template ID and Version ID are required",
});
}
console.log(`Deleting version ${versionId} for template ${templateId}`);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Check if template exists
const template = await prisma.notification_templates.findUnique({
where: { id: templateId }
});
if (!template) {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
});
}
// Check if version exists
const version = await prisma.notification_template_versions.findUnique({
where: { id: versionId }
});
if (!version || version.template_id !== templateId) {
throw createError({
statusCode: 404,
statusMessage: "Version not found",
});
}
// Check if this is the current version
if (version.is_current) {
throw createError({
statusCode: 400,
statusMessage: "Cannot delete current version",
data: {
error: "You cannot delete the current version of a template. Please restore a different version first."
}
});
}
// Check if there are other versions (prevent deletion of the only version)
const versionCount = await prisma.notification_template_versions.count({
where: { template_id: templateId }
});
if (versionCount <= 1) {
throw createError({
statusCode: 400,
statusMessage: "Cannot delete the only version",
data: {
error: "This is the only version of the template. At least one version must exist."
}
});
}
// Delete the version
await prisma.notification_template_versions.delete({
where: { id: versionId }
});
console.log(`Version ${version.version} deleted successfully`);
// Return success response
return {
success: true,
data: {
message: `Version ${version.version} has been deleted successfully`,
templateId,
deletedVersion: version.version,
deletedVersionId: versionId
}
};
} catch (error) {
console.error("Version delete 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 === 'P2025') {
throw createError({
statusCode: 404,
statusMessage: "Template or version not found",
data: {
error: "The template or version you're looking for 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 delete version",
data: {
error: error.message
}
});
} finally {
}
});

View File

@@ -0,0 +1,231 @@
import prisma from "~/server/utils/prisma";
export default defineEventHandler(async (event) => {
try {
// Get template ID and version ID from route parameters
const templateId = getRouterParam(event, "id");
const versionId = getRouterParam(event, "versionId");
if (!templateId || !versionId) {
throw createError({
statusCode: 400,
statusMessage: "Template ID and Version ID are required",
});
}
console.log(`Restoring version ${versionId} for template ${templateId}`);
// Get current user (assuming auth middleware provides this)
const user = event.context.user;
if (!user || !user.userID) {
throw createError({
statusCode: 401,
statusMessage: "Authentication required",
});
}
// Check if template exists
const template = await prisma.notification_templates.findUnique({
where: { id: templateId }
});
if (!template) {
throw createError({
statusCode: 404,
statusMessage: "Template not found",
});
}
// Check if version exists
const version = await prisma.notification_template_versions.findUnique({
where: { id: versionId }
});
if (!version || version.template_id !== templateId) {
throw createError({
statusCode: 404,
statusMessage: "Version not found",
});
}
// Generate new version number (increment current version)
const currentVersion = template.version || "1.0";
const versionParts = currentVersion.split('.');
const majorVersion = parseInt(versionParts[0]) || 1;
const minorVersion = parseInt(versionParts[1]) || 0;
const newVersion = `${majorVersion}.${minorVersion + 1}`;
// Create version history entry for current template state before restore
await prisma.notification_template_versions.create({
data: {
template_id: templateId,
version: currentVersion,
name: template.name,
description: template.description,
subject: template.subject,
preheader: template.preheader,
email_content: template.email_content,
push_title: template.push_title,
push_body: template.push_body,
push_icon: template.push_icon,
push_url: template.push_url,
sms_content: template.sms_content,
category: template.category,
channels: template.channels,
status: template.status,
tags: template.tags,
is_personal: template.is_personal,
from_name: template.from_name,
reply_to: template.reply_to,
track_opens: template.track_opens,
variables: template.variables,
is_active: template.is_active,
change_description: `Automatic backup before restoring version ${version.version}`,
is_current: false,
created_by: user.userID.toString(),
}
});
// Update the current template with the version data
const updatedTemplate = await prisma.notification_templates.update({
where: { id: templateId },
data: {
name: version.name,
description: version.description,
subject: version.subject,
preheader: version.preheader,
email_content: version.email_content,
push_title: version.push_title,
push_body: version.push_body,
push_icon: version.push_icon,
push_url: version.push_url,
sms_content: version.sms_content,
category: version.category,
channels: version.channels,
status: version.status,
tags: version.tags,
is_personal: version.is_personal,
from_name: version.from_name,
reply_to: version.reply_to,
track_opens: version.track_opens,
variables: version.variables,
is_active: version.is_active,
version: newVersion,
updated_by: user.userID.toString(),
updated_at: new Date(),
}
});
// Create version history entry for the restored version
await prisma.notification_template_versions.create({
data: {
template_id: templateId,
version: newVersion,
name: version.name,
description: version.description,
subject: version.subject,
preheader: version.preheader,
email_content: version.email_content,
push_title: version.push_title,
push_body: version.push_body,
push_icon: version.push_icon,
push_url: version.push_url,
sms_content: version.sms_content,
category: version.category,
channels: version.channels,
status: version.status,
tags: version.tags,
is_personal: version.is_personal,
from_name: version.from_name,
reply_to: version.reply_to,
track_opens: version.track_opens,
variables: version.variables,
is_active: version.is_active,
change_description: `Restored from version ${version.version}`,
is_current: true,
created_by: user.userID.toString(),
}
});
// Mark previous current version as not current
await prisma.notification_template_versions.updateMany({
where: {
template_id: templateId,
is_current: true,
version: { not: newVersion }
},
data: {
is_current: false
}
});
console.log(`Version ${version.version} restored successfully as version ${newVersion}`);
// Return success response
return {
success: true,
data: {
message: `Version ${version.version} has been restored successfully as version ${newVersion}`,
templateId,
restoredVersion: version.version,
newVersion,
template: {
id: updatedTemplate.id,
title: updatedTemplate.name,
version: updatedTemplate.version,
updatedAt: updatedTemplate.updated_at
}
}
};
} catch (error) {
console.error("Version restore 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 === 'P2025') {
throw createError({
statusCode: 404,
statusMessage: "Template or version not found",
data: {
error: "The template or version you're looking for 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 restore version",
data: {
error: error.message
}
});
} finally {
}
});

View 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 {
}
});

View File

@@ -0,0 +1,64 @@
import prisma from "~/server/utils/prisma";
// Helper function to map is_active integer to status string
function getStatusFromIsActive(isActive) {
console.log("Converting is_active value:", isActive, "type:", typeof isActive);
switch (isActive) {
case 1:
return "Active";
case 0:
return "Inactive";
case 2:
return "Draft";
default:
return "Draft";
}
}
export default defineEventHandler(async (event) => {
try {
// Use raw query to get the actual integer values for is_active
const templates = await prisma.$queryRaw`
SELECT
id,
name,
value,
category,
is_active,
created_at,
updated_at
FROM notification_templates
ORDER BY name ASC
`;
console.log("RAW QUERY TEMPLATES:", templates);
console.log("First template is_active:", templates[0]?.is_active, "type:", typeof templates[0]?.is_active);
// Format the response to match frontend expectations
return {
success: true,
message: "Code: A1",
data: {
templates: templates.map((template) => ({
id: template.id,
title: template.name,
value: template.value,
category: template.category || "General",
is_active: template.is_active,
created_at: template.created_at || "",
updated_at: template.updated_at || "",
})),
},
};
} catch (error) {
console.error("Error fetching templates:", error);
return {
success: false,
data: {
message: "Failed to fetch templates",
error: error.message,
},
};
} finally {
}
});