Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ coverage
# Environment files
.env
.env.*
!.env.example


# Git
.git
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,35 @@ Then run:
docker-compose up -d
```

# Newsletter Management

## Quick Start

1. Visit `/admin/newsletter` (team members only)
2. Fill in the newsletter form
3. Click "Generate Code" and "Copy Code"
4. Open `data/newsletters.ts`
5. Paste the code at the start of the `newsletters` array
6. Save and deploy

That's it! ✨

## Example

The admin form generates this:
```typescript
{
id: "18",
slug: "january-2025-updates",
title: "January 2025 Updates",
content: `...`,
publishedAt: new Date("2025-01-17"),
author: "opensox.ai team",
},
```

Just paste it into the newsletters array!

## Our contributors

<a href="https://github.com/apsinghdev/opensox/graphs/contributors">
Expand Down
32 changes: 0 additions & 32 deletions apps/api/.env.example

This file was deleted.

46 changes: 30 additions & 16 deletions apps/api/src/clients/razorpay.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import Razorpay from "razorpay";

const RAZORPAY_KEY_ID = process.env.RAZORPAY_KEY_ID;
const RAZORPAY_KEY_SECRET = process.env.RAZORPAY_KEY_SECRET;
let rz_instance: Razorpay | null = null;

if (!RAZORPAY_KEY_ID) {
throw new Error(
"RAZORPAY_KEY_ID is required but not set in environment variables. Please configure it in your .env file."
);
}
/**
* Get Razorpay instance (lazy initialization)
* Only initializes when actually needed, allowing the app to start without Razorpay config
*/
export function getRazorpayInstance(): Razorpay {
if (rz_instance) {
return rz_instance;
}

if (!RAZORPAY_KEY_SECRET) {
throw new Error(
"RAZORPAY_KEY_SECRET is required but not set in environment variables. Please configure it in your .env file."
);
}
const RAZORPAY_KEY_ID = process.env.RAZORPAY_KEY_ID;
const RAZORPAY_KEY_SECRET = process.env.RAZORPAY_KEY_SECRET;

if (!RAZORPAY_KEY_ID) {
throw new Error(
"RAZORPAY_KEY_ID is required but not set in environment variables. Please configure it in your .env file."
);
}

export const rz_instance = new Razorpay({
key_id: RAZORPAY_KEY_ID,
key_secret: RAZORPAY_KEY_SECRET,
});
if (!RAZORPAY_KEY_SECRET) {
throw new Error(
"RAZORPAY_KEY_SECRET is required but not set in environment variables. Please configure it in your .env file."
);
}

rz_instance = new Razorpay({
key_id: RAZORPAY_KEY_ID,
key_secret: RAZORPAY_KEY_SECRET,
});

return rz_instance;
}
3 changes: 2 additions & 1 deletion apps/api/src/services/payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rz_instance } from "../clients/razorpay.js";
import { getRazorpayInstance } from "../clients/razorpay.js";
import crypto from "crypto";
import prismaModule from "../prisma.js";
import {
Expand Down Expand Up @@ -74,6 +74,7 @@ export const paymentService = {
const { amount, currency, receipt, notes } = input;

try {
const rz_instance = getRazorpayInstance();
const order = await rz_instance.orders.create({
amount,
currency,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
"posthog-js": "^1.203.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.18",
"react-tweet": "^3.2.1",
"remark-gfm": "^4.0.1",
"superjson": "^2.2.5",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
Expand Down
232 changes: 232 additions & 0 deletions apps/web/src/app/(main)/admin/newsletter/_components/NewsAdmin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"use client";
import { useState } from 'react';

export default function NewsletterAdmin() {
const [formData, setFormData] = useState({
title: '',
description: '',
content: '',
author: 'opensox.ai team',
issueNumber: '',
readTime: ''
});

const [generatedCode, setGeneratedCode] = useState('');
const [copied, setCopied] = useState(false);

const generateSlug = (title: string) => {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
};
Comment on lines +17 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Slug generation doesn't handle edge cases.

The generateSlug() function could produce empty strings or colliding slugs in edge cases (e.g., titles with only special characters, or identical titles).

Add validation and uniqueness checks:

 const generateSlug = (title: string) => {
-  return title
+  const slug = title
     .toLowerCase()
     .replace(/[^a-z0-9]+/g, '-')
     .replace(/(^-|-$)/g, '');
+  
+  if (!slug) {
+    throw new Error('Title must contain at least one alphanumeric character');
+  }
+  
+  return slug;
 };

For production, also check slug uniqueness against existing newsletters.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const generateSlug = (title: string) => {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
};
const generateSlug = (title: string) => {
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
if (!slug) {
throw new Error('Title must contain at least one alphanumeric character');
}
return slug;
};


const getNextId = () => {
return "18"; // Update this to the next ID number
};
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hard-coded ID creates maintenance burden.

The getNextId() function returns a hard-coded value that must be manually updated after each newsletter addition. This is error-prone and can lead to ID collisions if forgotten.

Consider one of these approaches:

  • Auto-increment IDs in a database
  • UUID generation: crypto.randomUUID()
  • Timestamp-based IDs

Example with UUID:

-const getNextId = () => {
-  return "18"; // Update this to the next ID number
-};
+const getNextId = () => {
+  return crypto.randomUUID();
+};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/src/app/(main)/admin/newsletter/_components/NewsAdmin.tsx around
lines 24 to 26, getNextId() currently returns a hard-coded "18" causing manual
updates and possible collisions; replace it with an automatic ID generator
(e.g., return crypto.randomUUID() or Date.now().toString()) and ensure any
required imports or browser API usage is supported (or fallback to a small UUID
helper) so new newsletters get unique IDs without manual intervention.


const generateCode = () => {
if (!formData.title || !formData.content) {
alert('Please fill in Title and Content');
return;
}

const slug = generateSlug(formData.title);
const today = new Date().toISOString().split('T')[0];

const code = `{
id: "${getNextId()}",
slug: "${slug}",
title: "${formData.title}",${formData.description ? `
description: "${formData.description}",` : ''}
content: \`${formData.content}\`,
publishedAt: new Date("${today}"),
author: "${formData.author}",${formData.issueNumber ? `
issueNumber: "${formData.issueNumber}",` : ''}${formData.readTime ? `
readTime: ${formData.readTime},` : ''}
},`;
Comment on lines +37 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential XSS vulnerability in generated code.

The generated code doesn't escape special characters in user input. If an admin enters quotes, backticks, or other special characters in form fields, it could break the JavaScript syntax or create injection risks when pasted into source code.

Add proper escaping for string values:

+const escapeString = (str: string) => {
+  return str.replace(/\\/g, '\\\\')
+           .replace(/`/g, '\\`')
+           .replace(/\$/g, '\\$');
+};
+
 const generateCode = () => {
   // ... validation ...
   
   const code = `{
   id: "${getNextId()}",
   slug: "${slug}",
-  title: "${formData.title}",${formData.description ? `
-  description: "${formData.description}",` : ''}
-  content: \`${formData.content}\`,
+  title: "${escapeString(formData.title)}",${formData.description ? `
+  description: "${escapeString(formData.description)}",` : ''}
+  content: \`${escapeString(formData.content)}\`,

Committable suggestion skipped: line range outside the PR's diff.


setGeneratedCode(code);
};

const copyToClipboard = () => {
navigator.clipboard.writeText(generatedCode);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

const handleReset = () => {
setFormData({
title: '',
description: '',
content: '',
author: 'opensox.ai team',
issueNumber: '',
readTime: ''
});
setGeneratedCode('');
setCopied(false);
};

return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-6">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
Add New Newsletter
</h1>
<p className="text-gray-400">Fill in the details below, then copy the generated code to newsletters.ts</p>
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Form Section */}
<div className="bg-gray-900 rounded-lg border border-gray-800 p-6">
<h2 className="text-xl font-semibold mb-6 text-purple-400">Newsletter Details</h2>

<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Title *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none"
placeholder="January 2025 Updates"
/>
</div>

<div>
<label className="block text-sm font-medium mb-2">Description (optional)</label>
<input
type="text"
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none"
placeholder="Brief summary of the newsletter"
/>
</div>

<div>
<label className="block text-sm font-medium mb-2">Content (Markdown) *</label>
<textarea
value={formData.content}
onChange={(e) => setFormData({...formData, content: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none font-mono text-sm"
rows={12}
placeholder="# Welcome to January Updates

We're excited to share what we've been working on!

## New Features

**Feature Name** - Description

## What's Next

- Feature 1
- Feature 2"
/>
<p className="text-xs text-gray-500 mt-1">
Use Markdown: **bold**, *italic*, [links](url), ## headings
</p>
</div>

<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">Author</label>
<input
type="text"
value={formData.author}
onChange={(e) => setFormData({...formData, author: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none"
/>
</div>

<div>
<label className="block text-sm font-medium mb-2">Issue # (optional)</label>
<input
type="text"
value={formData.issueNumber}
onChange={(e) => setFormData({...formData, issueNumber: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none"
placeholder="#18"
/>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-2">Read Time (minutes, optional)</label>
<input
type="number"
value={formData.readTime}
onChange={(e) => setFormData({...formData, readTime: e.target.value})}
className="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg focus:border-purple-500 focus:outline-none"
placeholder="8"
/>
</div>

<div className="flex gap-3 pt-4">
<button
onClick={generateCode}
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 text-white px-6 py-3 rounded-lg font-semibold hover:from-purple-600 hover:to-pink-600 transition"
>
Generate Code
</button>
<button
onClick={handleReset}
className="px-6 py-3 bg-gray-800 rounded-lg hover:bg-gray-700 transition"
>
Reset
</button>
</div>
</div>
</div>

{/* Output Section */}
<div className="bg-gray-900 rounded-lg border border-gray-800 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-purple-400">Generated Code</h2>
{generatedCode && (
<button
onClick={copyToClipboard}
className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition text-sm font-medium"
>
{copied ? '✓ Copied!' : 'Copy Code'}
</button>
)}
</div>

{generatedCode ? (
<div>
<div className="bg-gray-950 rounded-lg p-4 border border-gray-800 overflow-x-auto">
<pre className="text-sm text-purple-300 font-mono whitespace-pre-wrap">
{generatedCode}
</pre>
</div>

<div className="mt-6 p-4 bg-blue-950/30 border border-blue-800 rounded-lg">
<h3 className="font-semibold mb-2 text-blue-400">📝 Next Steps:</h3>
<ol className="text-sm space-y-2 text-gray-300">
<li>1. Click "Copy Code" above</li>
<li>2. Open <code className="bg-gray-800 px-2 py-1 rounded">data/newsletters.ts</code></li>
<li>3. Find the <code className="bg-gray-800 px-2 py-1 rounded">newsletters</code> array</li>
<li>4. Paste the code at the <strong>beginning</strong> of the array (after the opening <code>[</code>)</li>
<li>5. Save the file</li>
<li>6. Commit and push to deploy!</li>
</ol>
</div>
</div>
) : (
<div className="bg-gray-950 rounded-lg p-12 border border-gray-800 text-center text-gray-500">
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<p>Fill in the form and click "Generate Code"</p>
</div>
)}
</div>
</div>
</div>
</div>
);
}
Comment on lines +4 to +232
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major architectural concern: Manual workflow doesn't scale.

This component generates code snippets for manual copy-paste into source files rather than persisting data to a database. This approach has significant limitations:

  • No version control or audit trail for content changes
  • Deployment required for every newsletter update
  • Error-prone manual ID management
  • No editing capability for existing newsletters
  • Security risk if admins accidentally paste malicious content
  • Doesn't support the PR objectives of "Admin content management" with creation, editing, and deletion

For a production newsletter system, consider implementing:

  • Server actions or API routes for CRUD operations
  • Database storage (e.g., PostgreSQL, MongoDB)
  • Proper authentication checks server-side
  • Rich text editor for content
  • Preview functionality
  • Draft/publish workflow

Would you like me to help design a proper database-backed newsletter management system with Next.js server actions?

Loading