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
7 changes: 7 additions & 0 deletions app/(root)/dashboard/api-integrations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page(){

return(
<>
</>
)
}
4 changes: 2 additions & 2 deletions app/api/image_upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server'; // Supabase client utility
import { randomUUID } from 'crypto'; // For unique file names
import { createClient } from '@/utils/supabase/server';
import { randomUUID } from 'crypto';

export async function POST(req: Request) {
const supabase = await createClient();
Expand Down
2 changes: 1 addition & 1 deletion components/Layout/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function Dashboard() {

// 3. Empty State: Show No Content Message
return (
<div className="rounded-xl border border-indigo-200 bg-indigo-50 p-16 text-center">
<div className="rounded-xl border w-full border-indigo-200 bg-indigo-50 p-16 text-center">
<p className="mb-2 text-2xl font-bold text-black">No content found.</p>
<p className="text-md text-black">
Start a new search or create your first piece of content!
Expand Down
10 changes: 2 additions & 8 deletions components/Layout/Dashboard/Generate/ApiSenderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useContent } from '@/context/GenerationContext';
import { cn } from '@/lib/utils';
import { Input } from '@/components/ui/input';
import { motion } from 'framer-motion';
import { makeRequest } from '@/lib/actions/content.actions';

export const ApiSenderDialog = ({ isOpen, onClose }) => {
// Mock hook usage
Expand Down Expand Up @@ -106,20 +107,13 @@ export const ApiSenderDialog = ({ isOpen, onClose }) => {
setErrorDetails('');
setApiResult(null);

// --- Start Mock API Call (Replace this section with actual fetch in a real environment) ---
try {
// Implement exponential backoff for resilience (optional but good practice)
const maxRetries = 3;
let delay = 1000;

for (let i = 0; i < maxRetries; i++) {
try {
// Simulate successful response
const mockResponse = {
id: 'post-' + Math.floor(Math.random() * 10000),
status: 'published',
url: 'https://preview.yourcms.com/article/' + Math.floor(Math.random() * 10000),
};
const mockResponse = await makeRequest(apiEndpoint, customBody);

setApiResult(mockResponse);
setStatus('success');
Expand Down
3 changes: 2 additions & 1 deletion components/Layout/Dashboard/Generate/ContentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '@mdxeditor/editor/style.css';
import rehypeRaw from 'rehype-raw';
import { useGlobalState } from '@/context/GlobalStateContext';
import { useContent } from '@/context/GenerationContext';
import { PromptSelector } from '@/components/Layout/Dashboard/Generate/PromptSelector';

interface ContentRendererProps {
isLoading: boolean;
Expand Down Expand Up @@ -52,7 +53,7 @@ export const Content: React.FC<ContentRendererProps> = ({ isLoading }: { isLoadi
>
{isLoading ? (
<ContentLoadingSkeleton />
) : (
) : !generatedContent && !isLoading ? (<PromptSelector/>) : (
// ARTICLE CONTAINER: This is where we control the content's width.
// - max-w-4xl mx-auto: Limits content to a readable width (e.g., 56rem) and centers it.
// - flex-col: Ensures Markdown content stacks vertically.
Expand Down
161 changes: 161 additions & 0 deletions components/Layout/Dashboard/Generate/PromptSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client'

import React, { useState } from 'react';
import {
Feather, // Placeholder icon for writing
FileText,
Rocket,
Search,
Zap,
CheckCircle,
TrendingUp,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
import { useContent } from '@/context/GenerationContext';

// --- Prompt Data Structure ---
interface Prompt {
id: string;
title: string;
icon: React.ElementType;
summary: string;
fullPrompt: string;
}

// --- List of Well-Written Prompts ---
const professionalPrompts: Prompt[] = [
{
id: 'p-1',
title: 'Executive Summary Generator',
icon: FileText,
summary: 'Analyze a 500-word document and extract the three most critical takeaways, summarizing them into a professional executive overview, suitable for C-level review.',
fullPrompt: 'Analyze the provided text. Identify the three most critical business insights, risks, or opportunities. Summarize these points into a single, concise executive summary paragraph, maintaining a formal and objective tone.',
},
{
id: 'p-2',
title: 'SEO-Optimized Blog Post',
icon: TrendingUp,
summary: 'Generate a 1000-word blog post on "The Future of Quantum Computing," incorporating a required set of five target keywords naturally throughout the text.',
fullPrompt: 'Draft a 1000-word SEO-optimized blog post titled "The Future of Quantum Computing." The post must include the following keywords: [quantum entanglement], [superposition], [qubit], [d-wave], and [quantum supremacy]. Structure the post with a compelling introduction, three detailed body sections, and a conclusion with a clear call to action.',
},
{
id: 'p-3',
title: 'Competitive Analysis Report',
icon: Search,
summary: 'Conduct a high-level competitive analysis of three specified companies, detailing their market position, primary product, and unique selling proposition (USP).',
fullPrompt: 'Perform a high-level competitive analysis comparing Company A, Company B, and Company C. For each company, detail their current market share (if known), their flagship product, their core technology advantage, and a concise summary of their Unique Selling Proposition (USP). Present the findings in a clear, comparative table format.',
},
{
id: 'p-4',
title: 'Viral Social Media Campaign',
icon: Rocket,
summary: 'Design a three-part social media campaign (Twitter thread, LinkedIn post, Instagram caption) to promote a new SaaS feature launch.',
fullPrompt: 'Design a high-impact, three-part social media campaign to promote the launch of a new SaaS feature: "Real-time AI Document Collaboration." Create a compelling, detailed Twitter thread (max 7 tweets), a professional LinkedIn post focused on business value, and a short, engaging Instagram caption with relevant hashtags.',
},
];


export const PromptSelector = () => {
// State to track the ID of the currently selected prompt
const [selectedPromptId, setSelectedPromptId] = useState(null);

const { setPrompt } = useContent();

// Derive the full details of the selected prompt
const selectedPrompt = professionalPrompts.find(p => p.id === selectedPromptId);

const handleSelect = (id: string, prompt = '') => {
setSelectedPromptId(id === selectedPromptId ? null : id);
setPrompt(prompt);
};

return (
<div className="font-sans p-6 bg-neutral-50 rounded-xl shadow-lg border border-neutral-200">
<h2 className="text-2xl font-bold text-neutral-900 mb-2 flex items-center">
<Feather className="w-6 h-6 mr-3 text-black" />
Prompt Templates Library
</h2>
<p className="text-neutral-600 mb-6 border-b pb-3 text-sm">
Select a template to instantly load a professionally written prompt into the editor, or use it as inspiration.
</p>

{/* Prompt Grid Container (Responsive Layout) */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-1 lg:grid-cols-2">
{professionalPrompts.map((prompt) => {
const isSelected = prompt.id === selectedPromptId;
const IconComponent = prompt.icon;

return (
<motion.div
key={prompt.id}
initial={{ scale: 1 }}
whileHover={{ scale: 1.01, zIndex: 10 }}
onClick={() => handleSelect(prompt.id, prompt.fullPrompt)}
className={cn(
'group p-4 rounded-xl border-2 transition-300 cursor-pointer shadow-sm',
'flex flex-col justify-between min-h-[180px]',
// Default state
'bg-white border-neutral-200 hover:border-black/50 hover:shadow-md',
// Selected state
isSelected
? 'border-black bg-black text-white shadow-xl ring-2 ring-offset-2 ring-black/50'
: 'text-neutral-900',
)}
>
<div className="mb-4">
<div className={cn(
'p-2 rounded-full mb-3 inline-block transition-colors duration-300',
isSelected
? 'bg-white text-black'
: 'bg-neutral-100 text-neutral-700 group-hover:bg-black group-hover:text-white',
)}>
<IconComponent className="w-5 h-5" />
</div>
<h3 className={cn(
"text-lg font-semibold",
isSelected ? 'text-white' : 'text-neutral-900 group-hover:text-black'
)}>
{prompt.title}
</h3>
</div>

<div className="text-xs opacity-90">
<p className={cn(
"line-clamp-3",
isSelected ? 'text-neutral-300' : 'text-neutral-600 group-hover:text-neutral-800'
)}>{prompt.summary}</p>
</div>

{isSelected && (
<div className="mt-3 flex items-center text-xs font-semibold text-white">
<CheckCircle className="w-4 h-4 mr-1.5" /> Selected
</div>
)}
</motion.div>
);
})}
</div>

{/*/!* Selected Prompt Display (for demonstration) *!/*/}
{/*{selectedPrompt && (*/}
{/* <motion.div*/}
{/* initial={{ opacity: 0, y: 20 }}*/}
{/* animate={{ opacity: 1, y: 0 }}*/}
{/* className="mt-8 p-5 bg-black rounded-xl shadow-xl border border-neutral-700"*/}
{/* >*/}
{/* <h3 className="flex items-center text-lg font-bold text-white mb-2">*/}
{/* <Zap className="w-5 h-5 mr-2 text-yellow-400" />*/}
{/* Active Prompt Text*/}
{/* </h3>*/}
{/* <p className="font-mono text-sm text-neutral-300 whitespace-pre-wrap">*/}
{/* {selectedPrompt.fullPrompt}*/}
{/* </p>*/}
{/* <p className="mt-4 text-xs text-neutral-500">*/}
{/* *This text is now ready to be loaded into your main input field.**/}
{/* </p>*/}
{/* </motion.div>*/}
{/*)}*/}
</div>
);
};
8 changes: 8 additions & 0 deletions components/Layout/Dashboard/Generate/content/Renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MarkdownViewer } from '@/components/Layout/Dashboard/Generate/content/M
import { Source } from '../RightSidebarPanel';
import EditorLoader from '@/components/Layout/Dashboard/Generate/Editor/EditorLoader';
import { FileText, PenLine, Link as LinkIcon, Code } from 'lucide-react';
import { PromptSelector } from '@/components/Layout/Dashboard/Generate/PromptSelector';

const Editor = dynamic(() => import('../Editor/Editor'), {
// Make sure we turn SSR off
Expand Down Expand Up @@ -54,6 +55,12 @@ export default function Renderer() {
>
<MarkdownViewer />
</TabsContent>
<TabsContent
className={'container-full overflow-hidden'}
value={contentRendererTabsState.templates}
>
<PromptSelector/>
</TabsContent>
</div>
</Tabs>
</div>
Expand All @@ -68,6 +75,7 @@ const RendererTabs = () => {
[contentRendererTabsState.editor]: PenLine,
[contentRendererTabsState.sources]: LinkIcon,
[contentRendererTabsState.markdown]: Code,
[contentRendererTabsState.templates]: Code,
};

return (
Expand Down
1 change: 1 addition & 0 deletions components/Layout/Navigations/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const navigation = [
title: 'Other',
items: [
{ href: '/dashboard/explore', icon: Globe, label: 'Explore Tools' },
{ href: '/dashboard/api-integrations', icon: Globe, label: 'API Intgrations' },
],
},
];
Expand Down
2 changes: 2 additions & 0 deletions context/GenerationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const contentRendererTabsState = {
editor: 'editor',
sources: 'sources',
markdown: 'markdown',
templates: 'templates'
};

// --- 1. Define the Context Interface (What components can access) ---
Expand Down Expand Up @@ -211,6 +212,7 @@ export function ContextProvider({ children }: { children: ReactNode }) {

setIsLoading(true);
setGeneratedContent('');
setPrompt('');
setChatHistory((prev) => [...prev, { id, role: 'user', content: prompt }]);

try {
Expand Down
11 changes: 11 additions & 0 deletions lib/actions/content.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { db } from '@/db';
import { contents } from '@/drizzle/schema';
import { and, eq } from 'drizzle-orm';
import axios from 'axios';

export async function deleteContent(contentId: string) {
const supabase = await createClient();
Expand Down Expand Up @@ -69,3 +70,13 @@
return { error: 'Failed to update content.' };
}
}

export async function makeRequest(url: string, options: any) {

try {
return await axios.post(url, options);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 11 months ago

To address SSRF, you must restrict which URLs can be targeted by requests made through makeRequest. The safest approach is to maintain an allowlist (whitelist) of permitted hostnames or base URLs. Instead of allowing arbitrary URL input, you should use a lookup mechanism: callers pass an identifier (e.g., "serviceA", "serviceB") or choose from a list, and the actual URL used for the request is determined by a secure mapping in the server code.

  • General fix: Replace unrestricted use of the url parameter in requests with a system that validates or selects URLs from an allowlist before making the request.
  • Best fix for this code: Define an allowlist mapping permitted identifiers (or a small set of base URLs) within makeRequest. Have the function consult this mapping, and use only values from it—never raw user input as the request destination. Validate input against this mapping and throw an error if the supplied identifier or URL isn't allowed.
  • File/region to change: All changes are limited to the existing implementation of makeRequest in lib/actions/content.actions.ts.
  • Implementation needs: Define an allowlist mapping; update the function signature to take an identifier instead of a raw URL; lookup and use the mapped allowed URL; throw an error on an invalid selection.

Suggested changeset 1
lib/actions/content.actions.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/lib/actions/content.actions.ts b/lib/actions/content.actions.ts
--- a/lib/actions/content.actions.ts
+++ b/lib/actions/content.actions.ts
@@ -71,8 +71,18 @@
   }
 }
 
-export async function makeRequest(url: string, options: any) {
+// Allowlist mapping of permitted service endpoints
+const ALLOWED_ENDPOINTS: { [key: string]: string } = {
+  serviceA: "https://serviceA.example.com/api",
+  serviceB: "https://serviceB.example.com/api",
+  // Add other allowed service endpoints here
+};
 
+export async function makeRequest(endpointKey: string, options: any) {
+  const url = ALLOWED_ENDPOINTS[endpointKey];
+  if (!url) {
+    throw new Error('Invalid endpoint requested');
+  }
   try {
     return await axios.post(url, options);
   }catch (e) {
EOF
@@ -71,8 +71,18 @@
}
}

export async function makeRequest(url: string, options: any) {
// Allowlist mapping of permitted service endpoints
const ALLOWED_ENDPOINTS: { [key: string]: string } = {
serviceA: "https://serviceA.example.com/api",
serviceB: "https://serviceB.example.com/api",
// Add other allowed service endpoints here
};

export async function makeRequest(endpointKey: string, options: any) {
const url = ALLOWED_ENDPOINTS[endpointKey];
if (!url) {
throw new Error('Invalid endpoint requested');
}
try {
return await axios.post(url, options);
}catch (e) {
Copilot is powered by AI and may make mistakes. Always verify output.
}catch (e) {
console.log(e);
throw e;
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
"@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.21",
"drizzle-kit": "^0.31.7",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"eslint-config-prettier": "^10.1.8",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
Expand Down
Loading