index.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
  2. import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
  3. import { isValidExternalUrl, verifySuperUser } from '../_shared/security.ts'
  4. const corsHeaders = {
  5. 'Access-Control-Allow-Origin': '*',
  6. 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  7. }
  8. serve(async (req) => {
  9. if (req.method === 'OPTIONS') {
  10. return new Response(null, { headers: corsHeaders })
  11. }
  12. try {
  13. // Authentication: Only super users can update feeds
  14. const isSuperUser = await verifySuperUser(req);
  15. if (!isSuperUser) {
  16. console.log('Unauthorized access attempt to update-feed');
  17. return new Response(
  18. JSON.stringify({ error: 'Unauthorized - Super user access required' }),
  19. { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  20. )
  21. }
  22. const { feedId, url } = await req.json()
  23. // Input validation
  24. if (!feedId || typeof feedId !== 'string') {
  25. return new Response(
  26. JSON.stringify({ error: 'Valid Feed ID is required' }),
  27. { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  28. )
  29. }
  30. if (!url || typeof url !== 'string') {
  31. return new Response(
  32. JSON.stringify({ error: 'Valid URL is required' }),
  33. { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  34. )
  35. }
  36. // SSRF Protection: Validate URL
  37. const urlValidation = isValidExternalUrl(url);
  38. if (!urlValidation.valid) {
  39. console.log(`SSRF blocked in update-feed: ${url} - ${urlValidation.error}`);
  40. return new Response(
  41. JSON.stringify({ error: urlValidation.error }),
  42. { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  43. )
  44. }
  45. // Initialize Supabase client
  46. const supabase = createClient(
  47. Deno.env.get('SUPABASE_URL') ?? '',
  48. Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  49. )
  50. // Verify the feed exists
  51. const { data: existingFeed, error: feedCheckError } = await supabase
  52. .from('feeds')
  53. .select('id')
  54. .eq('id', feedId)
  55. .single()
  56. if (feedCheckError || !existingFeed) {
  57. return new Response(
  58. JSON.stringify({ error: 'Feed not found' }),
  59. { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  60. )
  61. }
  62. console.log(`Fetching RSS feed from: ${url}`)
  63. // Fetch RSS feed with timeout
  64. const controller = new AbortController();
  65. const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
  66. let response;
  67. try {
  68. response = await fetch(url, {
  69. signal: controller.signal,
  70. headers: {
  71. 'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Updater/1.0)'
  72. }
  73. })
  74. } finally {
  75. clearTimeout(timeoutId);
  76. }
  77. if (!response.ok) {
  78. throw new Error(`HTTP error! status: ${response.status}`)
  79. }
  80. const rssText = await response.text()
  81. // Limit content size
  82. if (rssText.length > 5 * 1024 * 1024) { // 5MB limit
  83. throw new Error('RSS feed content too large');
  84. }
  85. console.log('RSS content fetched successfully')
  86. // Parse RSS content to extract metadata
  87. const titleMatch = rssText.match(/<title[^>]*>(.*?)<\/title>/i)
  88. const descriptionMatch = rssText.match(/<description[^>]*>(.*?)<\/description>/i) ||
  89. rssText.match(/<subtitle[^>]*>(.*?)<\/subtitle>/i)
  90. // Count items in the feed
  91. const itemMatches = rssText.match(/<item[^>]*>/gi) || rssText.match(/<entry[^>]*>/gi) || []
  92. const articleCount = itemMatches.length
  93. // Clean up extracted text (remove HTML tags and decode entities)
  94. const cleanText = (text: string) => {
  95. if (!text) return ''
  96. return text
  97. .replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1')
  98. .replace(/<[^>]+>/g, '')
  99. .replace(/&lt;/g, '<')
  100. .replace(/&gt;/g, '>')
  101. .replace(/&amp;/g, '&')
  102. .replace(/&quot;/g, '"')
  103. .replace(/&#39;/g, "'")
  104. .trim()
  105. }
  106. const extractedTitle = titleMatch ? cleanText(titleMatch[1]) : null
  107. const extractedDescription = descriptionMatch ? cleanText(descriptionMatch[1]) : null
  108. console.log(`Extracted data: title="${extractedTitle}", description="${extractedDescription}", articles=${articleCount}`)
  109. // Update feed in database
  110. const updateData: any = {
  111. last_updated: new Date().toISOString(),
  112. article_count: articleCount,
  113. status: 'active'
  114. }
  115. // Only update title and description if we found them and they're different
  116. if (extractedTitle) {
  117. updateData.name = extractedTitle
  118. }
  119. if (extractedDescription) {
  120. updateData.description = extractedDescription
  121. }
  122. const { data, error } = await supabase
  123. .from('feeds')
  124. .update(updateData)
  125. .eq('id', feedId)
  126. .select()
  127. if (error) {
  128. console.error('Database update error:', error)
  129. return new Response(
  130. JSON.stringify({ error: 'Failed to update feed in database' }),
  131. { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  132. )
  133. }
  134. console.log('Feed updated successfully')
  135. return new Response(
  136. JSON.stringify({
  137. success: true,
  138. data: data[0],
  139. extracted: {
  140. title: extractedTitle,
  141. description: extractedDescription,
  142. articleCount
  143. }
  144. }),
  145. { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  146. )
  147. } catch (error: any) {
  148. console.error('Error updating feed:', error)
  149. return new Response(
  150. JSON.stringify({ error: 'An error occurred while updating the feed' }),
  151. { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  152. )
  153. }
  154. })