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