fix(secrets): store ops-created secrets encrypted via createSecret (security)
The ops 'Add Secret' modal wrote the secret value straight to the DB from the browser in PLAINTEXT — despite the 'Stored encrypted at rest' label — because the AES key is server-only. Route it through the createSecret server action, which encrypts the value (AES-256-GCM), enforces the admin/board gate, validates input, and logs the activity. Existing plaintext rows (encryption_version 0) still reveal correctly; new secrets are encrypted when a key is configured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9100532bef
commit
71ba558522
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { createClient } from '@/lib/supabase/client'
|
import { createSecret } from '@/lib/actions'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import type { TablesInsert } from '@/types/database'
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import type { Secret } from '../types'
|
import type { Secret } from '../types'
|
||||||
|
|
||||||
@@ -11,11 +10,9 @@ import type { Secret } from '../types'
|
|||||||
export function AddSecretModal({
|
export function AddSecretModal({
|
||||||
onClose,
|
onClose,
|
||||||
onSaved,
|
onSaved,
|
||||||
spaceId,
|
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSaved: (s: Secret) => void
|
onSaved: (s: Secret) => void
|
||||||
spaceId: string
|
|
||||||
}) {
|
}) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [area, setArea] = useState('')
|
const [area, setArea] = useState('')
|
||||||
@@ -26,22 +23,20 @@ export function AddSecretModal({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim() || !value.trim()) return
|
if (!title.trim() || !value.trim()) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
const supabase = createClient()
|
// Use the createSecret server action, which AES-encrypts the value at rest
|
||||||
const { data, error } = await supabase.from('secrets').insert({
|
// (and applies the admin/board authz gate + Zod validation + activity log).
|
||||||
space_id: spaceId,
|
// The previous direct browser insert wrote the value in PLAINTEXT — despite
|
||||||
|
// the "Stored encrypted at rest" promise — since the encryption key is
|
||||||
|
// server-only and never reaches the browser.
|
||||||
|
const result = await createSecret({
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
label: title.trim(),
|
|
||||||
area: area.trim() || null,
|
|
||||||
value: value.trim(),
|
value: value.trim(),
|
||||||
// `satisfies` keeps the payload type-checked; the trailing `as never`
|
area: area.trim() || undefined,
|
||||||
// only bridges the typed browser client whose .insert() generic collapses
|
})
|
||||||
// to `never`.
|
|
||||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
|
||||||
} satisfies TablesInsert<'secrets'> as never).select('id, title, area, created_at').single()
|
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
if (error) { toast.error(error.message); return }
|
if (result.error) { toast.error(result.error); return }
|
||||||
toast.success('Secret saved')
|
toast.success('Secret saved')
|
||||||
onSaved(data as Secret)
|
onSaved(result.data as Secret)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ export function OpsClient({ spaceId, kbEntries: initial, areaLeads: initialLeads
|
|||||||
<AddSecretModal
|
<AddSecretModal
|
||||||
onClose={() => setShowSecretModal(false)}
|
onClose={() => setShowSecretModal(false)}
|
||||||
onSaved={s => setSecrets(prev => [s, ...prev])}
|
onSaved={s => setSecrets(prev => [s, ...prev])}
|
||||||
spaceId={spaceId}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showLeadModal && (
|
{showLeadModal && (
|
||||||
|
|||||||
Reference in New Issue
Block a user