diff --git a/app/(root)/dashboard/api-integrations/page.tsx b/app/(root)/dashboard/api-integrations/page.tsx
deleted file mode 100644
index 6e62c52..0000000
--- a/app/(root)/dashboard/api-integrations/page.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { WebhookManager } from '@/components/Layout/Dashboard/API-Integrations/WebhookManager';
-
-export default function Page(){
-
- return
-}
\ No newline at end of file
diff --git a/app/(root)/dashboard/apikeys/page.tsx b/app/(root)/dashboard/apikeys/page.tsx
new file mode 100644
index 0000000..8baedd7
--- /dev/null
+++ b/app/(root)/dashboard/apikeys/page.tsx
@@ -0,0 +1,8 @@
+import React from 'react'
+import ApiKeysDashboard from '@/components/apikey/ApikeysManagement';
+
+const page = () => {
+ return
+}
+
+export default page
\ No newline at end of file
diff --git a/app/(root)/dashboard/webhooks/page.tsx b/app/(root)/dashboard/webhooks/page.tsx
new file mode 100644
index 0000000..a3b4ab9
--- /dev/null
+++ b/app/(root)/dashboard/webhooks/page.tsx
@@ -0,0 +1,6 @@
+import { WebhookManager } from '@/components/Layout/Dashboard/webhooks/WebhookManager';
+
+export default function Page(){
+
+ return
+}
\ No newline at end of file
diff --git a/app/api/generate/[contentId]/[sessionId]/route.ts b/app/api/contents/[contentId]/[sessionId]/route.ts
similarity index 100%
rename from app/api/generate/[contentId]/[sessionId]/route.ts
rename to app/api/contents/[contentId]/[sessionId]/route.ts
diff --git a/app/api/contents/route.ts b/app/api/contents/route.ts
new file mode 100644
index 0000000..4d0e2fa
--- /dev/null
+++ b/app/api/contents/route.ts
@@ -0,0 +1,52 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { fetchContentsFromDB, resolveUserIdFromApiKey } from '@/lib/db/content';
+
+export const runtime = 'nodejs';
+
+
+export async function GET(request: NextRequest) {
+ try {
+ const apiKey = request.headers.get('x-api-key');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Invalid or missing API Key' },
+ { status: 401 }
+ );
+ }
+
+ const authenticatedUserId = await resolveUserIdFromApiKey(apiKey);
+
+ if (!authenticatedUserId) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Invalid or revoked API Key.' },
+ { status: 401 }
+ );
+ }
+
+
+ const searchParams = request.nextUrl.searchParams;
+ const limit = parseInt(searchParams.get('limit'));
+ const versions = parseInt(searchParams.get('versions'));
+ const authorID = searchParams.get('author_id');
+
+ // Ensure limit is a positive number
+ const safeLimit = Math.max(1, limit);
+ // Ensure versions is a non-negative number
+ const safeVersions = Math.max(0, versions);
+
+ const contentData = await fetchContentsFromDB(safeLimit, safeVersions, authorID);
+
+ return NextResponse.json({
+ success: true,
+ count: contentData.length,
+ data: contentData,
+ }, { status: 200 });
+
+ } catch (error) {
+ return NextResponse.json(
+ { error: 'Internal Server Error' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/format/route.ts b/app/api/format/route.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/app/api/keys/route.ts b/app/api/keys/route.ts
new file mode 100644
index 0000000..9989b20
--- /dev/null
+++ b/app/api/keys/route.ts
@@ -0,0 +1,30 @@
+import { generateAndStoreNewApiKey } from "@/lib/db/content";
+import { NextRequest, NextResponse } from "next/server";
+
+export const runtime = 'nodejs';
+
+export async function POST(request: NextRequest) {
+
+ try {
+ const { keyName } = await request.json();
+
+ // 2. Input Validation
+ if (!keyName || typeof keyName !== 'string' || keyName.trim().length === 0) {
+ return NextResponse.json({ error: 'A valid key name is required.' }, { status: 400 });
+ }
+
+ // 3. Generate, Hash, and Store the new key
+ const plainTextKey = await generateAndStoreNewApiKey(keyName.trim());
+
+ // 4. Return the plaintext key (ONLY return it on creation!)
+ return NextResponse.json({
+ success: true,
+ plainTextKey: plainTextKey,
+ message: "Key generated. Store this key securely, it will not be shown again."
+ }, { status: 201 });
+
+ } catch (error) {
+ console.error('API Key Generation Error:', error);
+ return NextResponse.json({ error: 'Failed to process key generation request.' }, { status: 500 });
+ }
+}
diff --git a/app/api/scheduled/[content_id]/[session_id]/route.ts b/app/api/schedule/[content_id]/[session_id]/route.ts
similarity index 100%
rename from app/api/scheduled/[content_id]/[session_id]/route.ts
rename to app/api/schedule/[content_id]/[session_id]/route.ts
diff --git a/app/api/scheduled/content_generate/[time]/route.ts b/app/api/schedule/content/[time]/route.ts
similarity index 97%
rename from app/api/scheduled/content_generate/[time]/route.ts
rename to app/api/schedule/content/[time]/route.ts
index e50a1b6..b1be9ac 100644
--- a/app/api/scheduled/content_generate/[time]/route.ts
+++ b/app/api/schedule/content/[time]/route.ts
@@ -9,7 +9,7 @@ interface Context {
};
}
-const API_ENDPOINT = 'http://localhost:3000/api/scheduled';
+const API_ENDPOINT = 'http://localhost:3000/api/schedule';
export async function GET(request: NextRequest, context: Context) {
const { time } = await context.params;
diff --git a/app/api/execute-webhook/route.ts b/app/api/webhook/route.ts
similarity index 100%
rename from app/api/execute-webhook/route.ts
rename to app/api/webhook/route.ts
diff --git a/components/Layout/Dashboard/API-Integrations/WebhookManager.tsx b/components/Layout/Dashboard/webhooks/WebhookManager.tsx
similarity index 100%
rename from components/Layout/Dashboard/API-Integrations/WebhookManager.tsx
rename to components/Layout/Dashboard/webhooks/WebhookManager.tsx
diff --git a/components/Layout/Navigations/Sidebar.tsx b/components/Layout/Navigations/Sidebar.tsx
index 392ac1b..592b6c5 100644
--- a/components/Layout/Navigations/Sidebar.tsx
+++ b/components/Layout/Navigations/Sidebar.tsx
@@ -39,7 +39,8 @@ const navigation = [
title: 'Other',
items: [
{ href: '/dashboard/explore', icon: Globe, label: 'Explore Tools' },
- { href: '/dashboard/api-integrations', icon: Globe, label: 'API Intgrations' },
+ { href: '/dashboard/webhooks', icon: Globe, label: 'Webhook Intgrations' },
+ { href: '/dashboard/apikeys', icon: Globe, label: 'Api Keys' },
],
},
];
diff --git a/components/apikey/ApikeysManagement.tsx b/components/apikey/ApikeysManagement.tsx
new file mode 100644
index 0000000..20a3508
--- /dev/null
+++ b/components/apikey/ApikeysManagement.tsx
@@ -0,0 +1,343 @@
+'use client'
+import { useAuth } from '@/context/AuthContext';
+import { fetchUserApiKeys, generateAndStoreNewApiKey, revokeOrUnrevokeApiKey } from '@/lib/db/content';
+import React, { useState, useEffect, useCallback, useMemo, memo } from 'react';
+
+/**
+ * Utility to copy text to clipboard
+ */
+const copyToClipboard = (text) => {
+ try {
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(text);
+ } else {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ document.body.appendChild(textarea);
+ textarea.focus();
+ textarea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textarea);
+ }
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
+
+const SkeletonLoader = () => (
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+);
+
+const NewKeyDialog = ({ isOpen, onClose, userId, onKeyGenerated }) => {
+ const [keyName, setKeyName] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [plainTextKey, setPlainTextKey] = useState('');
+ const [error, setError] = useState(null);
+
+ const handleGenerate = async (e) => {
+ e.preventDefault();
+ if (!keyName.trim() || !userId) return;
+
+ setIsLoading(true);
+ setError(null);
+ setPlainTextKey('');
+
+ try {
+ const plainTextKey = await generateAndStoreNewApiKey(keyName.trim());
+
+ if (plainTextKey) {
+ setPlainTextKey(plainTextKey);
+ onKeyGenerated();
+ } else {
+ setError('Failed to generate key.');
+ }
+ } catch (err) {
+ setError(err.message.toString() || 'Network error during key generation.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ setKeyName('');
+ setPlainTextKey('');
+ setError(null);
+ onClose();
+ };
+
+ // Simulated Shadcn Dialog structure (Light theme)
+ return (
+
+
+
+
+ {plainTextKey ? 'Key Generated!' : 'Create New API Key'}
+
+
+ {plainTextKey ? (
+
+
+ ⚠️ SUCCESS: Copy the key below. It will not be shown again.
+
+
+
{plainTextKey}
+
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+const ApiKeysDashboard = () => {
+ const { user} = useAuth();
+ const userId = useMemo(() => user?.id || null, [user]);
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [keys, setKeys] = useState([]);
+ const [showDialog, setShowDialog] = useState(false);
+ const [message, setMessage] = useState(null);
+
+ // 2. Data Fetching (GET /api/keys)
+ const fetchKeys = useCallback(async () => {
+ if (!userId) return;
+ setIsLoading(true);
+
+ try {
+ const keys = await fetchUserApiKeys();
+ if (keys) {
+ setKeys(keys || []);
+ } else {
+ setMessage({ type: 'error', text: 'Failed to load keys.' });
+ }
+ } catch (error) {
+ setMessage({ type: 'error', text: error.message || 'Network error while fetching keys.' });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [userId]);
+
+ useEffect(() => {
+ fetchKeys();
+ }, [fetchKeys]);
+
+
+ const handleRevoke = async (keyId, state) => {
+ if (!window.confirm("Are you sure you want to permanently revoke this API key? This cannot be undone.")) return;
+
+ setIsLoading(true);
+ setMessage(null);
+
+ try {
+ const response = await revokeOrUnrevokeApiKey(keyId, state);
+ if (response) {
+ setMessage({ type: 'success', text: state === false ? "Key revoke successfully" : "Key unrevoked successfully." });
+ fetchKeys(); // Refresh the list
+ } else {
+ setMessage({ type: 'error', text: 'Failed to revoke key.' });
+ setIsLoading(false);
+ }
+ } catch (error) {
+ setMessage({ type: 'error', text: error.message || 'Network error while revoking key.' });
+ setIsLoading(false);
+ }
+ };
+
+ // Utility for date formatting
+ const formatDate = (dateValue) => {
+ if (!dateValue) return 'N/A';
+ // Assuming dateValue is an ISO string or a Firestore Timestamp object from the API
+ const date = dateValue.toDate ? dateValue.toDate() : new Date(dateValue);
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ const isDataLoading = isLoading;
+
+ // Main Dashboard Render (Light theme)
+ return (
+
+
+
setShowDialog(false)}
+ userId={userId}
+ onKeyGenerated={() => {
+ setShowDialog(false);
+ fetchKeys();
+ setMessage({ type: 'success', text: 'Key generated successfully! Check the dialog for the key value.' });
+ }}
+ />
+
+
+
+
+
API Keys Dashboard
+
+
+
+ Use these keys to access your data via the external platform API.
+
+
+
+ {message && (
+
+ {message.text}
+
+ )}
+
+ {/* Key List / Skeleton Loader */}
+
+ {isDataLoading ? (
+
+ ) : (
+
+ {keys.length === 0 ? (
+
+ You have no API keys. Click "New Key" to create one.
+
+ ) : (
+
+ {keys.map((key) => (
+
+ {/* TOP SECTION: Name, Status, and REVOKE BUTTON */}
+
+
+ {/* Left Section: Icon, Key Name, Status */}
+
+
+
+
+ {key.name}
+
+
+
+ {key.isActive ? 'Active' : 'Revoked'}
+
+
+
+ {/* REVOKE BUTTON (Subtle, Icon-only) */}
+
+
+
+
+
+ {/* Right Section: Creation Date */}
+
+
+
+ Created:
+ {formatDate(key.createdAt)}
+
+
+
+
+ {/* BOTTOM SECTION: Metadata Details (ID, Last Used) */}
+
+
+ {/* Key ID
+
+
+
Key ID:
+
+ {key.id}
+
+
*/}
+
+ {/* Last Used */}
+
+
+
Last Used:
+
{formatDate(key.lastUsed)}
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default memo(ApiKeysDashboard);
\ No newline at end of file
diff --git a/context/GenerationContext.tsx b/context/GenerationContext.tsx
index 58ec209..f800967 100644
--- a/context/GenerationContext.tsx
+++ b/context/GenerationContext.tsx
@@ -48,6 +48,8 @@ export const contentRendererTabsState = {
templates: 'templates'
};
+const API_Endpoint = '/api/contents';
+
// --- 1. Define the Context Interface (What components can access) ---
interface GenerationContextType {
// The final structured output from the AI
@@ -265,7 +267,7 @@ export function ContextProvider({ children }: { children: ReactNode }) {
formData.append('document', blob, file.name); // 'document' is the expected field name on your server
}
- const response = await fetch(`/api/generate/${content_id}/${sessionId}`, {
+ const response = await fetch(`${API_Endpoint}/${content_id}/${sessionId}`, {
method: 'POST',
body: formData,
});
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index 8971fa2..03bea3c 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -8,7 +8,7 @@ import {
pgSchema,
boolean,
} from 'drizzle-orm/pg-core';
-import { relations } from 'drizzle-orm';
+import { relations, sql } from 'drizzle-orm';
const authSchema = pgSchema('auth');
export const users = authSchema.table('users', {
@@ -98,6 +98,25 @@ export const userContents = pgTable(
},
);
+export const apiKeys = pgTable("api_keys", {
+ id: uuid("id").primaryKey().default(sql`gen_random_uuid()`),
+
+ // Storing the HASH of the API key (MANDATORY for security)
+ keyHash: text("key_hash").notNull().unique(),
+
+ // Link back to the user who owns this key
+ userId: uuid("user_id")
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }), // Link to the user
+
+ name: text("name").notNull(), // E.g., "Integration Key for Mobile App"
+ isActive: boolean("is_active").default(true).notNull(),
+
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ lastUsed: timestamp("last_used"),
+});
+
+
// Relations for 'userContents'
export const userContentsRelations = relations(userContents, ({ one }) => ({
// Defines the 'one' master content record this version belongs to.
@@ -106,3 +125,10 @@ export const userContentsRelations = relations(userContents, ({ one }) => ({
references: [contents.contentId],
}),
}));
+
+export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
+ owner: one(users, {
+ fields: [apiKeys.userId],
+ references: [users.id],
+ }),
+}));
diff --git a/lib/db/content.ts b/lib/db/content.ts
index bbc090c..b8437ce 100644
--- a/lib/db/content.ts
+++ b/lib/db/content.ts
@@ -1,8 +1,10 @@
'use server';
import { createClient } from '@/utils/supabase/server';
import { db } from '@/db';
-import { contents, userContents } from '@/drizzle/schema';
-import { and, desc, eq } from 'drizzle-orm';
+import { v4 as uuidv4 } from 'uuid';
+import * as bcrypt from 'bcryptjs';
+import { apiKeys, contents, userContents } from '@/drizzle/schema';
+import { and, desc, eq, sql } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export interface WebhookCredentials {
id: string;
@@ -21,7 +23,7 @@ interface ScheduledJob {
job_id: string;
cronSchedule: string;
frequency: string;
- time: string; // e.g., '09:30'
+ time: string;
isActive: boolean;
runSlot: string;
referenceUrls: string[];
@@ -557,3 +559,156 @@ export async function getWebhookCredentials() {
throw error;
}
}
+
+
+export async function fetchContentsFromDB(limit = 10, versions = 3, userId?: string, contentId?: string) {
+
+ try{
+ const relationConfig = {
+ limit: versions,
+ orderBy: [desc(userContents.createdAt)],
+ }
+
+ const userIdClause = userId ? eq(contents.authorId, userId) : undefined;
+ const contentIdClause = userId ? eq(contents.contentId, contentId) : undefined;
+
+
+ const results = await db.query.contents.findMany({
+ limit: limit,
+ where: and(userIdClause, contentIdClause),
+
+ with: {
+ versions: relationConfig,
+ },
+ orderBy: (contents, { desc }) => [desc(contents.createdAt)],
+ });
+
+ return results;
+
+ }catch (e){
+ console.error(e.message);
+ throw e;
+ }
+}
+
+export async function generateAndStoreNewApiKey(keyName: string): Promise {
+ const SALT_ROUNDS = 10;
+
+ // Generate a secure, unique plaintext key (e.g., sk_prefix_longrandomstring)
+ const prefix = "sk_ai_"; // Secret Key prefix
+ // Generate a random part using UUID and basic string manipulation
+ const randomPart = Buffer.from(uuidv4()).toString('base64').replace(/=/g, '').slice(0, 32);
+ const plainTextKey = `${prefix}${randomPart}`;
+
+ // Hash the key before storing it
+ const keyHash = await bcrypt.hash(plainTextKey, SALT_ROUNDS);
+
+ try {
+ const supabase = await createClient();
+
+ const { data: userData, error: authError } = await supabase.auth.getUser();
+
+ if (authError || !userData?.user) {
+ console.error('Authentication Error:', authError?.message || 'User not logged in.');
+ throw new Error('User authentication required to create API keys.');
+ }
+
+ // Save the HASH and related metadata to the database
+ await db.insert(apiKeys).values({
+ keyHash: keyHash,
+ userId: userData.user.id,
+ name: keyName,
+ // Default values (isActive, createdAt) are handled by Drizzle/PostgreSQL defaults
+ });
+
+ // Return the plaintext key (the one the user needs to copy)
+ return plainTextKey;
+
+ } catch (e: any) {
+ console.error('Error saving API Key hash:', e);
+ throw new Error('Failed to generate and store API key.');
+ }
+}
+
+
+export async function resolveUserIdFromApiKey(plainTextKey: string): Promise {
+
+
+ const activeKeys = await db.select({
+ id: apiKeys.id,
+ keyHash: apiKeys.keyHash,
+ userId: apiKeys.userId,
+ })
+ .from(apiKeys)
+ .where(sql`${apiKeys.isActive} = TRUE`);
+
+ for (const keyRecord of activeKeys) {
+ // Use bcrypt.compare to check the plaintext key against the stored hash
+ if (await bcrypt.compare(plainTextKey, keyRecord.keyHash)) {
+ // Update lastUsed timestamp on successful verification (Optional but recommended)
+ await db.update(apiKeys)
+ .set({ lastUsed: sql`now()` }) // Use SQL function for current time
+ .where(sql`${apiKeys.id} = ${keyRecord.id}`);
+
+ return keyRecord.userId;
+ }
+ }
+
+ return null;
+}
+
+
+export async function fetchUserApiKeys() {
+ try {
+ const supabase = await createClient();
+
+ const { data: userData, error: authError } = await supabase.auth.getUser();
+
+ if (authError || !userData?.user) {
+ console.error('Authentication Error:', authError?.message || 'User not logged in.');
+ throw new Error('User authentication required to fetch API keys.');
+ }
+
+ const keys = await db.select({
+ id: apiKeys.id,
+ name: apiKeys.name,
+ isActive: apiKeys.isActive,
+ createdAt: apiKeys.createdAt,
+ lastUsed: apiKeys.lastUsed,
+ })
+ .from(apiKeys)
+ .where(eq(apiKeys.userId, userData.user.id))
+ .orderBy(desc(apiKeys.createdAt));
+
+ return keys;
+ } catch (e) {
+ console.error('Error fetching API keys:', e);
+ throw new Error('Failed to retrieve keys from database.');
+ }
+}
+
+
+export async function revokeOrUnrevokeApiKey(keyId: string, state: boolean): Promise {
+ try {
+ const supabase = await createClient();
+
+ const { data: userData, error: authError } = await supabase.auth.getUser();
+
+ if (authError || !userData?.user) {
+ console.error('Authentication Error:', authError?.message || 'User not logged in.');
+ throw new Error('User authentication required to revoke API keys.');
+ }
+
+ const userId = userData.user.id;
+ // Only allow the key owner to revoke the key, and only if it's currently active.
+ const result = await db.update(apiKeys)
+ .set({ isActive: state })
+ .where(sql`${apiKeys.id} = ${keyId} AND ${apiKeys.userId} = ${userId}`)
+ .returning({ id: apiKeys.id });
+
+ return result.length > 0; // If one row was returned, the update was successful.
+ } catch (e) {
+ console.error('Error revoking API key:', e);
+ throw new Error('Failed to revoke key.');
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 55bbd2a..3b11915 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@xdevplatform/xdk": "^0.2.1-beta",
"ai": "^5.0.87",
"axios": "^1.13.2",
+ "bcryptjs": "^3.0.3",
"cheerio": "^1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -69,6 +70,7 @@
"tailwind-merge": "^3.3.1",
"unsplash-js": "^7.0.20",
"use-debounce": "^10.0.6",
+ "uuid": "^13.0.0",
"vaul": "^1.1.2",
"zod": "^4.1.12"
},
diff --git a/vercel.json b/vercel.json
index 1113000..4013eb5 100644
--- a/vercel.json
+++ b/vercel.json
@@ -3,11 +3,11 @@
"framework": "nextjs",
"crons": [
{
- "path": "/api/scheduled/content_generate/early",
+ "path": "/api/schedule/content/early",
"schedule": "0 9 * * *"
},
{
- "path": "/api/scheduled/content_generate/later",
+ "path": "/api/schedule/content/later",
"schedule": "0 17 * * *"
}
]