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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.
|
|
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";
|