Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions app/api/routes-b/email-templates/[id]/preview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import Handlebars from 'handlebars';
import prisma from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth'; // Adjust based on your auth implementation

const previewSchema = z.object({
sampleData: z.record(z.any()).default({}),
});

export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// Auth Check
const user = await getCurrentUser();
if (!user) {
return NextResponse.json(
{ error: 'Unauthorized', message: 'You must be logged in to perform this action.' },
{ status: 401 }
);
}

// Request Validation
const body = await request.json().catch(() => ({}));
const validation = previewSchema.safeParse(body);

if (!validation.success) {
return NextResponse.json(
{
error: 'Bad Request',
message: 'Invalid sample data provided.',
details: validation.error.format()
},
{ status: 400 }
);
}

// Fetch Template & Ownership Check
const template = await prisma.emailTemplate.findUnique({
where: { id: params.id },
});

if (!template) {
return NextResponse.json(
{ error: 'Not Found', message: 'Email template not found.' },
{ status: 404 }
);
}

if (template.userId !== user.id) {
return NextResponse.json(
{ error: 'Forbidden', message: 'You do not have permission to access this template.' },
{ status: 403 }
);
}

// Render Template
const compiledTemplate = Handlebars.compile(template.content || '');
const renderedHtml = compiledTemplate(validation.data.sampleData);

// Success Response
return NextResponse.json(
{
data: {
renderedHtml,
templateId: template.id
}
},
{ status: 200 }
);

} catch (error) {
console.error('[EMAIL_TEMPLATE_PREVIEW_POST]', error);
return NextResponse.json(
{ error: 'Internal Server Error', message: 'An unexpected error occurred.' },
{ status: 500 }
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { verifyAuthToken } from '@/lib/auth';

export async function POST(
request: NextRequest,
{ params }: { params: { id: string; v: string } }
) {
try {
// 1. Extract and verify Privy token
const authHeader = request.headers.get('authorization');
const token = authHeader?.replace('Bearer ', '');

if (!token) {
return NextResponse.json(
{ error: 'Unauthorized', message: 'Missing authentication token.' },
{ status: 401 }
);
}

const claims = await verifyAuthToken(token);
if (!claims) {
return NextResponse.json(
{ error: 'Unauthorized', message: 'Invalid or expired token.' },
{ status: 401 }
);
}

// 2. Verify template existence and ownership
const template = await prisma.emailTemplate.findUnique({
where: { id: params.id },
});

if (!template) {
return NextResponse.json(
{ error: 'Not Found', message: 'Email template not found.' },
{ status: 404 }
);
}

if (template.userId !== claims.userId) {
return NextResponse.json(
{ error: 'Forbidden', message: 'You do not have permission to modify this template.' },
{ status: 403 }
);
}

// 3. Verify the target version exists and belongs to this template
const targetVersion = await prisma.emailTemplateVersion.findUnique({
where: { id: params.v },
});

if (!targetVersion || targetVersion.templateId !== params.id) {
return NextResponse.json(
{ error: 'Not Found', message: 'Email template version not found.' },
{ status: 404 }
);
}

// 4. Activate the version
// NOTE: Adjust 'activeVersionId' to match your exact Prisma schema field name
const updatedTemplate = await prisma.emailTemplate.update({
where: { id: params.id },
data: { activeVersionId: params.v },
});

return NextResponse.json({ data: updatedTemplate }, { status: 200 });
} catch (error) {
console.error('[EMAIL_TEMPLATE_VERSION_ACTIVATE]', error);
return NextResponse.json(
{ error: 'Internal Server Error', message: 'An unexpected error occurred.' },
{ status: 500 }
);
}
}
116 changes: 116 additions & 0 deletions app/api/routes-b/email-templates/activate-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { NextRequest } from 'next/server';
import { POST } from '@/app/api/routes-b/email-templates/[id]/versions/[v]/activate/route';
import { verifyAuthToken } from '@/lib/auth';
import prisma from '@/lib/prisma';

jest.mock('@/lib/auth');
jest.mock('@/lib/prisma', () => ({
__esModule: true,
default: {
emailTemplate: {
findUnique: jest.fn(),
update: jest.fn(),
},
emailTemplateVersion: {
findUnique: jest.fn(),
},
},
}));

describe('POST /api/routes-b/email-templates/[id]/versions/[v]/activate', () => {
const mockParams = { params: { id: 'template-123', v: 'version-456' } };
const mockClaims = { userId: 'privy-user-789' };

beforeEach(() => {
jest.clearAllMocks();
});

const createRequest = (withAuth = true) => {
const headers = new Headers();
if (withAuth) headers.set('Authorization', 'Bearer valid-token');
return new NextRequest('http://localhost/api/routes-b/email-templates/template-123/versions/version-456/activate', {
method: 'POST',
headers,
});
};

it('should return 401 if missing auth token', async () => {
const response = await POST(createRequest(false), mockParams);
expect(response.status).toBe(401);
});

it('should return 404 if parent template is not found', async () => {
(verifyAuthToken as jest.Mock).mockResolvedValue(mockClaims);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue(null);

const response = await POST(createRequest(), mockParams);
expect(response.status).toBe(404);
});

it('should return 403 if user does not own the parent template', async () => {
(verifyAuthToken as jest.Mock).mockResolvedValue(mockClaims);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: 'different-user'
});

const response = await POST(createRequest(), mockParams);
expect(response.status).toBe(403);
});

it('should return 404 if the target version does not exist', async () => {
(verifyAuthToken as jest.Mock).mockResolvedValue(mockClaims);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: mockClaims.userId
});
(prisma.emailTemplateVersion.findUnique as jest.Mock).mockResolvedValue(null);

const response = await POST(createRequest(), mockParams);
const data = await response.json();

expect(response.status).toBe(404);
expect(data.error).toBe('Not Found');
});

it('should return 404 if target version belongs to a different template', async () => {
(verifyAuthToken as jest.Mock).mockResolvedValue(mockClaims);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: mockClaims.userId
});
(prisma.emailTemplateVersion.findUnique as jest.Mock).mockResolvedValue({
id: 'version-456',
templateId: 'wrong-template-id' // Mismatched relation
});

const response = await POST(createRequest(), mockParams);
expect(response.status).toBe(404);
});

it('should return 200 and update the template on success', async () => {
(verifyAuthToken as jest.Mock).mockResolvedValue(mockClaims);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: mockClaims.userId
});
(prisma.emailTemplateVersion.findUnique as jest.Mock).mockResolvedValue({
id: 'version-456',
templateId: 'template-123'
});
(prisma.emailTemplate.update as jest.Mock).mockResolvedValue({
id: 'template-123',
activeVersionId: 'version-456'
});

const response = await POST(createRequest(), mockParams);
const data = await response.json();

expect(response.status).toBe(200);
expect(data.data.activeVersionId).toBe('version-456');
expect(prisma.emailTemplate.update).toHaveBeenCalledWith({
where: { id: 'template-123' },
data: { activeVersionId: 'version-456' },
});
});
});
91 changes: 91 additions & 0 deletions app/api/routes-b/email-templates/preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextRequest } from 'next/server';
import { POST } from '@/app/api/routes-b/email-templates/[id]/preview/route';
import { getCurrentUser } from '@/lib/auth';
import prisma from '@/lib/prisma';

jest.mock('@/lib/auth');
jest.mock('@/lib/prisma', () => ({
__esModule: true,
default: {
emailTemplate: {
findUnique: jest.fn(),
},
},
}));

describe('POST /api/routes-b/email-templates/[id]/preview', () => {
const mockParams = { params: { id: 'template-123' } };
const mockUser = { id: 'user-456' };

beforeEach(() => {
jest.clearAllMocks();
});

const createMockRequest = (body: any = {}) => {
return new NextRequest('http://localhost/api/routes-b/email-templates/template-123/preview', {
method: 'POST',
body: JSON.stringify(body),
});
};

it('should return 401 if user is not authenticated', async () => {
(getCurrentUser as jest.Mock).mockResolvedValue(null);

const request = createMockRequest();
const response = await POST(request, mockParams);
const data = await response.json();

expect(response.status).toBe(401);
expect(data.error).toBe('Unauthorized');
});

it('should return 404 if template does not exist', async () => {
(getCurrentUser as jest.Mock).mockResolvedValue(mockUser);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue(null);

const request = createMockRequest({ sampleData: {} });
const response = await POST(request, mockParams);
const data = await response.json();

expect(response.status).toBe(404);
expect(data.error).toBe('Not Found');
});

it('should return 403 if user does not own the template', async () => {
(getCurrentUser as jest.Mock).mockResolvedValue(mockUser);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: 'different-user-789',
content: 'Hello {{name}}',
});

const request = createMockRequest({ sampleData: {} });
const response = await POST(request, mockParams);
const data = await response.json();

expect(response.status).toBe(403);
expect(data.error).toBe('Forbidden');
});

it('should return 200 and rendered HTML on the happy path', async () => {
(getCurrentUser as jest.Mock).mockResolvedValue(mockUser);
(prisma.emailTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-123',
userId: mockUser.id,
content: 'Hello {{name}}, your balance is {{balance}}.',
});

const request = createMockRequest({
sampleData: {
name: 'Alice',
balance: '$100',
},
});

const response = await POST(request, mockParams);
const data = await response.json();

expect(response.status).toBe(200);
expect(data.data.renderedHtml).toBe('Hello Alice, your balance is $100.');
});
});