realtimex-crm 0.7.14 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DealList-CZvo7G6M.js → DealList-DnGVfS15.js} +2 -2
- package/dist/assets/{DealList-CZvo7G6M.js.map → DealList-DnGVfS15.js.map} +1 -1
- package/dist/assets/index-DPrpo5Xq.js +159 -0
- package/dist/assets/index-DPrpo5Xq.js.map +1 -0
- package/dist/assets/index-kM1Og1AS.css +1 -0
- package/dist/index.html +1 -1
- package/dist/stats.html +1 -1
- package/package.json +2 -1
- package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
- package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
- package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +34 -0
- package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
- package/src/components/atomic-crm/layout/Header.tsx +14 -1
- package/src/components/atomic-crm/root/CRM.tsx +2 -0
- package/src/components/supabase/change-password-page.tsx +9 -1
- package/src/components/supabase/forgot-password-page.tsx +30 -0
- package/src/components/supabase/layout.tsx +6 -2
- package/src/components/ui/alert-dialog.tsx +155 -0
- package/src/lib/api-key-utils.ts +22 -0
- package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
- package/supabase/functions/_shared/webhookSignature.ts +23 -0
- package/supabase/functions/api-v1-activities/index.ts +137 -0
- package/supabase/functions/api-v1-companies/index.ts +166 -0
- package/supabase/functions/api-v1-contacts/index.ts +171 -0
- package/supabase/functions/api-v1-deals/index.ts +166 -0
- package/supabase/functions/webhook-dispatcher/index.ts +133 -0
- package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
- package/supabase/migrations/20251219120100_webhook_triggers.sql +171 -0
- package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
- package/dist/assets/index-CdoQZFIX.css +0 -1
- package/dist/assets/index-D0sWLaB1.js +0 -153
- package/dist/assets/index-D0sWLaB1.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
|
|
3
|
+
import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
|
|
4
|
+
import {
|
|
5
|
+
validateApiKey,
|
|
6
|
+
checkRateLimit,
|
|
7
|
+
hasScope,
|
|
8
|
+
logApiRequest,
|
|
9
|
+
} from "../_shared/apiKeyAuth.ts";
|
|
10
|
+
|
|
11
|
+
Deno.serve(async (req: Request) => {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
|
|
14
|
+
// Handle CORS
|
|
15
|
+
if (req.method === "OPTIONS") {
|
|
16
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate API key
|
|
20
|
+
const authResult = await validateApiKey(req);
|
|
21
|
+
if ("status" in authResult) {
|
|
22
|
+
return authResult; // Error response
|
|
23
|
+
}
|
|
24
|
+
const { apiKey } = authResult;
|
|
25
|
+
|
|
26
|
+
// Check rate limit
|
|
27
|
+
const rateLimitError = checkRateLimit(apiKey.id);
|
|
28
|
+
if (rateLimitError) {
|
|
29
|
+
await logApiRequest(
|
|
30
|
+
apiKey.id,
|
|
31
|
+
"/v1/contacts",
|
|
32
|
+
req.method,
|
|
33
|
+
429,
|
|
34
|
+
Date.now() - startTime,
|
|
35
|
+
req
|
|
36
|
+
);
|
|
37
|
+
return rateLimitError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(req.url);
|
|
42
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
43
|
+
// pathParts: ["api-v1-contacts", "{id}"]
|
|
44
|
+
const contactId = pathParts[1];
|
|
45
|
+
|
|
46
|
+
let response: Response;
|
|
47
|
+
|
|
48
|
+
if (req.method === "GET" && contactId) {
|
|
49
|
+
response = await getContact(apiKey, contactId);
|
|
50
|
+
} else if (req.method === "POST") {
|
|
51
|
+
response = await createContact(apiKey, req);
|
|
52
|
+
} else if (req.method === "PATCH" && contactId) {
|
|
53
|
+
response = await updateContact(apiKey, contactId, req);
|
|
54
|
+
} else if (req.method === "DELETE" && contactId) {
|
|
55
|
+
response = await deleteContact(apiKey, contactId);
|
|
56
|
+
} else {
|
|
57
|
+
response = createErrorResponse(404, "Not found");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const responseTime = Date.now() - startTime;
|
|
61
|
+
await logApiRequest(
|
|
62
|
+
apiKey.id,
|
|
63
|
+
url.pathname,
|
|
64
|
+
req.method,
|
|
65
|
+
response.status,
|
|
66
|
+
responseTime,
|
|
67
|
+
req
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return response;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const responseTime = Date.now() - startTime;
|
|
73
|
+
await logApiRequest(
|
|
74
|
+
apiKey.id,
|
|
75
|
+
new URL(req.url).pathname,
|
|
76
|
+
req.method,
|
|
77
|
+
500,
|
|
78
|
+
responseTime,
|
|
79
|
+
req,
|
|
80
|
+
error.message
|
|
81
|
+
);
|
|
82
|
+
return createErrorResponse(500, "Internal server error");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
async function getContact(apiKey: any, contactId: string) {
|
|
87
|
+
if (!hasScope(apiKey, "contacts:read")) {
|
|
88
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const id = parseInt(contactId, 10);
|
|
92
|
+
const { data, error } = await supabaseAdmin
|
|
93
|
+
.from("contacts")
|
|
94
|
+
.select("*")
|
|
95
|
+
.eq("id", id)
|
|
96
|
+
.single();
|
|
97
|
+
|
|
98
|
+
if (error || !data) {
|
|
99
|
+
return createErrorResponse(404, "Contact not found");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Response(JSON.stringify({ data }), {
|
|
103
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function createContact(apiKey: any, req: Request) {
|
|
108
|
+
if (!hasScope(apiKey, "contacts:write")) {
|
|
109
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const body = await req.json();
|
|
113
|
+
|
|
114
|
+
const { data, error } = await supabaseAdmin
|
|
115
|
+
.from("contacts")
|
|
116
|
+
.insert({
|
|
117
|
+
...body,
|
|
118
|
+
sales_id: apiKey.sales_id, // Associate with API key owner
|
|
119
|
+
})
|
|
120
|
+
.select()
|
|
121
|
+
.single();
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
return createErrorResponse(400, error.message);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Response(JSON.stringify({ data }), {
|
|
128
|
+
status: 201,
|
|
129
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function updateContact(apiKey: any, contactId: string, req: Request) {
|
|
134
|
+
if (!hasScope(apiKey, "contacts:write")) {
|
|
135
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const body = await req.json();
|
|
139
|
+
|
|
140
|
+
const { data, error } = await supabaseAdmin
|
|
141
|
+
.from("contacts")
|
|
142
|
+
.update(body)
|
|
143
|
+
.eq("id", parseInt(contactId, 10))
|
|
144
|
+
.select()
|
|
145
|
+
.single();
|
|
146
|
+
|
|
147
|
+
if (error || !data) {
|
|
148
|
+
return createErrorResponse(404, "Contact not found");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return new Response(JSON.stringify({ data }), {
|
|
152
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function deleteContact(apiKey: any, contactId: string) {
|
|
157
|
+
if (!hasScope(apiKey, "contacts:write")) {
|
|
158
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { error } = await supabaseAdmin
|
|
162
|
+
.from("contacts")
|
|
163
|
+
.delete()
|
|
164
|
+
.eq("id", parseInt(contactId, 10));
|
|
165
|
+
|
|
166
|
+
if (error) {
|
|
167
|
+
return createErrorResponse(404, "Contact not found");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
171
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
|
|
3
|
+
import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
|
|
4
|
+
import {
|
|
5
|
+
validateApiKey,
|
|
6
|
+
checkRateLimit,
|
|
7
|
+
hasScope,
|
|
8
|
+
logApiRequest,
|
|
9
|
+
} from "../_shared/apiKeyAuth.ts";
|
|
10
|
+
|
|
11
|
+
Deno.serve(async (req: Request) => {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
|
|
14
|
+
if (req.method === "OPTIONS") {
|
|
15
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const authResult = await validateApiKey(req);
|
|
19
|
+
if ("status" in authResult) {
|
|
20
|
+
return authResult;
|
|
21
|
+
}
|
|
22
|
+
const { apiKey } = authResult;
|
|
23
|
+
|
|
24
|
+
const rateLimitError = checkRateLimit(apiKey.id);
|
|
25
|
+
if (rateLimitError) {
|
|
26
|
+
await logApiRequest(
|
|
27
|
+
apiKey.id,
|
|
28
|
+
"/v1/deals",
|
|
29
|
+
req.method,
|
|
30
|
+
429,
|
|
31
|
+
Date.now() - startTime,
|
|
32
|
+
req
|
|
33
|
+
);
|
|
34
|
+
return rateLimitError;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(req.url);
|
|
39
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
40
|
+
const dealId = pathParts[1];
|
|
41
|
+
|
|
42
|
+
let response: Response;
|
|
43
|
+
|
|
44
|
+
if (req.method === "GET" && dealId) {
|
|
45
|
+
response = await getDeal(apiKey, dealId);
|
|
46
|
+
} else if (req.method === "POST") {
|
|
47
|
+
response = await createDeal(apiKey, req);
|
|
48
|
+
} else if (req.method === "PATCH" && dealId) {
|
|
49
|
+
response = await updateDeal(apiKey, dealId, req);
|
|
50
|
+
} else if (req.method === "DELETE" && dealId) {
|
|
51
|
+
response = await deleteDeal(apiKey, dealId);
|
|
52
|
+
} else {
|
|
53
|
+
response = createErrorResponse(404, "Not found");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const responseTime = Date.now() - startTime;
|
|
57
|
+
await logApiRequest(
|
|
58
|
+
apiKey.id,
|
|
59
|
+
url.pathname,
|
|
60
|
+
req.method,
|
|
61
|
+
response.status,
|
|
62
|
+
responseTime,
|
|
63
|
+
req
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return response;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const responseTime = Date.now() - startTime;
|
|
69
|
+
await logApiRequest(
|
|
70
|
+
apiKey.id,
|
|
71
|
+
new URL(req.url).pathname,
|
|
72
|
+
req.method,
|
|
73
|
+
500,
|
|
74
|
+
responseTime,
|
|
75
|
+
req,
|
|
76
|
+
error.message
|
|
77
|
+
);
|
|
78
|
+
return createErrorResponse(500, "Internal server error");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
async function getDeal(apiKey: any, dealId: string) {
|
|
83
|
+
if (!hasScope(apiKey, "deals:read")) {
|
|
84
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { data, error } = await supabaseAdmin
|
|
88
|
+
.from("deals")
|
|
89
|
+
.select("*")
|
|
90
|
+
.eq("id", dealId)
|
|
91
|
+
.single();
|
|
92
|
+
|
|
93
|
+
if (error || !data) {
|
|
94
|
+
return createErrorResponse(404, "Deal not found");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return new Response(JSON.stringify({ data }), {
|
|
98
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function createDeal(apiKey: any, req: Request) {
|
|
103
|
+
if (!hasScope(apiKey, "deals:write")) {
|
|
104
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const body = await req.json();
|
|
108
|
+
|
|
109
|
+
const { data, error } = await supabaseAdmin
|
|
110
|
+
.from("deals")
|
|
111
|
+
.insert({
|
|
112
|
+
...body,
|
|
113
|
+
sales_id: apiKey.sales_id,
|
|
114
|
+
})
|
|
115
|
+
.select()
|
|
116
|
+
.single();
|
|
117
|
+
|
|
118
|
+
if (error) {
|
|
119
|
+
return createErrorResponse(400, error.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new Response(JSON.stringify({ data }), {
|
|
123
|
+
status: 201,
|
|
124
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function updateDeal(apiKey: any, dealId: string, req: Request) {
|
|
129
|
+
if (!hasScope(apiKey, "deals:write")) {
|
|
130
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const body = await req.json();
|
|
134
|
+
|
|
135
|
+
const { data, error } = await supabaseAdmin
|
|
136
|
+
.from("deals")
|
|
137
|
+
.update(body)
|
|
138
|
+
.eq("id", dealId)
|
|
139
|
+
.select()
|
|
140
|
+
.single();
|
|
141
|
+
|
|
142
|
+
if (error || !data) {
|
|
143
|
+
return createErrorResponse(404, "Deal not found");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return new Response(JSON.stringify({ data }), {
|
|
147
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function deleteDeal(apiKey: any, dealId: string) {
|
|
152
|
+
if (!hasScope(apiKey, "deals:write")) {
|
|
153
|
+
return createErrorResponse(403, "Insufficient permissions");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { error } = await supabaseAdmin
|
|
157
|
+
.from("deals")
|
|
158
|
+
.delete()
|
|
159
|
+
.eq("id", dealId);
|
|
160
|
+
|
|
161
|
+
if (error) {
|
|
162
|
+
return createErrorResponse(404, "Deal not found");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
166
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
|
|
3
|
+
import { generateWebhookSignature } from "../_shared/webhookSignature.ts";
|
|
4
|
+
|
|
5
|
+
Deno.serve(async () => {
|
|
6
|
+
console.log("Webhook dispatcher running...");
|
|
7
|
+
|
|
8
|
+
// Get pending webhook deliveries
|
|
9
|
+
const { data: queueItems } = await supabaseAdmin
|
|
10
|
+
.from("webhook_queue")
|
|
11
|
+
.select("*, webhooks(*)")
|
|
12
|
+
.eq("status", "pending")
|
|
13
|
+
.lte("next_retry_at", new Date().toISOString())
|
|
14
|
+
.limit(50);
|
|
15
|
+
|
|
16
|
+
if (!queueItems || queueItems.length === 0) {
|
|
17
|
+
return new Response(JSON.stringify({ processed: 0 }), {
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`Processing ${queueItems.length} webhook deliveries`);
|
|
23
|
+
|
|
24
|
+
const results = await Promise.allSettled(
|
|
25
|
+
queueItems.map((item) => deliverWebhook(item))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const successful = results.filter((r) => r.status === "fulfilled").length;
|
|
29
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
30
|
+
|
|
31
|
+
return new Response(
|
|
32
|
+
JSON.stringify({ processed: queueItems.length, successful, failed }),
|
|
33
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function deliverWebhook(queueItem: any) {
|
|
38
|
+
const webhook = queueItem.webhooks;
|
|
39
|
+
|
|
40
|
+
// Mark as processing
|
|
41
|
+
await supabaseAdmin
|
|
42
|
+
.from("webhook_queue")
|
|
43
|
+
.update({ status: "processing" })
|
|
44
|
+
.eq("id", queueItem.id);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const payloadString = JSON.stringify(queueItem.payload);
|
|
48
|
+
const signature = await generateWebhookSignature(
|
|
49
|
+
webhook.secret,
|
|
50
|
+
payloadString
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const headers: Record<string, string> = {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"X-Webhook-Signature": signature,
|
|
56
|
+
"X-Webhook-Event": queueItem.event_type,
|
|
57
|
+
"User-Agent": "AtomicCRM-Webhooks/1.0",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Add custom headers from webhook config
|
|
61
|
+
if (webhook.headers) {
|
|
62
|
+
Object.assign(headers, webhook.headers);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await fetch(webhook.url, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers,
|
|
68
|
+
body: payloadString,
|
|
69
|
+
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
// Success
|
|
74
|
+
await supabaseAdmin
|
|
75
|
+
.from("webhook_queue")
|
|
76
|
+
.update({
|
|
77
|
+
status: "delivered",
|
|
78
|
+
delivered_at: new Date().toISOString(),
|
|
79
|
+
})
|
|
80
|
+
.eq("id", queueItem.id);
|
|
81
|
+
|
|
82
|
+
await supabaseAdmin
|
|
83
|
+
.from("webhooks")
|
|
84
|
+
.update({
|
|
85
|
+
last_triggered_at: new Date().toISOString(),
|
|
86
|
+
failure_count: 0,
|
|
87
|
+
})
|
|
88
|
+
.eq("id", webhook.id);
|
|
89
|
+
|
|
90
|
+
console.log(`Webhook ${queueItem.id} delivered successfully`);
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Webhook ${queueItem.id} delivery failed:`, error);
|
|
96
|
+
|
|
97
|
+
const newAttempts = queueItem.attempts + 1;
|
|
98
|
+
const shouldRetry = newAttempts < queueItem.max_attempts;
|
|
99
|
+
|
|
100
|
+
if (shouldRetry) {
|
|
101
|
+
// Exponential backoff: 1min, 5min, 15min
|
|
102
|
+
const retryDelays = [60, 300, 900];
|
|
103
|
+
const delaySeconds = retryDelays[newAttempts - 1] || 900;
|
|
104
|
+
const nextRetry = new Date(Date.now() + delaySeconds * 1000)
|
|
105
|
+
.toISOString();
|
|
106
|
+
|
|
107
|
+
await supabaseAdmin
|
|
108
|
+
.from("webhook_queue")
|
|
109
|
+
.update({
|
|
110
|
+
status: "pending",
|
|
111
|
+
attempts: newAttempts,
|
|
112
|
+
next_retry_at: nextRetry,
|
|
113
|
+
error_message: error.message,
|
|
114
|
+
})
|
|
115
|
+
.eq("id", queueItem.id);
|
|
116
|
+
} else {
|
|
117
|
+
// Max attempts reached
|
|
118
|
+
await supabaseAdmin
|
|
119
|
+
.from("webhook_queue")
|
|
120
|
+
.update({
|
|
121
|
+
status: "failed",
|
|
122
|
+
attempts: newAttempts,
|
|
123
|
+
error_message: error.message,
|
|
124
|
+
})
|
|
125
|
+
.eq("id", queueItem.id);
|
|
126
|
+
|
|
127
|
+
await supabaseAdmin
|
|
128
|
+
.from("webhooks")
|
|
129
|
+
.update({ failure_count: webhook.failure_count + 1 })
|
|
130
|
+
.eq("id", webhook.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- API Keys table
|
|
2
|
+
create table "public"."api_keys" (
|
|
3
|
+
"id" bigint generated by default as identity not null,
|
|
4
|
+
"created_at" timestamp with time zone not null default now(),
|
|
5
|
+
"sales_id" bigint not null,
|
|
6
|
+
"name" text not null,
|
|
7
|
+
"key_hash" text not null,
|
|
8
|
+
"key_prefix" text not null,
|
|
9
|
+
"scopes" text[] not null default '{}',
|
|
10
|
+
"is_active" boolean not null default true,
|
|
11
|
+
"expires_at" timestamp with time zone,
|
|
12
|
+
"last_used_at" timestamp with time zone,
|
|
13
|
+
"created_by_sales_id" bigint not null
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
alter table "public"."api_keys" enable row level security;
|
|
17
|
+
|
|
18
|
+
CREATE UNIQUE INDEX api_keys_pkey ON public.api_keys USING btree (id);
|
|
19
|
+
CREATE UNIQUE INDEX api_keys_key_hash_unique ON public.api_keys USING btree (key_hash);
|
|
20
|
+
CREATE INDEX api_keys_sales_id_idx ON public.api_keys USING btree (sales_id);
|
|
21
|
+
|
|
22
|
+
alter table "public"."api_keys" add constraint "api_keys_pkey" PRIMARY KEY using index "api_keys_pkey";
|
|
23
|
+
alter table "public"."api_keys" add constraint "api_keys_sales_id_fkey"
|
|
24
|
+
FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
25
|
+
alter table "public"."api_keys" validate constraint "api_keys_sales_id_fkey";
|
|
26
|
+
alter table "public"."api_keys" add constraint "api_keys_created_by_sales_id_fkey"
|
|
27
|
+
FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
28
|
+
alter table "public"."api_keys" validate constraint "api_keys_created_by_sales_id_fkey";
|
|
29
|
+
|
|
30
|
+
-- Webhooks table
|
|
31
|
+
create table "public"."webhooks" (
|
|
32
|
+
"id" bigint generated by default as identity not null,
|
|
33
|
+
"created_at" timestamp with time zone not null default now(),
|
|
34
|
+
"sales_id" bigint not null,
|
|
35
|
+
"name" text not null,
|
|
36
|
+
"url" text not null,
|
|
37
|
+
"events" text[] not null default '{}',
|
|
38
|
+
"headers" jsonb,
|
|
39
|
+
"is_active" boolean not null default true,
|
|
40
|
+
"secret" text not null,
|
|
41
|
+
"last_triggered_at" timestamp with time zone,
|
|
42
|
+
"failure_count" int not null default 0,
|
|
43
|
+
"created_by_sales_id" bigint not null
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
alter table "public"."webhooks" enable row level security;
|
|
47
|
+
|
|
48
|
+
CREATE UNIQUE INDEX webhooks_pkey ON public.webhooks USING btree (id);
|
|
49
|
+
CREATE INDEX webhooks_sales_id_idx ON public.webhooks USING btree (sales_id);
|
|
50
|
+
CREATE INDEX webhooks_events_idx ON public.webhooks USING gin (events);
|
|
51
|
+
|
|
52
|
+
alter table "public"."webhooks" add constraint "webhooks_pkey" PRIMARY KEY using index "webhooks_pkey";
|
|
53
|
+
alter table "public"."webhooks" add constraint "webhooks_sales_id_fkey"
|
|
54
|
+
FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
55
|
+
alter table "public"."webhooks" validate constraint "webhooks_sales_id_fkey";
|
|
56
|
+
alter table "public"."webhooks" add constraint "webhooks_created_by_sales_id_fkey"
|
|
57
|
+
FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
58
|
+
alter table "public"."webhooks" validate constraint "webhooks_created_by_sales_id_fkey";
|
|
59
|
+
|
|
60
|
+
-- API Request Logs table
|
|
61
|
+
create table "public"."api_logs" (
|
|
62
|
+
"id" bigint generated by default as identity not null,
|
|
63
|
+
"created_at" timestamp with time zone not null default now(),
|
|
64
|
+
"api_key_id" bigint not null,
|
|
65
|
+
"endpoint" text not null,
|
|
66
|
+
"method" text not null,
|
|
67
|
+
"status_code" int not null,
|
|
68
|
+
"response_time_ms" int,
|
|
69
|
+
"ip_address" text,
|
|
70
|
+
"user_agent" text,
|
|
71
|
+
"error_message" text
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
alter table "public"."api_logs" enable row level security;
|
|
75
|
+
|
|
76
|
+
CREATE UNIQUE INDEX api_logs_pkey ON public.api_logs USING btree (id);
|
|
77
|
+
CREATE INDEX api_logs_api_key_id_idx ON public.api_logs USING btree (api_key_id);
|
|
78
|
+
CREATE INDEX api_logs_created_at_idx ON public.api_logs USING btree (created_at DESC);
|
|
79
|
+
|
|
80
|
+
alter table "public"."api_logs" add constraint "api_logs_pkey" PRIMARY KEY using index "api_logs_pkey";
|
|
81
|
+
alter table "public"."api_logs" add constraint "api_logs_api_key_id_fkey"
|
|
82
|
+
FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
83
|
+
alter table "public"."api_logs" validate constraint "api_logs_api_key_id_fkey";
|
|
84
|
+
|
|
85
|
+
-- Webhook Queue table (for async delivery)
|
|
86
|
+
create table "public"."webhook_queue" (
|
|
87
|
+
"id" bigint generated by default as identity not null,
|
|
88
|
+
"created_at" timestamp with time zone not null default now(),
|
|
89
|
+
"webhook_id" bigint not null,
|
|
90
|
+
"event_type" text not null,
|
|
91
|
+
"payload" jsonb not null,
|
|
92
|
+
"status" text not null default 'pending',
|
|
93
|
+
"attempts" int not null default 0,
|
|
94
|
+
"max_attempts" int not null default 3,
|
|
95
|
+
"next_retry_at" timestamp with time zone,
|
|
96
|
+
"delivered_at" timestamp with time zone,
|
|
97
|
+
"error_message" text
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
alter table "public"."webhook_queue" enable row level security;
|
|
101
|
+
|
|
102
|
+
CREATE UNIQUE INDEX webhook_queue_pkey ON public.webhook_queue USING btree (id);
|
|
103
|
+
CREATE INDEX webhook_queue_status_idx ON public.webhook_queue USING btree (status, next_retry_at);
|
|
104
|
+
CREATE INDEX webhook_queue_webhook_id_idx ON public.webhook_queue USING btree (webhook_id);
|
|
105
|
+
|
|
106
|
+
alter table "public"."webhook_queue" add constraint "webhook_queue_pkey" PRIMARY KEY using index "webhook_queue_pkey";
|
|
107
|
+
alter table "public"."webhook_queue" add constraint "webhook_queue_webhook_id_fkey"
|
|
108
|
+
FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
|
109
|
+
alter table "public"."webhook_queue" validate constraint "webhook_queue_webhook_id_fkey";
|
|
110
|
+
|
|
111
|
+
-- Grant permissions
|
|
112
|
+
grant select, insert, update, delete on table "public"."api_keys" to "authenticated";
|
|
113
|
+
grant select, insert, update, delete on table "public"."webhooks" to "authenticated";
|
|
114
|
+
grant select on table "public"."api_logs" to "authenticated";
|
|
115
|
+
grant select on table "public"."webhook_queue" to "authenticated";
|
|
116
|
+
|
|
117
|
+
grant all on table "public"."api_keys" to "service_role";
|
|
118
|
+
grant all on table "public"."webhooks" to "service_role";
|
|
119
|
+
grant all on table "public"."api_logs" to "service_role";
|
|
120
|
+
grant all on table "public"."webhook_queue" to "service_role";
|
|
121
|
+
|
|
122
|
+
-- RLS Policies (permissive for authenticated users)
|
|
123
|
+
create policy "Enable all for authenticated users" on "public"."api_keys"
|
|
124
|
+
for all to authenticated using (true) with check (true);
|
|
125
|
+
|
|
126
|
+
create policy "Enable all for authenticated users" on "public"."webhooks"
|
|
127
|
+
for all to authenticated using (true) with check (true);
|
|
128
|
+
|
|
129
|
+
create policy "Enable read for authenticated users" on "public"."api_logs"
|
|
130
|
+
for select to authenticated using (true);
|
|
131
|
+
|
|
132
|
+
create policy "Enable read for authenticated users" on "public"."webhook_queue"
|
|
133
|
+
for select to authenticated using (true);
|