|
@@ -0,0 +1,561 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Scraper Management API
|
|
|
|
|
+ *
|
|
|
|
|
+ * Provides endpoints for managing webshop scraping functionality:
|
|
|
|
|
+ * - Register/unregister shops with scraper
|
|
|
|
|
+ * - Get shop status and scraped content
|
|
|
|
|
+ * - Manage custom URLs
|
|
|
|
|
+ * - Configure webhooks and scheduling
|
|
|
|
|
+ * - Multi-scraper support with per-store configuration
|
|
|
|
|
+ *
|
|
|
|
|
+ * SECURITY: This function requires JWT authentication (verify_jwt: true)
|
|
|
|
|
+ * The JWT verification is handled automatically by Supabase Edge Functions
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
|
|
|
|
+import { createScraperClient, validateSameDomain } from '../_shared/scraper-client.ts';
|
|
|
|
|
+
|
|
|
|
|
+const corsHeaders = {
|
|
|
|
|
+ 'Access-Control-Allow-Origin': '*',
|
|
|
|
|
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
|
|
|
+ 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Get scraper configuration for a store
|
|
|
|
|
+ */
|
|
|
|
|
+async function getStoreScraperConfig(supabase: any, storeId: string, userId: string) {
|
|
|
|
|
+ const { data: store, error } = await supabase
|
|
|
|
|
+ .from('stores')
|
|
|
|
|
+ .select('id, store_url, scraper_api_url, scraper_api_secret, scraper_registered, scraper_enabled, user_id')
|
|
|
|
|
+ .eq('id', storeId)
|
|
|
|
|
+ .eq('user_id', userId)
|
|
|
|
|
+ .single();
|
|
|
|
|
+
|
|
|
|
|
+ if (error || !store) {
|
|
|
|
|
+ throw new Error('Store not found or access denied');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return store;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Update store scraper registration status
|
|
|
|
|
+ */
|
|
|
|
|
+async function updateStoreScraperStatus(supabase: any, storeId: string, updates: any) {
|
|
|
|
|
+ const { error } = await supabase
|
|
|
|
|
+ .from('stores')
|
|
|
|
|
+ .update(updates)
|
|
|
|
|
+ .eq('id', storeId);
|
|
|
|
|
+
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ throw new Error(`Failed to update store status: ${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Deno.serve(async (req) => {
|
|
|
|
|
+ // Handle CORS preflight requests
|
|
|
|
|
+ if (req.method === 'OPTIONS') {
|
|
|
|
|
+ return new Response(null, {
|
|
|
|
|
+ headers: corsHeaders
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Initialize Supabase client with ANON key for proper RLS
|
|
|
|
|
+ const supabaseUrl = Deno.env.get('SUPABASE_URL');
|
|
|
|
|
+ const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY');
|
|
|
|
|
+
|
|
|
|
|
+ if (!supabaseUrl || !supabaseAnonKey) {
|
|
|
|
|
+ throw new Error('Missing Supabase configuration');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get JWT from authorization header
|
|
|
|
|
+ const authHeader = req.headers.get('Authorization');
|
|
|
|
|
+ if (!authHeader) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Authorization header required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 401,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const jwt = authHeader.replace('Bearer ', '');
|
|
|
|
|
+
|
|
|
|
|
+ // Create Supabase client with user's JWT for proper authentication
|
|
|
|
|
+ const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|
|
|
|
+ global: {
|
|
|
|
|
+ headers: { Authorization: authHeader }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the JWT and get user
|
|
|
|
|
+ const { data: { user }, error: authError } = await supabase.auth.getUser(jwt);
|
|
|
|
|
+
|
|
|
|
|
+ if (authError || !user) {
|
|
|
|
|
+ console.error('Authentication failed:', authError);
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Invalid authorization token'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 401,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify user is authenticated (double-check)
|
|
|
|
|
+ if (!user.id) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Authentication required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 401,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const url = new URL(req.url);
|
|
|
|
|
+ const pathParts = url.pathname.split('/').filter(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ // Route to different endpoints based on path
|
|
|
|
|
+ if (pathParts.length < 1) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Invalid endpoint'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const action = pathParts[0];
|
|
|
|
|
+
|
|
|
|
|
+ switch (action) {
|
|
|
|
|
+ case 'register-shop': {
|
|
|
|
|
+ // POST /register-shop
|
|
|
|
|
+ // Register a store with the scraper
|
|
|
|
|
+ if (req.method !== 'POST') {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Method not allowed'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { store_id } = await req.json();
|
|
|
|
|
+ if (!store_id) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'store_id is required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const store = await getStoreScraperConfig(supabase, store_id, user.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (store.scraper_registered) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ message: 'Store already registered',
|
|
|
|
|
+ shop_id: store_id
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create scraper client with store configuration
|
|
|
|
|
+ const scraperClient = await createScraperClient({
|
|
|
|
|
+ scraper_api_url: store.scraper_api_url,
|
|
|
|
|
+ scraper_api_secret: store.scraper_api_secret
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Register shop with scraper using store_id as custom_id
|
|
|
|
|
+ const job = await scraperClient.registerShop(store.store_url, store.id);
|
|
|
|
|
+
|
|
|
|
|
+ // Set up webhook
|
|
|
|
|
+ const webhookUrl = `${Deno.env.get('SUPABASE_URL')}/functions/v1/scraper-webhook`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await scraperClient.setWebhook(store.id, webhookUrl);
|
|
|
|
|
+ console.log(`Webhook configured for store ${store.id}`);
|
|
|
|
|
+ } catch (webhookError) {
|
|
|
|
|
+ console.warn(`Failed to configure webhook for store ${store.id}:`, webhookError);
|
|
|
|
|
+ // Continue anyway - webhook is optional
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Enable scheduled scraping
|
|
|
|
|
+ try {
|
|
|
|
|
+ await scraperClient.setScheduling(store.id, true);
|
|
|
|
|
+ console.log(`Scheduled scraping enabled for store ${store.id}`);
|
|
|
|
|
+ } catch (scheduleError) {
|
|
|
|
|
+ console.warn(`Failed to enable scheduling for store ${store.id}:`, scheduleError);
|
|
|
|
|
+ // Continue anyway
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update database
|
|
|
|
|
+ await updateStoreScraperStatus(supabase, store.id, {
|
|
|
|
|
+ scraper_registered: true
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ message: 'Store registered with scraper successfully',
|
|
|
|
|
+ job_id: job.id,
|
|
|
|
|
+ shop_id: store.id
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 201,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case 'shop-status': {
|
|
|
|
|
+ // GET /shop-status?store_id=xxx
|
|
|
|
|
+ if (req.method !== 'GET') {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Method not allowed'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const storeId = url.searchParams.get('store_id');
|
|
|
|
|
+ if (!storeId) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'store_id parameter is required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const store = await getStoreScraperConfig(supabase, storeId, user.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!store.scraper_registered) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ registered: false,
|
|
|
|
|
+ enabled: store.scraper_enabled,
|
|
|
|
|
+ message: 'Store not registered with scraper'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get shop status from scraper
|
|
|
|
|
+ const scraperClient = await createScraperClient({
|
|
|
|
|
+ scraper_api_url: store.scraper_api_url,
|
|
|
|
|
+ scraper_api_secret: store.scraper_api_secret
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const shopData = await scraperClient.getShop(store.id);
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ registered: true,
|
|
|
|
|
+ enabled: store.scraper_enabled,
|
|
|
|
|
+ shop_data: shopData
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`Failed to get shop status for ${storeId}:`, error);
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ registered: true,
|
|
|
|
|
+ enabled: store.scraper_enabled,
|
|
|
|
|
+ error: 'Failed to fetch shop data from scraper',
|
|
|
|
|
+ message: error.message
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case 'shop-content': {
|
|
|
|
|
+ // GET /shop-content?store_id=xxx&content_type=faq&limit=50
|
|
|
|
|
+ if (req.method !== 'GET') {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Method not allowed'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const storeId = url.searchParams.get('store_id');
|
|
|
|
|
+ if (!storeId) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'store_id parameter is required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const store = await getStoreScraperConfig(supabase, storeId, user.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!store.scraper_registered) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Store not registered with scraper'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 404,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scraperClient = await createScraperClient({
|
|
|
|
|
+ scraper_api_url: store.scraper_api_url,
|
|
|
|
|
+ scraper_api_secret: store.scraper_api_secret
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Build filter from query parameters
|
|
|
|
|
+ const filter: any = {};
|
|
|
|
|
+ const contentType = url.searchParams.get('content_type');
|
|
|
|
|
+ const dateFrom = url.searchParams.get('date_from');
|
|
|
|
|
+ const dateTo = url.searchParams.get('date_to');
|
|
|
|
|
+ const limit = url.searchParams.get('limit');
|
|
|
|
|
+
|
|
|
|
|
+ if (contentType) filter.content_type = contentType;
|
|
|
|
|
+ if (dateFrom) filter.date_from = dateFrom;
|
|
|
|
|
+ if (dateTo) filter.date_to = dateTo;
|
|
|
|
|
+ if (limit) filter.limit = parseInt(limit, 10);
|
|
|
|
|
+
|
|
|
|
|
+ const content = await scraperClient.getShopContent(store.id, filter);
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(JSON.stringify(content), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case 'custom-urls': {
|
|
|
|
|
+ const storeId = url.searchParams.get('store_id') || (await req.json())?.store_id;
|
|
|
|
|
+ if (!storeId) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'store_id is required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const store = await getStoreScraperConfig(supabase, storeId, user.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!store.scraper_registered) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Store not registered with scraper'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 404,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scraperClient = await createScraperClient({
|
|
|
|
|
+ scraper_api_url: store.scraper_api_url,
|
|
|
|
|
+ scraper_api_secret: store.scraper_api_secret
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (req.method === 'GET') {
|
|
|
|
|
+ // List custom URLs
|
|
|
|
|
+ const customUrls = await scraperClient.listCustomUrls(store.id);
|
|
|
|
|
+ return new Response(JSON.stringify(customUrls), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (req.method === 'POST') {
|
|
|
|
|
+ // Add custom URL
|
|
|
|
|
+ const { url: customUrl, content_type } = await req.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (!customUrl || !content_type) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'url and content_type are required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate same domain
|
|
|
|
|
+ if (!validateSameDomain(store.store_url, customUrl)) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Custom URL must be from the same domain as the store'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result = await scraperClient.addCustomUrl(store.id, customUrl, content_type);
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(JSON.stringify(result), {
|
|
|
|
|
+ status: 201,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Method not allowed'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ case 'scheduling': {
|
|
|
|
|
+ // PATCH /scheduling
|
|
|
|
|
+ if (req.method !== 'PATCH') {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Method not allowed'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { store_id, enabled } = await req.json();
|
|
|
|
|
+ if (!store_id || typeof enabled !== 'boolean') {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'store_id and enabled (boolean) are required'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const store = await getStoreScraperConfig(supabase, store_id, user.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!store.scraper_registered) {
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Store not registered with scraper'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 404,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scraperClient = await createScraperClient({
|
|
|
|
|
+ scraper_api_url: store.scraper_api_url,
|
|
|
|
|
+ scraper_api_secret: store.scraper_api_secret
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ await scraperClient.setScheduling(store.id, enabled);
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ message: `Scheduling ${enabled ? 'enabled' : 'disabled'}`
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Unknown endpoint'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 404,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error in scraper management API:', error);
|
|
|
|
|
+ return new Response(JSON.stringify({
|
|
|
|
|
+ error: 'Internal server error',
|
|
|
|
|
+ message: error instanceof Error ? error.message : 'Unknown error'
|
|
|
|
|
+ }), {
|
|
|
|
|
+ status: 500,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...corsHeaders,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+});
|