realtimex-crm 0.8.1 → 0.9.2

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.
Files changed (55) hide show
  1. package/bin/realtimex-crm.js +56 -32
  2. package/dist/assets/{DealList-B3alafQv.js → DealList-DbwJCRGl.js} +2 -2
  3. package/dist/assets/{DealList-B3alafQv.js.map → DealList-DbwJCRGl.js.map} +1 -1
  4. package/dist/assets/index-C__S90Gb.css +1 -0
  5. package/dist/assets/index-mE-upBfc.js +166 -0
  6. package/dist/assets/index-mE-upBfc.js.map +1 -0
  7. package/dist/index.html +1 -1
  8. package/dist/stats.html +1 -1
  9. package/package.json +3 -1
  10. package/src/components/atomic-crm/activities/ActivitiesPage.tsx +16 -0
  11. package/src/components/atomic-crm/activities/ActivityFeed.tsx +212 -0
  12. package/src/components/atomic-crm/activities/FileUpload.tsx +359 -0
  13. package/src/components/atomic-crm/contacts/ContactShow.tsx +28 -10
  14. package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
  15. package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
  16. package/src/components/atomic-crm/integrations/CreateChannelDialog.tsx +139 -0
  17. package/src/components/atomic-crm/integrations/IngestionChannelsTab.tsx +188 -0
  18. package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +46 -0
  19. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
  20. package/src/components/atomic-crm/layout/Header.tsx +14 -1
  21. package/src/components/atomic-crm/root/CRM.tsx +2 -0
  22. package/src/components/ui/alert-dialog.tsx +155 -0
  23. package/src/lib/api-key-utils.ts +22 -0
  24. package/supabase/fix_webhook_hardcoded.sql +34 -0
  25. package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
  26. package/supabase/functions/_shared/ingestionGuard.ts +128 -0
  27. package/supabase/functions/_shared/utils.ts +1 -1
  28. package/supabase/functions/_shared/webhookSignature.ts +23 -0
  29. package/supabase/functions/api-v1-activities/index.ts +137 -0
  30. package/supabase/functions/api-v1-companies/index.ts +166 -0
  31. package/supabase/functions/api-v1-contacts/index.ts +171 -0
  32. package/supabase/functions/api-v1-deals/index.ts +166 -0
  33. package/supabase/functions/ingest-activity/.well-known/supabase/config.toml +4 -0
  34. package/supabase/functions/ingest-activity/index.ts +261 -0
  35. package/supabase/functions/webhook-dispatcher/index.ts +133 -0
  36. package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
  37. package/supabase/migrations/20251219120100_webhook_triggers.sql +176 -0
  38. package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
  39. package/supabase/migrations/20251220120000_realtime_ingestion.sql +154 -0
  40. package/supabase/migrations/20251221000000_contact_matching.sql +94 -0
  41. package/supabase/migrations/20251221000001_fix_ingestion_providers_rls.sql +23 -0
  42. package/supabase/migrations/20251221000002_fix_contact_matching_jsonb.sql +67 -0
  43. package/supabase/migrations/20251221000003_fix_email_matching.sql +70 -0
  44. package/supabase/migrations/20251221000004_time_based_work_stealing.sql +99 -0
  45. package/supabase/migrations/20251221000005_realtime_functions.sql +73 -0
  46. package/supabase/migrations/20251221000006_enable_pg_net.sql +3 -0
  47. package/supabase/migrations/20251222075019_enable_extensions_and_configure_cron.sql +86 -0
  48. package/supabase/migrations/20251222094036_large_payload_storage.sql +213 -0
  49. package/supabase/migrations/20251222094247_large_payload_cron.sql +28 -0
  50. package/supabase/migrations/20251222220000_fix_large_payload_types.sql +72 -0
  51. package/supabase/migrations/20251223000000_enable_realtime_all_crm_tables.sql +50 -0
  52. package/supabase/migrations/20251223185638_remove_large_payload_storage.sql +54 -0
  53. package/dist/assets/index-CHk72bAf.css +0 -1
  54. package/dist/assets/index-mhAjQ1_w.js +0 -153
  55. package/dist/assets/index-mhAjQ1_w.js.map +0 -1
@@ -0,0 +1,184 @@
1
+ import { useState } from "react";
2
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { useDataProvider, useNotify, useGetIdentity } from "ra-core";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Plus, Trash2, Copy } from "lucide-react";
7
+ import { CreateApiKeyDialog } from "./CreateApiKeyDialog";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { format } from "date-fns";
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogFooter,
17
+ AlertDialogHeader,
18
+ AlertDialogTitle,
19
+ } from "@/components/ui/alert-dialog";
20
+
21
+ export const ApiKeysTab = () => {
22
+ const [showCreateDialog, setShowCreateDialog] = useState(false);
23
+ const [keyToDelete, setKeyToDelete] = useState<number | null>(null);
24
+ const dataProvider = useDataProvider();
25
+ const notify = useNotify();
26
+ const queryClient = useQueryClient();
27
+ const { identity: _identity } = useGetIdentity();
28
+
29
+ const { data: apiKeys, isLoading } = useQuery({
30
+ queryKey: ["api_keys"],
31
+ queryFn: async () => {
32
+ const { data } = await dataProvider.getList("api_keys", {
33
+ pagination: { page: 1, perPage: 100 },
34
+ sort: { field: "created_at", order: "DESC" },
35
+ filter: {},
36
+ });
37
+ return data;
38
+ },
39
+ });
40
+
41
+ const deleteMutation = useMutation({
42
+ mutationFn: async (id: number) => {
43
+ await dataProvider.delete("api_keys", { id, previousData: {} });
44
+ },
45
+ onSuccess: () => {
46
+ queryClient.invalidateQueries({ queryKey: ["api_keys"] });
47
+ notify("API key deleted successfully");
48
+ setKeyToDelete(null);
49
+ },
50
+ onError: () => {
51
+ notify("Failed to delete API key", { type: "error" });
52
+ },
53
+ });
54
+
55
+ return (
56
+ <div className="space-y-4">
57
+ <div className="flex justify-between items-center">
58
+ <p className="text-sm text-muted-foreground">
59
+ API keys allow external applications to access your CRM data
60
+ programmatically.
61
+ </p>
62
+ <Button onClick={() => setShowCreateDialog(true)}>
63
+ <Plus className="h-4 w-4 mr-2" />
64
+ Create API Key
65
+ </Button>
66
+ </div>
67
+
68
+ {isLoading ? (
69
+ <Card>
70
+ <CardContent className="py-8 text-center text-muted-foreground">
71
+ Loading...
72
+ </CardContent>
73
+ </Card>
74
+ ) : apiKeys && apiKeys.length > 0 ? (
75
+ <div className="space-y-3">
76
+ {apiKeys.map((key: any) => (
77
+ <ApiKeyCard
78
+ key={key.id}
79
+ apiKey={key}
80
+ onDelete={() => setKeyToDelete(key.id)}
81
+ />
82
+ ))}
83
+ </div>
84
+ ) : (
85
+ <Card>
86
+ <CardContent className="py-12 text-center">
87
+ <p className="text-muted-foreground mb-4">No API keys yet</p>
88
+ <Button onClick={() => setShowCreateDialog(true)}>
89
+ <Plus className="h-4 w-4 mr-2" />
90
+ Create your first API key
91
+ </Button>
92
+ </CardContent>
93
+ </Card>
94
+ )}
95
+
96
+ <CreateApiKeyDialog
97
+ open={showCreateDialog}
98
+ onClose={() => setShowCreateDialog(false)}
99
+ />
100
+
101
+ <AlertDialog
102
+ open={keyToDelete !== null}
103
+ onOpenChange={() => setKeyToDelete(null)}
104
+ >
105
+ <AlertDialogContent>
106
+ <AlertDialogHeader>
107
+ <AlertDialogTitle>Delete API Key?</AlertDialogTitle>
108
+ <AlertDialogDescription>
109
+ This will permanently delete this API key. Any applications using
110
+ this key will stop working immediately. This action cannot be
111
+ undone.
112
+ </AlertDialogDescription>
113
+ </AlertDialogHeader>
114
+ <AlertDialogFooter>
115
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
116
+ <AlertDialogAction
117
+ onClick={() => keyToDelete && deleteMutation.mutate(keyToDelete)}
118
+ className="bg-destructive hover:bg-destructive/90"
119
+ >
120
+ Delete
121
+ </AlertDialogAction>
122
+ </AlertDialogFooter>
123
+ </AlertDialogContent>
124
+ </AlertDialog>
125
+ </div>
126
+ );
127
+ };
128
+
129
+ const ApiKeyCard = ({
130
+ apiKey,
131
+ onDelete,
132
+ }: {
133
+ apiKey: any;
134
+ onDelete: () => void;
135
+ }) => {
136
+ const notify = useNotify();
137
+
138
+ const copyKey = () => {
139
+ navigator.clipboard.writeText(apiKey.key_prefix + "••••••••");
140
+ notify("Key prefix copied (full key only shown once at creation)");
141
+ };
142
+
143
+ return (
144
+ <Card>
145
+ <CardHeader className="pb-3">
146
+ <div className="flex justify-between items-start">
147
+ <div>
148
+ <CardTitle className="text-lg">{apiKey.name}</CardTitle>
149
+ <div className="flex gap-2 mt-2">
150
+ {apiKey.is_active ? (
151
+ <Badge variant="default">Active</Badge>
152
+ ) : (
153
+ <Badge variant="secondary">Inactive</Badge>
154
+ )}
155
+ {apiKey.scopes && apiKey.scopes.length > 0 && (
156
+ <Badge variant="outline">{apiKey.scopes.join(", ")}</Badge>
157
+ )}
158
+ </div>
159
+ </div>
160
+ <Button variant="ghost" size="icon" onClick={onDelete}>
161
+ <Trash2 className="h-4 w-4 text-destructive" />
162
+ </Button>
163
+ </div>
164
+ </CardHeader>
165
+ <CardContent className="space-y-2">
166
+ <div className="flex items-center gap-2 font-mono text-sm bg-muted p-2 rounded">
167
+ <span>{apiKey.key_prefix}••••••••••••••••••••</span>
168
+ <Button variant="ghost" size="icon" onClick={copyKey}>
169
+ <Copy className="h-4 w-4" />
170
+ </Button>
171
+ </div>
172
+ <div className="text-xs text-muted-foreground space-y-1">
173
+ <p>Created: {format(new Date(apiKey.created_at), "PPP")}</p>
174
+ {apiKey.last_used_at && (
175
+ <p>Last used: {format(new Date(apiKey.last_used_at), "PPp")}</p>
176
+ )}
177
+ {apiKey.expires_at && (
178
+ <p>Expires: {format(new Date(apiKey.expires_at), "PPP")}</p>
179
+ )}
180
+ </div>
181
+ </CardContent>
182
+ </Card>
183
+ );
184
+ };
@@ -0,0 +1,217 @@
1
+ import { useState } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useDataProvider, useNotify, useGetIdentity } from "ra-core";
5
+ import { generateApiKey, hashApiKey } from "@/lib/api-key-utils";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
16
+ import { Checkbox } from "@/components/ui/checkbox";
17
+ import { Alert, AlertDescription } from "@/components/ui/alert";
18
+ import { Copy, CheckCircle } from "lucide-react";
19
+
20
+ interface CreateApiKeyDialogProps {
21
+ open: boolean;
22
+ onClose: () => void;
23
+ }
24
+
25
+ const AVAILABLE_SCOPES = [
26
+ { value: "contacts:read", label: "Contacts: Read" },
27
+ { value: "contacts:write", label: "Contacts: Write" },
28
+ { value: "companies:read", label: "Companies: Read" },
29
+ { value: "companies:write", label: "Companies: Write" },
30
+ { value: "deals:read", label: "Deals: Read" },
31
+ { value: "deals:write", label: "Deals: Write" },
32
+ { value: "activities:write", label: "Activities: Write" },
33
+ ];
34
+
35
+ export const CreateApiKeyDialog = ({
36
+ open,
37
+ onClose,
38
+ }: CreateApiKeyDialogProps) => {
39
+ const [createdKey, setCreatedKey] = useState<string | null>(null);
40
+ const [copied, setCopied] = useState(false);
41
+ const dataProvider = useDataProvider();
42
+ const notify = useNotify();
43
+ const queryClient = useQueryClient();
44
+ const { identity } = useGetIdentity();
45
+
46
+ const { register, handleSubmit, watch, setValue, reset } = useForm({
47
+ defaultValues: {
48
+ name: "",
49
+ scopes: [] as string[],
50
+ expires_at: "",
51
+ },
52
+ });
53
+
54
+ const createMutation = useMutation({
55
+ mutationFn: async (values: any) => {
56
+ const apiKey = generateApiKey();
57
+ const keyHash = await hashApiKey(apiKey);
58
+ const keyPrefix = apiKey.substring(0, 12);
59
+
60
+ const { data } = await dataProvider.create("api_keys", {
61
+ data: {
62
+ name: values.name,
63
+ key_hash: keyHash,
64
+ key_prefix: keyPrefix,
65
+ scopes: values.scopes,
66
+ is_active: true,
67
+ expires_at: values.expires_at || null,
68
+ sales_id: identity?.id,
69
+ created_by_sales_id: identity?.id,
70
+ },
71
+ });
72
+
73
+ return { data, apiKey };
74
+ },
75
+ onSuccess: ({ apiKey }) => {
76
+ setCreatedKey(apiKey);
77
+ queryClient.invalidateQueries({ queryKey: ["api_keys"] });
78
+ notify("API key created successfully");
79
+ },
80
+ onError: () => {
81
+ notify("Failed to create API key", { type: "error" });
82
+ },
83
+ });
84
+
85
+ const handleClose = () => {
86
+ setCreatedKey(null);
87
+ setCopied(false);
88
+ reset();
89
+ onClose();
90
+ };
91
+
92
+ const copyApiKey = () => {
93
+ if (createdKey) {
94
+ navigator.clipboard.writeText(createdKey);
95
+ setCopied(true);
96
+ setTimeout(() => setCopied(false), 2000);
97
+ }
98
+ };
99
+
100
+ const toggleScope = (scope: string) => {
101
+ const currentScopes = watch("scopes");
102
+ if (currentScopes.includes(scope)) {
103
+ setValue(
104
+ "scopes",
105
+ currentScopes.filter((s) => s !== scope)
106
+ );
107
+ } else {
108
+ setValue("scopes", [...currentScopes, scope]);
109
+ }
110
+ };
111
+
112
+ return (
113
+ <Dialog open={open} onOpenChange={handleClose}>
114
+ <DialogContent className="sm:max-w-[500px]">
115
+ <DialogHeader>
116
+ <DialogTitle>
117
+ {createdKey ? "API Key Created" : "Create API Key"}
118
+ </DialogTitle>
119
+ <DialogDescription>
120
+ {createdKey
121
+ ? "Copy this key now - it won't be shown again!"
122
+ : "Create a new API key to access the CRM API"}
123
+ </DialogDescription>
124
+ </DialogHeader>
125
+
126
+ {createdKey ? (
127
+ <div className="space-y-4">
128
+ <Alert>
129
+ <AlertDescription>
130
+ Make sure to copy your API key now. You won't be able to see it
131
+ again!
132
+ </AlertDescription>
133
+ </Alert>
134
+
135
+ <div className="space-y-2">
136
+ <Label>Your API Key</Label>
137
+ <div className="flex gap-2">
138
+ <Input
139
+ value={createdKey}
140
+ readOnly
141
+ className="font-mono text-sm"
142
+ />
143
+ <Button
144
+ type="button"
145
+ variant="outline"
146
+ size="icon"
147
+ onClick={copyApiKey}
148
+ >
149
+ {copied ? (
150
+ <CheckCircle className="h-4 w-4 text-green-500" />
151
+ ) : (
152
+ <Copy className="h-4 w-4" />
153
+ )}
154
+ </Button>
155
+ </div>
156
+ </div>
157
+
158
+ <div className="flex justify-end">
159
+ <Button onClick={handleClose}>Done</Button>
160
+ </div>
161
+ </div>
162
+ ) : (
163
+ <form
164
+ onSubmit={handleSubmit((values) => createMutation.mutate(values))}
165
+ >
166
+ <div className="space-y-4">
167
+ <div className="space-y-2">
168
+ <Label htmlFor="name">Name</Label>
169
+ <Input
170
+ id="name"
171
+ placeholder="e.g., Production API Key"
172
+ {...register("name", { required: true })}
173
+ />
174
+ </div>
175
+
176
+ <div className="space-y-2">
177
+ <Label>Scopes</Label>
178
+ <div className="space-y-2">
179
+ {AVAILABLE_SCOPES.map((scope) => (
180
+ <div key={scope.value} className="flex items-center space-x-2">
181
+ <Checkbox
182
+ id={scope.value}
183
+ checked={watch("scopes").includes(scope.value)}
184
+ onCheckedChange={() => toggleScope(scope.value)}
185
+ />
186
+ <label htmlFor={scope.value} className="text-sm cursor-pointer">
187
+ {scope.label}
188
+ </label>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </div>
193
+
194
+ <div className="space-y-2">
195
+ <Label htmlFor="expires_at">Expiration (optional)</Label>
196
+ <Input
197
+ id="expires_at"
198
+ type="date"
199
+ {...register("expires_at")}
200
+ />
201
+ </div>
202
+
203
+ <div className="flex justify-end gap-2">
204
+ <Button type="button" variant="outline" onClick={handleClose}>
205
+ Cancel
206
+ </Button>
207
+ <Button type="submit" disabled={createMutation.isPending}>
208
+ Create
209
+ </Button>
210
+ </div>
211
+ </div>
212
+ </form>
213
+ )}
214
+ </DialogContent>
215
+ </Dialog>
216
+ );
217
+ };
@@ -0,0 +1,139 @@
1
+ import { useState } from "react";
2
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { useDataProvider, useNotify } from "ra-core";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogFooter,
12
+ } from "@/components/ui/dialog";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Label } from "@/components/ui/label";
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from "@/components/ui/select";
22
+ import { Loader2 } from "lucide-react";
23
+
24
+ interface CreateChannelDialogProps {
25
+ open: boolean;
26
+ onClose: () => void;
27
+ }
28
+
29
+ export const CreateChannelDialog = ({
30
+ open,
31
+ onClose,
32
+ }: CreateChannelDialogProps) => {
33
+ const [providerCode, setProviderCode] = useState<string>("twilio");
34
+ const [name, setName] = useState("");
35
+ const [authToken, setAuthToken] = useState("");
36
+ const dataProvider = useDataProvider();
37
+ const notify = useNotify();
38
+ const queryClient = useQueryClient();
39
+
40
+ const createMutation = useMutation({
41
+ mutationFn: async () => {
42
+ // Generate a cryptographically secure random ingestion key
43
+ const ingestionKey = "ik_live_" + crypto.randomUUID().replace(/-/g, '');
44
+
45
+ const config: any = {};
46
+ if (authToken) {
47
+ config.auth_token = authToken;
48
+ }
49
+
50
+ await dataProvider.create("ingestion_providers", {
51
+ data: {
52
+ name,
53
+ provider_code: providerCode,
54
+ is_active: true,
55
+ config,
56
+ ingestion_key: ingestionKey
57
+ },
58
+ });
59
+ },
60
+ onSuccess: () => {
61
+ queryClient.invalidateQueries({ queryKey: ["ingestion_providers"] });
62
+ notify("Ingestion Channel created successfully");
63
+ onClose();
64
+ // Reset form
65
+ setName("");
66
+ setAuthToken("");
67
+ setProviderCode("twilio");
68
+ },
69
+ onError: (error: Error) => {
70
+ notify(`Failed to create channel: ${error.message}`, { type: "error" });
71
+ },
72
+ });
73
+
74
+ return (
75
+ <Dialog open={open} onOpenChange={onClose}>
76
+ <DialogContent className="sm:max-w-[425px]">
77
+ <DialogHeader>
78
+ <DialogTitle>Add Ingestion Channel</DialogTitle>
79
+ <DialogDescription>
80
+ Configure a new source for incoming activities.
81
+ </DialogDescription>
82
+ </DialogHeader>
83
+ <div className="grid gap-4 py-4">
84
+ <div className="grid gap-2">
85
+ <Label htmlFor="name">Channel Name</Label>
86
+ <Input
87
+ id="name"
88
+ placeholder="e.g. US Support Line"
89
+ value={name}
90
+ onChange={(e) => setName(e.target.value)}
91
+ />
92
+ </div>
93
+ <div className="grid gap-2">
94
+ <Label htmlFor="provider">Provider</Label>
95
+ <Select value={providerCode} onValueChange={setProviderCode}>
96
+ <SelectTrigger>
97
+ <SelectValue placeholder="Select provider" />
98
+ </SelectTrigger>
99
+ <SelectContent>
100
+ <SelectItem value="twilio">Twilio (Voice/SMS)</SelectItem>
101
+ <SelectItem value="postmark">Postmark (Email)</SelectItem>
102
+ <SelectItem value="generic">Generic / Internal</SelectItem>
103
+ </SelectContent>
104
+ </Select>
105
+ </div>
106
+
107
+ {providerCode === "twilio" && (
108
+ <div className="grid gap-2">
109
+ <Label htmlFor="token">Auth Token (Validation)</Label>
110
+ <Input
111
+ id="token"
112
+ type="password"
113
+ placeholder="Twilio Auth Token"
114
+ value={authToken}
115
+ onChange={(e) => setAuthToken(e.target.value)}
116
+ />
117
+ <p className="text-xs text-muted-foreground">Required to validate inbound requests.</p>
118
+ </div>
119
+ )}
120
+
121
+ </div>
122
+ <DialogFooter>
123
+ <Button variant="outline" onClick={onClose}>
124
+ Cancel
125
+ </Button>
126
+ <Button
127
+ onClick={() => createMutation.mutate()}
128
+ disabled={!name || createMutation.isPending}
129
+ >
130
+ {createMutation.isPending && (
131
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
132
+ )}
133
+ Create Channel
134
+ </Button>
135
+ </DialogFooter>
136
+ </DialogContent>
137
+ </Dialog>
138
+ );
139
+ };