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
38 changes: 24 additions & 14 deletions components/Layout/Dashboard/API-Integrations/WebhookManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import { saveWebhookCredentials } from '@/lib/db/content';
import { toast } from 'sonner';
import { useContent } from '@/context/GenerationContext';

// Mock Encryption/Decryption Utility (Simulating secure storage/retrieval)
const mockEncrypt = (data) => `ENC:${btoa(data)}`;
Expand Down Expand Up @@ -71,7 +74,9 @@ const WebhookFormDialog = ({ isOpen, onClose, initialData, onSave, status, messa
}, [initialData]);

useEffect(() => {
setMessageBox({ message, status })
const timer = setTimeout(() => setMessageBox({ message, status }), 300);

return () => clearTimeout(timer);
}, [status, message]);


Expand All @@ -81,7 +86,6 @@ const WebhookFormDialog = ({ isOpen, onClose, initialData, onSave, status, messa
try {
const url = new URL(formData.url.trim());
if (url.protocol !== 'https:') {
// Using console.error instead of alert as per instructions
console.error("Validation Error: Only HTTPS URLs are permitted for security.");
// setMessageBox('Validation Error: Only HTTPS URLs are permitted for security.', 'error');
return;
Expand Down Expand Up @@ -221,17 +225,17 @@ const WebhookFormDialog = ({ isOpen, onClose, initialData, onSave, status, messa

// --- Main Webhook Manager Component ---
export const WebhookManager = () => {
// Initialize state with mock data. We decrypt for display purposes.
const [webhooks, setWebhooks] = useState(initialMockWebhooks.map(h => ({
...h,
secret_key: mockDecrypt(h.secret_key),
})));
const {
webhookCredentials: webhooks,
setWebhookCredentials: setWebhooks,
isWebhookCredentialsLoading
} = useContent();

const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingWebhook, setEditingWebhook] = useState(null);

// UI Status State (used for global actions like delete or toggle)
const [status, setStatus] = useState('idle'); // 'idle', 'loading', 'success', 'error'
const [status, setStatus] = useState<'idle'| 'loading'| 'success'| 'error'>('idle');
const [message, setMessage] = useState('');

// --- CRUD Handlers (Local State Management) ---
Expand All @@ -247,19 +251,20 @@ export const WebhookManager = () => {
setIsDialogOpen(true);
};

const handleSave = (data) => {
const handleSave = async (data) => {
setStatus('loading');
setMessage(data.id ? 'Updating webhook...' : 'Creating new webhook...');

// Simulate async operation delay
setTimeout(() => {
try {
const dataToSave = {
...data,
// Re-encrypt the key before saving to mock persistent storage
secret_key: mockEncrypt(data.secret_key),
updated_at: Date.now(),
};

await saveWebhookCredentials(dataToSave);

setWebhooks(prevHooks => {
if (data.id) {
// Update existing hook
Expand All @@ -286,7 +291,12 @@ export const WebhookManager = () => {
setIsDialogOpen(false);
}, 1000);

}, 500); // Mock network delay
}catch (e) {
toast.error(e.message || "Error saving webhook...");
setMessage("Error saving webhook...")
}finally {

}
};

const handleDelete = (id) => {
Expand Down Expand Up @@ -353,9 +363,9 @@ export const WebhookManager = () => {
<p className="text-lg font-medium text-neutral-700">{message}</p>
</div>
) : (
<ScrollArea className="flex flex-col space-y-4 max-h-full pr-3">
<ScrollArea className="flex flex-col space-y-4 max-h-[78dvh] rounded-lg pr-3">
{webhooks.length === 0 ? (
<div className="text-center p-12 border border-neutral-200 rounded-lg bg-white shadow-sm">
<div className="text-center p-8 border border-neutral-200 rounded-lg bg-white shadow-sm">
<Database className="h-10 w-10 text-neutral-400 mx-auto mb-3" />
<p className="text-lg font-medium text-neutral-700">No webhooks configured.</p>
<p className="text-xs text-neutral-500">
Expand Down
37 changes: 35 additions & 2 deletions context/GenerationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { toast } from 'sonner';
import { ReadonlyURLSearchParams, useParams, useRouter, useSearchParams } from 'next/navigation';
import { createClient } from '@/utils/supabase/client';
import { useAuth } from '@/context/AuthContext';
import { getGeneratedContents, getScheduledJobs } from '@/lib/db/content';
import {
getGeneratedContents,
getScheduledJobs,
getWebhookCredentials,
WebhookCredentials,
} from '@/lib/db/content';
import { SystemPromptOption } from '@/components/Layout/Dashboard/Generate/AISystemConfig';
import { predefinedPrompts } from '@/lib/AI/ai.system.prompt';
import { refinePrompt } from '@/lib/AI/ai.actions';
Expand Down Expand Up @@ -118,6 +123,11 @@ interface GenerationContextType {
onRefinePrompt: () => Promise<string>;

triggerWebhookDispatch: () => Promise<void>;

webhookCredentials: WebhookCredentials[];
setWebhookCredentials: (webhooks: WebhookCredentials[]) => void;
isWebhookCredentialsLoading: boolean,
setIsWebhookCredentialsLoading: (prev?: boolean) => void;
}

// --- 2. Create the Context with Default Values ---
Expand Down Expand Up @@ -150,11 +160,14 @@ export function ContextProvider({ children }: { children: ReactNode }) {
const [isViewingGoal, setIsViewingGoal] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [prompt, setPrompt] = useState('');
const [webhookCredentials, setWebhookCredentials] = useState<WebhookCredentials[]>([]);
const [isWebhookCredentialsLoading, setIsWebhookCredentialsLoading] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<SystemPromptOption>(predefinedPrompts[0]);

useEffect(() => {
fetchContents();
fetchScheduledJobs();
fetchWebhookCredentials()
}, []);

const fetchContents = useCallback(async () => {
Expand Down Expand Up @@ -189,6 +202,22 @@ export function ContextProvider({ children }: { children: ReactNode }) {
}
}, [setScheduledJobs]);

const fetchWebhookCredentials = useCallback(async () => {
setIsWebhookCredentialsLoading(true);
try {
const credentials = await getWebhookCredentials();
if (credentials) {
setWebhookCredentials(credentials);
} else {
setWebhookCredentials([]);
}
} catch (e) {
toast.error(e.message || 'Error fetching webhook credentials');
} finally {
setIsWebhookCredentialsLoading(false);
}
}, [setWebhookCredentials]);

// The function to call the Next.js API Route
const generateContent = async (
prompt: string,
Expand Down Expand Up @@ -494,7 +523,11 @@ export function ContextProvider({ children }: { children: ReactNode }) {
onRefinePrompt,
isDialogOpen,
setIsDialogOpen,
triggerWebhookDispatch
triggerWebhookDispatch,
webhookCredentials,
setWebhookCredentials,
isWebhookCredentialsLoading,
setIsWebhookCredentialsLoading,
};

return <GenerationContext.Provider value={value}>{children}</GenerationContext.Provider>;
Expand Down
86 changes: 86 additions & 0 deletions lib/db/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { db } from '@/db';
import { contents, userContents } from '@/drizzle/schema';
import { and, desc, eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export interface WebhookCredentials {
id: string;
user_id: string;
url: string;
secret_key: string;
trigger_event: string;
is_active: boolean;
created_at: string;
updated_at: string;
}

interface ScheduledJob {
jobType: string;
Expand Down Expand Up @@ -442,3 +452,79 @@ export async function saveContentFromSchedules(
console.error(`Error saving content version for ID ${contentId} with Drizzle:`, error);
}
}


export async function saveWebhookCredentials(hookData: WebhookCredentials) {
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 save webhooks.');
}

const userId = userData.user.id;

const dataToSave = {
id: hookData.id ? hookData.id : crypto.randomUUID(),
user_id: userId,
url: hookData.url,
secret_key: hookData.secret_key, // Assuming this is already encrypted (mock or real)
trigger_event: hookData.trigger_event,
is_active: hookData.is_active,
updated_at: new Date().toISOString(),
};

const { data, error } = await supabase
.from('api_integrations')
.upsert(dataToSave, {
onConflict: 'user_id, id',
ignoreDuplicates: false,
})
.select();

if (error) {
console.error('Supabase Upsert Error:', error.message);
throw new Error(`Database save failed: ${error.message}`);
}

console.log('Webhook successfully saved/updated:', data[0]);
return data[0];

} catch (error) {
console.error('saveWebhookToSupabase execution failed:', error.message);
throw error;
}
}

export async function getWebhookCredentials() {
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 save webhooks.');
}

const userId = userData.user.id;

const { data, error } = await supabase
.from('api_integrations')
.select()
.eq('user_id', userId);

if (error) {
console.error('Supabase select Error:', error.message);
throw new Error(`Database fetch failed: ${error.message}`);
}

return data;
}catch (error) {
console.error('Webhook fetch execution failed:', error.message);
throw error;
}
}
Loading