Path: blob/master/src/packages/frontend/components/api-keys.tsx
1503 views
/*1React component for managing a list of api keys.23Applications:45- the keys for a project6- the keys for an account7*/89import {10Alert,11Button,12DatePicker,13Form,14Input,15Modal,16Popconfirm,17Space,18Table,19Typography,20} from "antd";21import { ColumnsType } from "antd/es/table";22import dayjs from "dayjs";23import { useEffect, useState } from "react";24import TimeAgo from "react-timeago"; // so can use from nextjs25const { Text, Paragraph } = Typography; // so can use from nextjs2627import { CancelText } from "@cocalc/frontend/i18n/components";28import type { ApiKey } from "@cocalc/util/db-schema/api-keys";29import { A } from "./A";30import CopyToClipBoard from "./copy-to-clipboard";31import { Icon } from "./icon";3233const { useForm } = Form;3435interface 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 truncated41// 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 returned44// 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 id46// to have the given name. Similar for expire.47manage: (opts: {48action: "get" | "delete" | "create" | "edit";49id?: number;50name?: string;51expire?: Date;52}) => Promise<ApiKey[] | undefined>;53mode?: "project" | "flyout";54}5556export default function ApiKeys({ manage, mode = "project" }: Props) {57const isFlyout = mode === "flyout";58const size = isFlyout ? "small" : undefined; // for e.g. buttons59const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);60const [loading, setLoading] = useState<boolean>(true);61const [editingKey, setEditingKey] = useState<number | undefined>(undefined);62const [addModalVisible, setAddModalVisible] = useState<boolean>(false);63const [editModalVisible, setEditModalVisible] = useState<boolean>(false);64const [form] = useForm();65const [error, setError] = useState<string | null>(null);6667useEffect(() => {68getAllApiKeys();69}, []);7071const getAllApiKeys = async () => {72setLoading(true);73try {74const response = await manage({ action: "get" });75setApiKeys(response as ApiKey[]);76setLoading(false);77setError(null);78} catch (err) {79setLoading(false);80setError(`${err}`);81}82};8384const deleteApiKey = async (id: number) => {85try {86await manage({ action: "delete", id });87getAllApiKeys();88} catch (err) {89setError(`${err}`);90}91};9293const deleteAllApiKeys = async () => {94for (const { id } of apiKeys) {95await deleteApiKey(id);96}97};9899const editApiKey = async (id: number, name: string, expire?: Date) => {100try {101await manage({ action: "edit", id, name, expire });102getAllApiKeys();103} catch (err) {104setError(`${err}`);105}106};107108const createApiKey = async (name: string, expire?: Date) => {109try {110const response = await manage({111action: "create",112name,113expire,114});115setAddModalVisible(false);116getAllApiKeys();117118Modal.success({119width: 600,120title: "New Secret API Key",121content: (122<>123<div>124Save this secret key somewhere safe.{" "}125<b>You won't be able to view it again here.</b> If you lose this126secret key, you'll need to generate a new one.127</div>128<div style={{ marginTop: 16 }}>129<strong>Secret API Key</strong>{" "}130<CopyToClipBoard131style={{ marginTop: "16px" }}132value={response?.[0].secret ?? "failed to get secret"}133/>134</div>135</>136),137});138setError(null);139} catch (err) {140setError(`${err}`);141}142};143144const columns: ColumnsType<ApiKey> = [145{146dataIndex: "name",147title: "Name/Key",148render: (name, record) => {149return (150<>151{name}152<br />153<Text type="secondary">({record.trunc})</Text>154</>155);156},157},158{159dataIndex: "last_active",160title: "Last Used",161render: (last_active) =>162last_active ? <TimeAgo date={last_active} /> : "Never",163},164{165dataIndex: "expire",166title: "Expire",167render: (expire) => (expire ? <TimeAgo date={expire} /> : "Never"),168},169{170dataIndex: "operation",171title: "Operation",172align: "right",173render: (_text, record) => (174<Space.Compact direction={isFlyout ? "vertical" : "horizontal"}>175<Popconfirm176title="Are you sure you want to delete this key?"177onConfirm={() => deleteApiKey(record.id)}178>179<a>Delete</a>180</Popconfirm>181<a182onClick={() => {183// Set the initial form value as the current key name184form.setFieldsValue({ name: record.name });185setEditModalVisible(true);186setEditingKey(record.id);187}}188style={{ marginLeft: "1em" }}189>190Edit191</a>192</Space.Compact>193),194},195];196197if (!isFlyout) {198columns.splice(1, 0, { dataIndex: "id", title: "Id" });199}200201const handleAdd = () => {202setAddModalVisible(true);203};204205const handleModalOK = () => {206const name = form.getFieldValue("name");207const expire = form.getFieldValue("expire");208if (editingKey != null) {209editApiKey(editingKey, name, expire);210setEditModalVisible(false);211setEditingKey(undefined);212form.resetFields();213} else {214createApiKey(name, expire);215form.resetFields();216}217};218219const handleModalCancel = () => {220setAddModalVisible(false);221setEditModalVisible(false);222setEditingKey(undefined);223form.resetFields();224};225226return (227<>228{error && (229<Alert230message={error}231type="error"232closable233onClose={() => setError(null)}234style={{ marginBottom: 16 }}235/>236)}237{apiKeys.length > 0 && (238<Table239style={{ marginBottom: 16 }}240dataSource={apiKeys}241columns={columns}242loading={loading}243rowKey="id"244pagination={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}>253Refresh254</Button>255{apiKeys.length > 0 && (256<Popconfirm257title="Are you sure you want to delete all these api keys?"258onConfirm={deleteAllApiKeys}259>260<Button danger size={size}>261Delete All...262</Button>263</Popconfirm>264)}265</Space.Compact>266<Paragraph style={{ marginTop: "10px" }}>267Read the <A href="https://doc.cocalc.com/api/">API documentation</A>.268</Paragraph>269<Modal270open={addModalVisible || editModalVisible}271title={272editingKey != null ? "Edit API Key Name" : "Create a New API Key"273}274okText={editingKey != null ? "Save" : "Create"}275cancelText={<CancelText />}276onCancel={handleModalCancel}277onOk={handleModalOK}278>279<Form form={form} layout="vertical">280<Form.Item281name="name"282label="Name"283rules={[{ required: true, message: "Please enter a name" }]}284>285<Input />286</Form.Item>287<Form.Item288name="expire"289label="Expire"290rules={[291{292required: false,293message:294"Optional date when key will be automatically deleted",295},296]}297>298<DatePicker299changeOnBlur300showTime301disabledDate={(current) => {302// disable all dates before today303return current && current < dayjs();304}}305/>306</Form.Item>307</Form>308</Modal>309</div>310</>311);312}313314315