Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/components/api-keys.tsx
5783 views
1
/*
2
React component for managing a list of api keys.
3
4
Applications:
5
6
- the keys for a project
7
- the keys for an account
8
*/
9
10
import {
11
Alert,
12
Button,
13
DatePicker,
14
Form,
15
Input,
16
Modal,
17
Popconfirm,
18
Space,
19
Table,
20
Typography,
21
} from "antd";
22
import { ColumnsType } from "antd/es/table";
23
import dayjs from "dayjs";
24
import { useEffect, useState } from "react";
25
import TimeAgo from "react-timeago"; // so can use from nextjs
26
const { Text, Paragraph } = Typography; // so can use from nextjs
27
import { CancelText } from "@cocalc/frontend/i18n/components";
28
import type { ApiKey } from "@cocalc/util/db-schema/api-keys";
29
import { A } from "./A";
30
import CopyToClipBoard from "./copy-to-clipboard";
31
import { Icon } from "./icon";
32
33
const { useForm } = Form;
34
35
interface Props {
36
// Manage is a function that lets you get all api keys, delete a single api key,
37
// or create an api key.
38
// - If you call manage with input "get" it will return a Javascript array ApiKey[]
39
// of all your api keys, with each api key represented as an object {name, id, trunc, last_active?}
40
// as defined above. The actual key itself is not returned, and trunc is a truncated
41
// version of the key used for display.
42
// - If you call manage with input "delete" and id set then that key will get deleted.
43
// - If you call manage with input "create", then a new api key is created and returned
44
// as a single string. This is the one and only time the user can see this *secret*.
45
// - If call with edit and both name and id set, changes the key determined by id
46
// to have the given name. Similar for expire.
47
manage: (opts: {
48
action: "get" | "delete" | "create" | "edit";
49
id?: number;
50
name?: string;
51
expire?: Date;
52
}) => Promise<ApiKey[] | undefined>;
53
mode?: "project" | "flyout";
54
}
55
56
export default function ApiKeys({ manage, mode = "project" }: Props) {
57
const isFlyout = mode === "flyout";
58
const size = isFlyout ? "small" : undefined; // for e.g. buttons
59
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
60
const [loading, setLoading] = useState<boolean>(true);
61
const [editingKey, setEditingKey] = useState<number | undefined>(undefined);
62
const [addModalVisible, setAddModalVisible] = useState<boolean>(false);
63
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
64
const [form] = useForm();
65
const [error, setError] = useState<string | null>(null);
66
67
useEffect(() => {
68
getAllApiKeys();
69
}, []);
70
71
const getAllApiKeys = async () => {
72
setLoading(true);
73
try {
74
const response = await manage({ action: "get" });
75
setApiKeys(response as ApiKey[]);
76
setLoading(false);
77
setError(null);
78
} catch (err) {
79
setLoading(false);
80
setError(`${err}`);
81
}
82
};
83
84
const deleteApiKey = async (id: number) => {
85
try {
86
await manage({ action: "delete", id });
87
getAllApiKeys();
88
} catch (err) {
89
setError(`${err}`);
90
}
91
};
92
93
const deleteAllApiKeys = async () => {
94
for (const { id } of apiKeys) {
95
await deleteApiKey(id);
96
}
97
};
98
99
const editApiKey = async (id: number, name: string, expire?: Date) => {
100
try {
101
await manage({ action: "edit", id, name, expire });
102
getAllApiKeys();
103
} catch (err) {
104
setError(`${err}`);
105
}
106
};
107
108
const createApiKey = async (name: string, expire?: Date) => {
109
try {
110
const response = await manage({
111
action: "create",
112
name,
113
expire,
114
});
115
setAddModalVisible(false);
116
getAllApiKeys();
117
118
Modal.success({
119
width: 600,
120
title: "New Secret API Key",
121
content: (
122
<>
123
<div>
124
Save this secret key somewhere safe.{" "}
125
<b>You won't be able to view it again here.</b> If you lose this
126
secret key, you'll need to generate a new one.
127
</div>
128
<div style={{ marginTop: 16 }}>
129
<strong>Secret API Key</strong>{" "}
130
<CopyToClipBoard
131
style={{ marginTop: "16px" }}
132
value={response?.[0].secret ?? "failed to get secret"}
133
/>
134
</div>
135
</>
136
),
137
});
138
setError(null);
139
} catch (err) {
140
setError(`${err}`);
141
}
142
};
143
144
const columns: ColumnsType<ApiKey> = [
145
{
146
dataIndex: "name",
147
title: "Name/Key",
148
render: (name, record) => {
149
return (
150
<>
151
{name}
152
<br />
153
<Text type="secondary">({record.trunc})</Text>
154
</>
155
);
156
},
157
},
158
{
159
dataIndex: "last_active",
160
title: "Last Used",
161
render: (last_active) =>
162
last_active ? <TimeAgo date={last_active} /> : "Never",
163
},
164
{
165
dataIndex: "expire",
166
title: "Expire",
167
render: (expire) => (expire ? <TimeAgo date={expire} /> : "Never"),
168
},
169
{
170
dataIndex: "operation",
171
title: "Operation",
172
align: "right",
173
render: (_text, record) => (
174
<Space.Compact direction={isFlyout ? "vertical" : "horizontal"}>
175
<Popconfirm
176
title="Are you sure you want to delete this key?"
177
onConfirm={() => deleteApiKey(record.id)}
178
>
179
<a>Delete</a>
180
</Popconfirm>
181
<a
182
onClick={() => {
183
// Set the initial form value as the current key name
184
form.setFieldsValue({ name: record.name });
185
setEditModalVisible(true);
186
setEditingKey(record.id);
187
}}
188
style={{ marginLeft: "1em" }}
189
>
190
Edit
191
</a>
192
</Space.Compact>
193
),
194
},
195
];
196
197
if (!isFlyout) {
198
columns.splice(1, 0, { dataIndex: "id", title: "Id" });
199
}
200
201
const handleAdd = () => {
202
setAddModalVisible(true);
203
};
204
205
const handleModalOK = () => {
206
const name = form.getFieldValue("name");
207
const expire = form.getFieldValue("expire")?.toDate();
208
if (editingKey != null) {
209
editApiKey(editingKey, name, expire);
210
setEditModalVisible(false);
211
setEditingKey(undefined);
212
form.resetFields();
213
} else {
214
createApiKey(name, expire);
215
form.resetFields();
216
}
217
};
218
219
const handleModalCancel = () => {
220
setAddModalVisible(false);
221
setEditModalVisible(false);
222
setEditingKey(undefined);
223
form.resetFields();
224
};
225
226
return (
227
<>
228
{error && (
229
<Alert
230
message={error}
231
type="error"
232
closable
233
onClose={() => setError(null)}
234
style={{ marginBottom: 16 }}
235
/>
236
)}
237
{apiKeys.length > 0 && (
238
<Table
239
style={{ marginBottom: 16 }}
240
dataSource={apiKeys}
241
columns={columns}
242
loading={loading}
243
rowKey="id"
244
pagination={false}
245
/>
246
)}
247
<div style={isFlyout ? { padding: "5px" } : undefined}>
248
<Space.Compact size={size}>
249
<Button onClick={handleAdd} size={size}>
250
<Icon name="plus-circle" /> Add API key...
251
</Button>
252
<Button onClick={getAllApiKeys} size={size}>
253
Refresh
254
</Button>
255
{apiKeys.length > 0 && (
256
<Popconfirm
257
title="Are you sure you want to delete all these api keys?"
258
onConfirm={deleteAllApiKeys}
259
>
260
<Button danger size={size}>
261
Delete All...
262
</Button>
263
</Popconfirm>
264
)}
265
</Space.Compact>
266
<Paragraph style={{ marginTop: "10px" }}>
267
Read the <A href="https://doc.cocalc.com/api2/">API documentation</A>.
268
</Paragraph>
269
<Modal
270
open={addModalVisible || editModalVisible}
271
title={
272
editingKey != null ? "Edit API Key Name" : "Create a New API Key"
273
}
274
okText={editingKey != null ? "Save" : "Create"}
275
cancelText={<CancelText />}
276
onCancel={handleModalCancel}
277
onOk={handleModalOK}
278
>
279
<Form form={form} layout="vertical">
280
<Form.Item
281
name="name"
282
label="Name"
283
rules={[{ required: true, message: "Please enter a name" }]}
284
>
285
<Input />
286
</Form.Item>
287
<Form.Item
288
name="expire"
289
label="Expire"
290
rules={[
291
{
292
required: false,
293
message:
294
"Optional date when key will be automatically deleted",
295
},
296
]}
297
>
298
<DatePicker
299
changeOnBlur
300
showTime
301
disabledDate={(current) => {
302
// disable all dates before today
303
return current && current < dayjs();
304
}}
305
/>
306
</Form.Item>
307
</Form>
308
</Modal>
309
</div>
310
</>
311
);
312
}
313
314