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.
Files changed (32) hide show
  1. package/dist/assets/{DealList-CZvo7G6M.js → DealList-DnGVfS15.js} +2 -2
  2. package/dist/assets/{DealList-CZvo7G6M.js.map → DealList-DnGVfS15.js.map} +1 -1
  3. package/dist/assets/index-DPrpo5Xq.js +159 -0
  4. package/dist/assets/index-DPrpo5Xq.js.map +1 -0
  5. package/dist/assets/index-kM1Og1AS.css +1 -0
  6. package/dist/index.html +1 -1
  7. package/dist/stats.html +1 -1
  8. package/package.json +2 -1
  9. package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
  10. package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
  11. package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +34 -0
  12. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
  13. package/src/components/atomic-crm/layout/Header.tsx +14 -1
  14. package/src/components/atomic-crm/root/CRM.tsx +2 -0
  15. package/src/components/supabase/change-password-page.tsx +9 -1
  16. package/src/components/supabase/forgot-password-page.tsx +30 -0
  17. package/src/components/supabase/layout.tsx +6 -2
  18. package/src/components/ui/alert-dialog.tsx +155 -0
  19. package/src/lib/api-key-utils.ts +22 -0
  20. package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
  21. package/supabase/functions/_shared/webhookSignature.ts +23 -0
  22. package/supabase/functions/api-v1-activities/index.ts +137 -0
  23. package/supabase/functions/api-v1-companies/index.ts +166 -0
  24. package/supabase/functions/api-v1-contacts/index.ts +171 -0
  25. package/supabase/functions/api-v1-deals/index.ts +166 -0
  26. package/supabase/functions/webhook-dispatcher/index.ts +133 -0
  27. package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
  28. package/supabase/migrations/20251219120100_webhook_triggers.sql +171 -0
  29. package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
  30. package/dist/assets/index-CdoQZFIX.css +0 -1
  31. package/dist/assets/index-D0sWLaB1.js +0 -153
  32. package/dist/assets/index-D0sWLaB1.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtimex-crm",
3
- "version": "0.7.14",
3
+ "version": "0.9.1",
4
4
  "description": "RealTimeX CRM - A full-featured CRM built with React, shadcn-admin-kit, and Supabase. Fork of Atomic CRM with RealTimeX App SDK integration.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -75,6 +75,7 @@
75
75
  "@inquirer/prompts": "^8.1.0",
76
76
  "@nivo/bar": "^0.99.0",
77
77
  "@radix-ui/react-accordion": "^1.2.12",
78
+ "@radix-ui/react-alert-dialog": "^1.1.15",
78
79
  "@radix-ui/react-avatar": "^1.1.11",
79
80
  "@radix-ui/react-checkbox": "^1.3.3",
80
81
  "@radix-ui/react-dialog": "^1.1.15",
@@ -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,34 @@
1
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2
+ import { ApiKeysTab } from "./ApiKeysTab";
3
+ import { WebhooksTab } from "./WebhooksTab";
4
+
5
+ export const IntegrationsPage = () => {
6
+ return (
7
+ <div className="max-w-6xl mx-auto mt-8 px-4">
8
+ <div className="mb-6">
9
+ <h1 className="text-3xl font-bold">Integrations</h1>
10
+ <p className="text-muted-foreground mt-2">
11
+ Manage API keys and webhooks to integrate Atomic CRM with external
12
+ systems.
13
+ </p>
14
+ </div>
15
+
16
+ <Tabs defaultValue="api-keys">
17
+ <TabsList className="mb-4">
18
+ <TabsTrigger value="api-keys">API Keys</TabsTrigger>
19
+ <TabsTrigger value="webhooks">Webhooks</TabsTrigger>
20
+ </TabsList>
21
+
22
+ <TabsContent value="api-keys">
23
+ <ApiKeysTab />
24
+ </TabsContent>
25
+
26
+ <TabsContent value="webhooks">
27
+ <WebhooksTab />
28
+ </TabsContent>
29
+ </Tabs>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ IntegrationsPage.path = "/integrations";