mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 01:51:13 -05:00
Implement planned features
This commit is contained in:
@@ -112,6 +112,56 @@ serve(async (req) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Generate idempotency key for duplicate prevention
|
||||
const { data: keyData, error: keyError } = await supabase
|
||||
.rpc('generate_notification_idempotency_key', {
|
||||
p_notification_type: 'moderation_submission',
|
||||
p_entity_id: submission_id,
|
||||
p_recipient_id: '00000000-0000-0000-0000-000000000000', // Topic-based, use placeholder
|
||||
p_event_data: { submission_type, action }
|
||||
});
|
||||
|
||||
const idempotencyKey = keyData || `mod_sub_${submission_id}_${Date.now()}`;
|
||||
|
||||
// Check for duplicate within 24h window
|
||||
const { data: existingLog, error: logCheckError } = await supabase
|
||||
.from('notification_logs')
|
||||
.select('id')
|
||||
.eq('idempotency_key', idempotencyKey)
|
||||
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
|
||||
.maybeSingle();
|
||||
|
||||
if (existingLog) {
|
||||
// Duplicate detected - log and skip
|
||||
await supabase.from('notification_logs').update({
|
||||
is_duplicate: true
|
||||
}).eq('id', existingLog.id);
|
||||
|
||||
edgeLogger.info('Duplicate notification prevented', {
|
||||
action: 'notify_moderators',
|
||||
requestId: tracking.requestId,
|
||||
idempotencyKey,
|
||||
submission_id
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Duplicate notification prevented',
|
||||
idempotencyKey,
|
||||
requestId: tracking.requestId,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
},
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare enhanced notification payload
|
||||
const notificationPayload = {
|
||||
baseUrl: 'https://www.thrillwiki.com',
|
||||
@@ -146,6 +196,19 @@ serve(async (req) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Log notification in notification_logs with idempotency key
|
||||
await supabase.from('notification_logs').insert({
|
||||
user_id: '00000000-0000-0000-0000-000000000000', // Topic-based
|
||||
notification_type: 'moderation_submission',
|
||||
idempotency_key: idempotencyKey,
|
||||
is_duplicate: false,
|
||||
metadata: {
|
||||
submission_id,
|
||||
submission_type,
|
||||
transaction_id: data?.transactionId
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const duration = endRequest(tracking);
|
||||
edgeLogger.error('Failed to notify moderators via topic', {
|
||||
|
||||
@@ -151,11 +151,64 @@ serve(async (req) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Generate idempotency key for duplicate prevention
|
||||
const { data: keyData, error: keyError } = await supabase
|
||||
.rpc('generate_notification_idempotency_key', {
|
||||
p_notification_type: `submission_${status}`,
|
||||
p_entity_id: submission_id,
|
||||
p_recipient_id: user_id,
|
||||
});
|
||||
|
||||
const idempotencyKey = keyData || `user_sub_${submission_id}_${user_id}_${status}_${Date.now()}`;
|
||||
|
||||
// Check for duplicate within 24h window
|
||||
const { data: existingLog, error: logCheckError } = await supabase
|
||||
.from('notification_logs')
|
||||
.select('id')
|
||||
.eq('user_id', user_id)
|
||||
.eq('idempotency_key', idempotencyKey)
|
||||
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
|
||||
.maybeSingle();
|
||||
|
||||
if (existingLog) {
|
||||
// Duplicate detected - log and skip
|
||||
await supabase.from('notification_logs').update({
|
||||
is_duplicate: true
|
||||
}).eq('id', existingLog.id);
|
||||
|
||||
console.log('Duplicate notification prevented:', {
|
||||
userId: user_id,
|
||||
idempotencyKey,
|
||||
submissionId: submission_id,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
endRequest(tracking, 200);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Duplicate notification prevented',
|
||||
idempotencyKey,
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
},
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Sending notification to user:', {
|
||||
userId: user_id,
|
||||
workflowId,
|
||||
entityName,
|
||||
status,
|
||||
idempotencyKey,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
@@ -175,6 +228,19 @@ serve(async (req) => {
|
||||
throw new Error(`Failed to trigger notification: ${notificationError.message}`);
|
||||
}
|
||||
|
||||
// Log notification in notification_logs with idempotency key
|
||||
await supabase.from('notification_logs').insert({
|
||||
user_id,
|
||||
notification_type: `submission_${status}`,
|
||||
idempotency_key: idempotencyKey,
|
||||
is_duplicate: false,
|
||||
metadata: {
|
||||
submission_id,
|
||||
submission_type,
|
||||
transaction_id: notificationResult?.transactionId
|
||||
}
|
||||
});
|
||||
|
||||
console.log('User notification sent successfully:', notificationResult);
|
||||
|
||||
endRequest(tracking, 200);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- Phase 1: Add conflict resolution tracking
|
||||
CREATE TABLE IF NOT EXISTS public.conflict_resolutions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
submission_id UUID NOT NULL REFERENCES public.content_submissions(id) ON DELETE CASCADE,
|
||||
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
resolved_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
resolution_strategy TEXT NOT NULL CHECK (resolution_strategy IN ('keep-mine', 'keep-theirs', 'reload', 'merge')),
|
||||
conflict_details JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_conflict_resolutions_submission
|
||||
ON public.conflict_resolutions(submission_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conflict_resolutions_detected_at
|
||||
ON public.conflict_resolutions(detected_at DESC);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.conflict_resolutions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Moderators can view all conflict resolutions
|
||||
CREATE POLICY "Moderators can view conflict resolutions"
|
||||
ON public.conflict_resolutions
|
||||
FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles
|
||||
WHERE user_id = auth.uid()
|
||||
AND role IN ('moderator', 'admin', 'superuser')
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Moderators can insert conflict resolutions
|
||||
CREATE POLICY "Moderators can insert conflict resolutions"
|
||||
ON public.conflict_resolutions
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles
|
||||
WHERE user_id = auth.uid()
|
||||
AND role IN ('moderator', 'admin', 'superuser')
|
||||
)
|
||||
AND resolved_by = auth.uid()
|
||||
);
|
||||
|
||||
-- Add index for notification deduplication performance (Phase 3)
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_logs_dedup
|
||||
ON public.notification_logs(user_id, idempotency_key, created_at);
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON TABLE public.conflict_resolutions IS 'Tracks resolution of concurrent edit conflicts in moderation system';
|
||||
Reference in New Issue
Block a user