Compare commits

...

3 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
036df6f5b7 Fix notification view security 2025-11-03 00:25:17 +00:00
gpt-engineer-app[bot]
a3b52b777c Fix notification logs migration 2025-11-03 00:24:38 +00:00
gpt-engineer-app[bot]
71bb364bce Fix: Correct database migrations 2025-11-03 00:23:04 +00:00
4 changed files with 198 additions and 0 deletions

View File

@@ -638,6 +638,8 @@ export type Database = {
first_reviewed_at: string | null first_reviewed_at: string | null
id: string id: string
is_test_data: boolean | null is_test_data: boolean | null
last_modified_at: string | null
last_modified_by: string | null
locked_until: string | null locked_until: string | null
original_submission_id: string | null original_submission_id: string | null
resolved_at: string | null resolved_at: string | null
@@ -664,6 +666,8 @@ export type Database = {
first_reviewed_at?: string | null first_reviewed_at?: string | null
id?: string id?: string
is_test_data?: boolean | null is_test_data?: boolean | null
last_modified_at?: string | null
last_modified_by?: string | null
locked_until?: string | null locked_until?: string | null
original_submission_id?: string | null original_submission_id?: string | null
resolved_at?: string | null resolved_at?: string | null
@@ -690,6 +694,8 @@ export type Database = {
first_reviewed_at?: string | null first_reviewed_at?: string | null
id?: string id?: string
is_test_data?: boolean | null is_test_data?: boolean | null
last_modified_at?: string | null
last_modified_by?: string | null
locked_until?: string | null locked_until?: string | null
original_submission_id?: string | null original_submission_id?: string | null
resolved_at?: string | null resolved_at?: string | null
@@ -1310,6 +1316,33 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
notification_duplicate_stats: {
Row: {
created_at: string | null
date: string
duplicates_prevented: number | null
id: string
prevention_rate: number | null
total_attempts: number | null
}
Insert: {
created_at?: string | null
date?: string
duplicates_prevented?: number | null
id?: string
prevention_rate?: number | null
total_attempts?: number | null
}
Update: {
created_at?: string | null
date?: string
duplicates_prevented?: number | null
id?: string
prevention_rate?: number | null
total_attempts?: number | null
}
Relationships: []
}
notification_logs: { notification_logs: {
Row: { Row: {
channel: string channel: string
@@ -1317,6 +1350,8 @@ export type Database = {
delivered_at: string | null delivered_at: string | null
error_message: string | null error_message: string | null
id: string id: string
idempotency_key: string | null
is_duplicate: boolean
novu_transaction_id: string | null novu_transaction_id: string | null
payload: Json | null payload: Json | null
read_at: string | null read_at: string | null
@@ -1330,6 +1365,8 @@ export type Database = {
delivered_at?: string | null delivered_at?: string | null
error_message?: string | null error_message?: string | null
id?: string id?: string
idempotency_key?: string | null
is_duplicate?: boolean
novu_transaction_id?: string | null novu_transaction_id?: string | null
payload?: Json | null payload?: Json | null
read_at?: string | null read_at?: string | null
@@ -1343,6 +1380,8 @@ export type Database = {
delivered_at?: string | null delivered_at?: string | null
error_message?: string | null error_message?: string | null
id?: string id?: string
idempotency_key?: string | null
is_duplicate?: boolean
novu_transaction_id?: string | null novu_transaction_id?: string | null
payload?: Json | null payload?: Json | null
read_at?: string | null read_at?: string | null
@@ -4608,6 +4647,16 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
notification_health_dashboard: {
Row: {
date: string | null
duplicates_prevented: number | null
health_status: string | null
prevention_rate: number | null
total_attempts: number | null
}
Relationships: []
}
} }
Functions: { Functions: {
anonymize_user_submissions: { anonymize_user_submissions: {
@@ -4686,6 +4735,15 @@ export type Database = {
} }
extract_cf_image_id: { Args: { url: string }; Returns: string } extract_cf_image_id: { Args: { url: string }; Returns: string }
generate_deletion_confirmation_code: { Args: never; Returns: string } generate_deletion_confirmation_code: { Args: never; Returns: string }
generate_notification_idempotency_key: {
Args: {
p_entity_id: string
p_event_data?: Json
p_notification_type: string
p_recipient_id: string
}
Returns: string
}
generate_ticket_number: { Args: never; Returns: string } generate_ticket_number: { Args: never; Returns: string }
get_auth0_sub_from_jwt: { Args: never; Returns: string } get_auth0_sub_from_jwt: { Args: never; Returns: string }
get_current_user_id: { Args: never; Returns: string } get_current_user_id: { Args: never; Returns: string }
@@ -4769,6 +4827,7 @@ export type Database = {
} }
hash_ip_address: { Args: { ip_text: string }; Returns: string } hash_ip_address: { Args: { ip_text: string }; Returns: string }
hash_session_ip: { Args: { session_ip: unknown }; Returns: string } hash_session_ip: { Args: { session_ip: unknown }; Returns: string }
immutable_date: { Args: { ts: string }; Returns: string }
increment_blog_view_count: { increment_blog_view_count: {
Args: { post_slug: string } Args: { post_slug: string }
Returns: undefined Returns: undefined

View File

@@ -0,0 +1,30 @@
-- Migration: Add conflict tracking to content_submissions
-- Add columns for optimistic locking
ALTER TABLE public.content_submissions
ADD COLUMN IF NOT EXISTS last_modified_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS last_modified_by UUID REFERENCES auth.users(id);
-- Create function to update modification tracking
CREATE OR REPLACE FUNCTION public.update_content_submission_modified()
RETURNS TRIGGER AS $$
BEGIN
NEW.last_modified_at = NOW();
NEW.last_modified_by = auth.uid();
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
-- Create trigger
DROP TRIGGER IF EXISTS trigger_update_submission_modified ON public.content_submissions;
CREATE TRIGGER trigger_update_submission_modified
BEFORE UPDATE ON public.content_submissions
FOR EACH ROW
EXECUTE FUNCTION public.update_content_submission_modified();
-- Add indexes for performance
CREATE INDEX IF NOT EXISTS idx_content_submissions_last_modified
ON public.content_submissions(last_modified_at);
CREATE INDEX IF NOT EXISTS idx_content_submissions_last_modified_by
ON public.content_submissions(last_modified_by);

View File

@@ -0,0 +1,89 @@
-- Migration: Add notification idempotency and duplicate prevention
-- Add idempotency columns to notification_logs
ALTER TABLE public.notification_logs
ADD COLUMN IF NOT EXISTS idempotency_key TEXT,
ADD COLUMN IF NOT EXISTS is_duplicate BOOLEAN NOT NULL DEFAULT false;
-- Create immutable function for date extraction (required for index)
CREATE OR REPLACE FUNCTION public.immutable_date(ts TIMESTAMPTZ)
RETURNS DATE AS $$
BEGIN
RETURN ts::DATE;
END;
$$ LANGUAGE plpgsql IMMUTABLE SECURITY DEFINER SET search_path = public;
-- Create unique index to prevent duplicates (same key per user per day)
DROP INDEX IF EXISTS idx_notification_logs_idempotency_unique;
CREATE UNIQUE INDEX idx_notification_logs_idempotency_unique
ON public.notification_logs (user_id, idempotency_key, immutable_date(created_at))
WHERE idempotency_key IS NOT NULL;
-- Index for monitoring duplicates
CREATE INDEX IF NOT EXISTS idx_notification_logs_duplicates
ON public.notification_logs(is_duplicate, created_at)
WHERE is_duplicate = true;
-- Create duplicate tracking stats table
CREATE TABLE IF NOT EXISTS public.notification_duplicate_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
date DATE NOT NULL DEFAULT CURRENT_DATE,
total_attempts INTEGER DEFAULT 0,
duplicates_prevented INTEGER DEFAULT 0,
prevention_rate DECIMAL(5,2) GENERATED ALWAYS AS (
CASE
WHEN total_attempts > 0 THEN (duplicates_prevented::DECIMAL / total_attempts * 100)
ELSE 0
END
) STORED,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(date)
);
-- Enable RLS on duplicate stats
ALTER TABLE public.notification_duplicate_stats ENABLE ROW LEVEL SECURITY;
-- Drop and recreate policy
DROP POLICY IF EXISTS "Admins can view duplicate stats" ON public.notification_duplicate_stats;
CREATE POLICY "Admins can view duplicate stats"
ON public.notification_duplicate_stats FOR SELECT
USING (is_moderator(auth.uid()));
-- Function to generate idempotency key
CREATE OR REPLACE FUNCTION public.generate_notification_idempotency_key(
p_notification_type TEXT,
p_entity_id UUID,
p_recipient_id UUID,
p_event_data JSONB DEFAULT NULL
)
RETURNS TEXT AS $$
BEGIN
RETURN encode(
digest(
p_notification_type ||
COALESCE(p_entity_id::TEXT, '') ||
p_recipient_id::TEXT ||
CURRENT_DATE::TEXT ||
COALESCE(p_event_data::TEXT, ''),
'sha256'
),
'hex'
);
END;
$$ LANGUAGE plpgsql IMMUTABLE SECURITY DEFINER SET search_path = public;
-- Create monitoring view for notification health
CREATE OR REPLACE VIEW public.notification_health_dashboard AS
SELECT
date,
total_attempts,
duplicates_prevented,
prevention_rate,
CASE
WHEN prevention_rate > 10 THEN 'critical'
WHEN prevention_rate > 5 THEN 'warning'
ELSE 'healthy'
END as health_status
FROM public.notification_duplicate_stats
ORDER BY date DESC
LIMIT 30;

View File

@@ -0,0 +1,20 @@
-- Fix: Remove SECURITY DEFINER from notification_health_dashboard view
-- Drop and recreate view without SECURITY DEFINER
DROP VIEW IF EXISTS public.notification_health_dashboard;
CREATE VIEW public.notification_health_dashboard
WITH (security_invoker = true) AS
SELECT
date,
total_attempts,
duplicates_prevented,
prevention_rate,
CASE
WHEN prevention_rate > 10 THEN 'critical'
WHEN prevention_rate > 5 THEN 'warning'
ELSE 'healthy'
END as health_status
FROM public.notification_duplicate_stats
ORDER BY date DESC
LIMIT 30;