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/account/ssh-keys/ssh-key-adder.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Button, Card, Input, Space } from "antd";
7
import { useState } from "react";
8
import { useIntl } from "react-intl";
9
10
import { A, ErrorDisplay, Icon } from "@cocalc/frontend/components";
11
import { labels } from "@cocalc/frontend/i18n";
12
13
import { compute_fingerprint } from "./fingerprint";
14
15
const ALLOWED_SSH_TYPES = [
16
"ssh-rsa",
17
"ssh-dss",
18
"ssh-ed25519",
19
"ecdsa-sha2-nistp256",
20
"ecdsa-sha2-nistp384",
21
"ecdsa-sha2-nistp521",
22
] as const;
23
24
const ALLOWED_SSH_TYPES_DESCRIPTION =
25
ALLOWED_SSH_TYPES.slice(0, -1).join(", ") +
26
", or " +
27
ALLOWED_SSH_TYPES[ALLOWED_SSH_TYPES.length - 1];
28
29
// Removes all new lines and trims the output
30
// Newlines are simply illegal in SSH keys
31
const normalize_key = (value) =>
32
value
33
.trim()
34
.split(/[\r\n]+/)
35
.join("");
36
37
// Splits an SSH key into its parts. Doesn't allow options
38
// Assumes the key has valid formatting ie.
39
// <key-type>[space]<public-key>[space]<comment>
40
interface ParsedKey {
41
type?: string;
42
pubkey?: string;
43
source?: string;
44
comments?: string;
45
error?: string;
46
value: string;
47
}
48
const parse_key = function (value: string): ParsedKey {
49
const parts: string[] = value.split(/\s+/);
50
const type = parts[0];
51
const pubkey = parts[1];
52
const source = parts[2];
53
const comments = parts.slice(3).join(" ");
54
55
return { value, type, pubkey, source, comments };
56
};
57
58
const validate_key = function (value): ParsedKey {
59
const key = parse_key(value);
60
if (!ALLOWED_SSH_TYPES.includes(key.type as any)) {
61
key.error = "Invalid key or type not supported";
62
} else {
63
delete key.error;
64
}
65
// TODO: Use some validation library?
66
return key;
67
};
68
69
interface Props {
70
add_ssh_key: Function;
71
toggleable?: boolean;
72
style?: React.CSSProperties;
73
extra?: JSX.Element;
74
}
75
76
export const SSHKeyAdder: React.FC<Props> = (props: Props) => {
77
const { add_ssh_key, toggleable, style, extra } = props;
78
const intl = useIntl();
79
const [key_title, set_key_title] = useState<string>("");
80
const [key_value, set_key_value] = useState<string>("");
81
const [show_panel, set_show_panel] = useState<boolean>(false);
82
const [error, set_error] = useState<undefined | string>(undefined);
83
84
const addKey = intl.formatMessage({
85
id: "account.ssh-key-adder.button",
86
defaultMessage: "Add SSH Key",
87
});
88
89
function cancel_and_close() {
90
set_key_title("");
91
set_key_value("");
92
set_show_panel(!toggleable);
93
set_error(undefined);
94
}
95
96
function submit_form(e?): void {
97
let title;
98
e?.preventDefault();
99
try {
100
const validated_key = validate_key(normalize_key(key_value));
101
if (validated_key.error != null) {
102
set_error(validated_key.error);
103
return;
104
} else {
105
set_error(undefined);
106
}
107
108
if (key_title) {
109
title = key_title;
110
} else {
111
title = validated_key.source;
112
}
113
114
const { value } = validated_key;
115
116
add_ssh_key({
117
title,
118
value,
119
fingerprint: compute_fingerprint(validated_key.pubkey),
120
});
121
122
cancel_and_close();
123
} catch (err) {
124
set_error(err.toString());
125
}
126
}
127
128
function render_panel() {
129
return (
130
<Card
131
title={
132
<>
133
<Icon name="plus-circle" />{" "}
134
{intl.formatMessage(
135
{
136
id: "account.ssh-key-adder.title",
137
defaultMessage: "Add an <A>SSH key</A>",
138
},
139
{
140
A: (c) => (
141
<A href="https://doc.cocalc.com/account/ssh.html">{c}</A>
142
),
143
},
144
)}
145
</>
146
}
147
style={style}
148
>
149
{extra && extra}
150
<div>
151
Title
152
<Input
153
id="ssh-title"
154
value={key_title}
155
onChange={(e) => set_key_title(e.target.value)}
156
placeholder={intl.formatMessage({
157
id: "account.ssh-key-adder.placeholder",
158
defaultMessage:
159
"Choose a name for this ssh key to help you keep track of it...",
160
})}
161
/>
162
<div style={{ marginTop: "15px" }}>
163
Key
164
<Input.TextArea
165
value={key_value}
166
rows={8}
167
placeholder={`Begins with ${ALLOWED_SSH_TYPES_DESCRIPTION}`}
168
onChange={(e) => set_key_value((e.target as any).value)}
169
onKeyDown={(e) => {
170
if (e.keyCode == 13) {
171
e.preventDefault();
172
submit_form();
173
}
174
}}
175
style={{ resize: "vertical" }}
176
/>
177
</div>
178
</div>
179
<div style={{ marginTop: "15px" }}>
180
<Space>
181
{toggleable ? (
182
<Button onClick={cancel_and_close}>
183
{intl.formatMessage(labels.cancel)}
184
</Button>
185
) : undefined}
186
<Button
187
type="primary"
188
onClick={submit_form}
189
disabled={key_value.length < 10}
190
>
191
{addKey}
192
</Button>
193
</Space>
194
{error && (
195
<ErrorDisplay
196
error={error}
197
onClose={() => set_error(undefined)}
198
style={{ marginTop: "10px" }}
199
/>
200
)}
201
</div>
202
</Card>
203
);
204
}
205
206
function render_open_button() {
207
return (
208
<Button onClick={() => set_show_panel(true)} style={style}>
209
<Icon name="terminal" /> {addKey}...
210
</Button>
211
);
212
}
213
214
if (!toggleable || show_panel) {
215
return render_panel();
216
} else {
217
return render_open_button();
218
}
219
};
220
221