CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/components/api-keys.tsx
Views: 687
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.message || "An error occurred");
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.message || "An error occurred");
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.message || "An error occurred");
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
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.message || "An error occurred");
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");
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/api/">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