mcpField
S
supabase-database-ops

supabase-database-ops

github.com/venture-formations安全136 次安装8 周前更新
代码仓库

关于 supabase-database-ops

supabase-database-ops 是面向 AIProDaily 开发的数据库操作安全护栏,适用于编写 Supabase 查询和数据库 API。它强制关注租户隔离、服务端访问、字段选择与错误检查,以降低跨租户数据泄漏风险。

主要功能

  1. 01租户表查询必须过滤 publication_id
  2. 02仅在服务端使用 supabaseAdmin
  3. 03避免无必要的 SELECT *
  4. 04检查数据库操作错误
  5. 05为插入、更新和删除提供模式

适用场景

  1. 01编写租户范围内的 Supabase 查询
  2. 02开发访问数据库的 API 路由
  3. 03修改文章、活动或 RSS 数据
  4. 04排查行级安全策略错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/venture-formations/aiprodaily --skill supabase-database-ops

安装后在 Claude Code 中输入/supabase-database-ops确认已加载。

详细介绍

Supabase 数据库操作——关键防护措施

目的

关键防护措施:防止 AIProDaily 平台发生多租户数据泄露,并确保遵循数据库最佳实践。

使用时机

在以下情况下,此技能会阻止数据库操作,直至完成验证:

  • 编写 Supabase 查询(supabaseAdmin.from())
  • 访问按租户划分的表
  • 创建可访问数据库的 API 路由
  • 处理营销活动、文章或 RSS 数据

🚨 关键规则 🚨

规则 #1:始终按 publication_id 筛选

对按租户划分的表执行的每个查询都必须包含 publication_id 筛选条件。

// ✅ CORRECT - publication_id filter present
const { data, error } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select('id, status, date')
  .eq('publication_id', newsletterId)  // ✅ REQUIRED
  .eq('id', campaignId)
  .single()

// ❌ WRONG - Missing publication_id filter (DATA LEAKAGE!)
const { data, error } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select('id, status, date')
  .eq('id', campaignId)  // ❌ Can access other tenants' data!
  .single()
按租户划分的表(必须按 publication_id 筛选):
  • newsletter_campaigns
  • articles
  • secondary_articles
  • rss_posts
  • post_ratings
  • rss_feeds
  • app_settings
  • advertisements
  • campaign_advertisements
  • archived_articles
  • archived_rss_posts
非按租户划分的表(无需 publication_id):
  • newsletters(顶层租户表)
  • 全系统配置表

规则 #2:服务端操作使用 supabaseAdmin

切勿在客户端暴露服务角色密钥。

// ✅ CORRECT - Server-side API route or Server Action
import { supabaseAdmin } from '@/lib/supabase'

export async function POST(request: NextRequest) {
  const { data } = await supabaseAdmin
    .from('newsletter_campaigns')
    .select('*')
    .eq('publication_id', newsletterId)

  return NextResponse.json({ data })
}

// ❌ WRONG - Never in client components
'use client'
import { supabaseAdmin } from '@/lib/supabase'  // ❌ Security risk!

export default function ClientComponent() {
  // This exposes service role key to browser
  const { data } = await supabaseAdmin.from('...').select()
}

适用场景:

  • ✅ API 路由(app/api/**/*.ts)
  • ✅ 服务端操作('use server' 函数)
  • ✅ 服务端组件(不含 'use client')
  • ✅ 后台任务/定时任务
  • ✅ 工作流步骤

不适用场景:

  • ❌ 客户端组件('use client')
  • ❌ 在浏览器中执行的代码
  • ❌ 面向公众的页面

规则 #3:避免使用 SELECT *

只选择所需字段。

// ✅ CORRECT - Specific fields
const { data } = await supabaseAdmin
  .from('articles')
  .select('id, headline, article_text, is_active')
  .eq('publication_id', newsletterId)
  .eq('campaign_id', campaignId)

// ❌ WRONG - Fetches all columns (performance impact)
const { data } = await supabaseAdmin
  .from('articles')
  .select('*')
  .eq('publication_id', newsletterId)
  .eq('campaign_id', campaignId)

例外情况:确实需要所有列来执行数据操作时。


规则 #4:始终检查错误

切勿假定数据库操作会成功。

// ✅ CORRECT - Check for errors
const { data, error } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select('id, status')
  .eq('publication_id', newsletterId)
  .eq('id', campaignId)
  .single()

if (error) {
  console.error('[DB] Query failed:', error.message)
  throw new Error('Failed to fetch campaign')
}

if (!data) {
  console.log('[DB] No campaign found')
  return null
}

// Now safe to use data
return data

// ❌ WRONG - No error handling
const { data } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select('id, status')
  .eq('id', campaignId)
  .single()

return data.status  // ❌ Crashes if error or data is null

数据库查询模式

标准查询模式
const { data, error } = await supabaseAdmin
  .from('table_name')
  .select('field1, field2, field3')
  .eq('publication_id', newsletterId)  // ✅ ALWAYS for tenant tables
  .eq('other_field', value)
  .single()  // or .maybeSingle() if record might not exist

if (error) {
  console.error('[DB] Query error:', error.message)
  throw new Error(`Database query failed: ${error.message}`)
}

if (!data) {
  console.log('[DB] No record found')
  return null
}

return data
插入模式
const { data, error } = await supabaseAdmin
  .from('articles')
  .insert({
    publication_id: newsletterId,  // ✅ REQUIRED
    campaign_id: campaignId,
    headline: 'Article headline',
    article_text: 'Content here',
    is_active: false
  })
  .select()
  .single()

if (error) {
  console.error('[DB] Insert failed:', error.message)
  throw new Error('Failed to create article')
}

return data
更新模式
const { data, error } = await supabaseAdmin
  .from('articles')
  .update({
    is_active: true,
    updated_at: new Date().toISOString()
  })
  .eq('id', articleId)
  .eq('publication_id', newsletterId)  // ✅ REQUIRED - prevents updating other tenants
  .select()
  .single()

if (error) {
  console.error('[DB] Update failed:', error.message)
  throw new Error('Failed to update article')
}

return data
删除模式
const { error } = await supabaseAdmin
  .from('rss_posts')
  .delete()
  .eq('id', postId)
  .eq('publication_id', newsletterId)  // ✅ REQUIRED - prevents deleting other tenants' data

if (error) {
  console.error('[DB] Delete failed:', error.message)
  throw new Error('Failed to delete post')
}
联接模式(关系)
const { data, error } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select(`
    id,
    status,
    date,
    articles (
      id,
      headline,
      is_active
    ),
    secondary_articles (
      id,
      headline,
      is_active
    )
  `)
  .eq('publication_id', newsletterId)  // ✅ REQUIRED on parent table
  .eq('id', campaignId)
  .single()

常见错误

❌ 忘记添加 publication_id 筛选条件
// This query can access ANY campaign from ANY tenant!
const { data } = await supabaseAdmin
  .from('newsletter_campaigns')
  .select('*')
  .eq('id', campaignId)  // ❌ Missing publication_id
❌ 在客户端使用 supabaseAdmin
'use client'

// ❌ Exposes service role key to browser
export default function MyComponent() {
  const { data } = await supabaseAdmin.from('...').select()
}
❌ 未处理错误
// ❌ No error check - will crash on failure
const { data } = await supabaseAdmin.from('...').select().single()
const status = data.status  // Crashes if data is null
❌ 使用 SELECT *
// ❌ Fetches unnecessary data, impacts performance
const { data } = await supabaseAdmin
  .from('articles')
  .select('*')

快速参考

✅ 应做事项:

  • 对按租户划分的表始终按 publication_id 筛选
  • 仅在服务端使用 supabaseAdmin
  • 选择特定字段
  • 检查错误
  • 查询单条记录时使用 .single()
  • 记录可能不存在时使用 .maybeSingle()
  • 使用 [DB] 前缀记录错误

❌ 不应做事项:

  • 跳过 publication_id 筛选条件
  • 在客户端组件中使用 supabaseAdmin
  • 无故使用 SELECT *
  • 忽略错误
  • 假定数据存在
  • 在客户端暴露服务密钥

错误恢复

如果看到“Row level security policy violated”:

  1. 检查是否按 publication_id 筛选
  2. 确认使用的是 supabaseAdmin(而非客户端)
  3. 确认代码运行在服务端(API 路由/服务端操作)

如果看到“column does not exist”:

  1. 确认列名拼写正确
  2. 检查数据库架构中是否存在该字段
  3. 确保查询的是正确的表

技能状态:防护措施已启用 ✅ 强制级别:阻止(关键) 行数:< 500 ✅ 目的:防止多租户数据泄露 ✅

评论

支持 Markdown