Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/user-settings/PersonalAccessTokens.tsx
2500 views
1
/**
2
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
8
import { useCallback, useEffect, useState } from "react";
9
import { useLocation } from "react-router";
10
import { personalAccessTokensService } from "../service/public-api";
11
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
12
import { settingsPathPersonalAccessTokenCreate, settingsPathPersonalAccessTokenEdit } from "./settings.routes";
13
import { Timestamp } from "@bufbuild/protobuf";
14
import Alert from "../components/Alert";
15
import { InputWithCopy } from "../components/InputWithCopy";
16
import { copyToClipboard } from "../utils";
17
import PillLabel from "../components/PillLabel";
18
import dayjs from "dayjs";
19
import { SpinnerLoader } from "../components/Loader";
20
import TokenEntry from "./TokenEntry";
21
import ShowTokenModal from "./ShowTokenModal";
22
import Pagination from "../Pagination/Pagination";
23
import { Heading2, Subheading } from "../components/typography/headings";
24
import { Button } from "@podkit/buttons/Button";
25
import { LinkButton } from "@podkit/buttons/LinkButton";
26
27
export default function PersonalAccessTokens() {
28
return (
29
<div>
30
<PageWithSettingsSubMenu>
31
<ListAccessTokensView />
32
</PageWithSettingsSubMenu>
33
</div>
34
);
35
}
36
37
export enum TokenAction {
38
Create = "CREATED",
39
Regenerate = "REGENERATED",
40
Delete = "DELETE",
41
}
42
43
const expirationOptions = [7, 30, 60, 180].map((d) => ({
44
label: `${d} Days`,
45
value: `${d} Days`,
46
getDate: () => dayjs().add(d, "days").toDate(),
47
}));
48
49
// Max value of timestamp(6) in mysql is 2038-01-19 03:14:17
50
const NoExpiresDate = dayjs("2038-01-01T00:00:00+00:00").toDate();
51
export function getTokenExpirationDays(showForever: boolean) {
52
if (!showForever) {
53
return expirationOptions;
54
}
55
return [...expirationOptions, { label: "No expiration", value: "No expiration", getDate: () => NoExpiresDate }];
56
}
57
58
export function isNeverExpired(date: Date) {
59
return date.getTime() >= NoExpiresDate.getTime();
60
}
61
62
export function getTokenExpirationDescription(date: Date) {
63
if (isNeverExpired(date)) {
64
return "The token will never expire!";
65
}
66
return `The token will expire on ${dayjs(date).format("MMM D, YYYY")}`;
67
}
68
69
export const AllPermissions: PermissionDetail[] = [
70
{
71
name: "Full Access",
72
description: "Grant complete read and write access to the API.",
73
// TODO: what if scopes are duplicate? maybe use a key: uniq string; to filter will be better
74
scopes: ["function:*", "resource:default"],
75
},
76
];
77
78
export interface TokenInfo {
79
method: TokenAction;
80
data: PersonalAccessToken;
81
}
82
83
interface PermissionDetail {
84
name: string;
85
description: string;
86
scopes: string[];
87
}
88
89
function ListAccessTokensView() {
90
const location = useLocation();
91
92
const [loading, setLoading] = useState<boolean>(false);
93
const [tokens, setTokens] = useState<PersonalAccessToken[]>([]);
94
const [tokenInfo, setTokenInfo] = useState<TokenInfo>();
95
const [modalData, setModalData] = useState<{ token: PersonalAccessToken; action: TokenAction }>();
96
const [errorMsg, setErrorMsg] = useState("");
97
const [totalResults, setTotalResults] = useState<number>();
98
const pageLength = 25;
99
const [currentPage, setCurrentPage] = useState<number>(1);
100
101
const loadTokens = useCallback(async () => {
102
try {
103
setLoading(true);
104
const response = await personalAccessTokensService.listPersonalAccessTokens({
105
pagination: { pageSize: pageLength, page: currentPage },
106
});
107
setTokens(response.tokens);
108
setTotalResults(Number(response.totalResults));
109
} catch (e) {
110
setErrorMsg(e.message);
111
}
112
setLoading(false);
113
}, [currentPage]);
114
115
useEffect(() => {
116
loadTokens();
117
}, [loadTokens]);
118
119
useEffect(() => {
120
if (location.state) {
121
setTokenInfo(location.state as any as TokenInfo);
122
window.history.replaceState({}, "");
123
}
124
}, [location.state]);
125
126
const handleCopyToken = () => {
127
copyToClipboard(tokenInfo!.data.value);
128
};
129
130
const handleDeleteToken = async (tokenId: string) => {
131
try {
132
await personalAccessTokensService.deletePersonalAccessToken({ id: tokenId });
133
if (tokenId === tokenInfo?.data.id) {
134
setTokenInfo(undefined);
135
}
136
loadTokens();
137
setModalData(undefined);
138
} catch (e) {
139
setErrorMsg(e.message);
140
}
141
};
142
143
const handleRegenerateToken = async (tokenId: string, expirationDate: Date) => {
144
try {
145
const resp = await personalAccessTokensService.regeneratePersonalAccessToken({
146
id: tokenId,
147
expirationTime: Timestamp.fromDate(expirationDate),
148
});
149
setTokenInfo({ method: TokenAction.Regenerate, data: resp.token! });
150
loadTokens();
151
setModalData(undefined);
152
} catch (e) {
153
setErrorMsg(e.message);
154
}
155
};
156
157
const loadPage = (page: number = 1) => {
158
setCurrentPage(page);
159
};
160
161
return (
162
<>
163
<div className="flex items-center sm:justify-between mb-4">
164
<div>
165
<Heading2 className="flex gap-4 items-center">Access Tokens</Heading2>
166
<Subheading>
167
Create or regenerate access tokens.{" "}
168
<a
169
className="gp-link"
170
href="https://www.gitpod.io/docs/configure/user-settings/access-tokens"
171
target="_blank"
172
rel="noreferrer"
173
>
174
Learn more
175
</a>
176
</Subheading>
177
</div>
178
{tokens.length > 0 && (
179
<LinkButton href={settingsPathPersonalAccessTokenCreate}>New Access Token</LinkButton>
180
)}
181
</div>
182
{errorMsg.length > 0 && (
183
<Alert type="error" className="mb-2">
184
{errorMsg}
185
</Alert>
186
)}
187
{tokenInfo && (
188
<div className="p-4 mb-4 divide-y rounded-xl bg-pk-surface-secondary">
189
<div className="pb-2">
190
<div className="flex gap-2 content-center font-semibold text-gray-700 dark:text-gray-200">
191
<span>{tokenInfo.data.name}</span>
192
<PillLabel
193
type={tokenInfo.method === TokenAction.Create ? "success" : "info"}
194
className="py-0.5 px-1"
195
>
196
{tokenInfo.method.toUpperCase()}
197
</PillLabel>
198
</div>
199
<div className="text-gray-400 dark:text-gray-300">
200
<span>
201
{isNeverExpired(tokenInfo.data.expirationTime!.toDate())
202
? "Never expires!"
203
: `Expires on ${dayjs(tokenInfo.data.expirationTime!.toDate()).format(
204
"MMM D, YYYY",
205
)}`}
206
</span>
207
<span> · </span>
208
<span>Created on {dayjs(tokenInfo.data.createdAt!.toDate()).format("MMM D, YYYY")}</span>
209
</div>
210
</div>
211
<div className="pt-2">
212
<div className="font-semibold text-gray-600 dark:text-gray-200">Your New Access Token</div>
213
<InputWithCopy className="my-2 max-w-md" value={tokenInfo.data.value} tip="Copy Token" />
214
<div className="mb-2 font-medium text-sm text-gray-500 dark:text-gray-300">
215
Make sure to copy your access token — you won't be able to access it again.
216
</div>
217
<Button variant="secondary" onClick={handleCopyToken}>
218
Copy Token to Clipboard
219
</Button>
220
</div>
221
</div>
222
)}
223
{loading ? (
224
<SpinnerLoader content="loading access token list" />
225
) : (
226
<>
227
{tokens.length === 0 ? (
228
<div className="bg-pk-surface-secondary rounded-xl w-full py-28 flex flex-col items-center">
229
<Heading2 className="text-center pb-3 text-pk-content-invert-secondary">
230
No Access Tokens
231
</Heading2>
232
<Subheading className="text-center pb-6 w-96">
233
Generate an access token for applications that need access to the Gitpod API.{" "}
234
</Subheading>
235
<LinkButton href={settingsPathPersonalAccessTokenCreate}>New Access Token</LinkButton>
236
</div>
237
) : (
238
<>
239
<div className="px-3 py-3 flex justify-between space-x-2 text-sm text-gray-400 mb-2 bg-pk-surface-secondary rounded-xl">
240
<Subheading className="w-4/12">Token Name</Subheading>
241
<Subheading className="w-4/12">Permissions</Subheading>
242
<Subheading className="w-3/12">Expires</Subheading>
243
<div className="w-1/12"></div>
244
</div>
245
{tokens.map((t: PersonalAccessToken) => (
246
<TokenEntry
247
key={t.id}
248
token={t}
249
menuEntries={[
250
{
251
title: "Edit",
252
link: `${settingsPathPersonalAccessTokenEdit}/${t.id}`,
253
},
254
{
255
title: "Regenerate",
256
href: "",
257
customFontStyle:
258
"text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
259
onClick: () => setModalData({ token: t, action: TokenAction.Regenerate }),
260
},
261
{
262
title: "Delete",
263
href: "",
264
customFontStyle:
265
"text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
266
onClick: () => setModalData({ token: t, action: TokenAction.Delete }),
267
},
268
]}
269
/>
270
))}
271
{totalResults && (
272
<Pagination
273
totalNumberOfPages={Math.ceil(totalResults / pageLength)}
274
currentPage={currentPage}
275
setPage={loadPage}
276
/>
277
)}
278
</>
279
)}
280
</>
281
)}
282
283
{modalData?.action === TokenAction.Delete && (
284
<ShowTokenModal
285
token={modalData.token}
286
title="Delete Access Token"
287
description="Are you sure you want to delete this access token?"
288
descriptionImportant="Any applications using this token will no longer be able to access the Gitpod API."
289
actionDescription="Delete Access Token"
290
onSave={() => handleDeleteToken(modalData.token.id)}
291
onClose={() => setModalData(undefined)}
292
/>
293
)}
294
{modalData?.action === TokenAction.Regenerate && (
295
<ShowTokenModal
296
token={modalData.token}
297
title="Regenerate Token"
298
description="Are you sure you want to regenerate this access token?"
299
descriptionImportant="Any applications using this token will no longer be able to access the Gitpod API."
300
actionDescription="Regenerate Token"
301
showDateSelector
302
onSave={({ expirationDate }) => handleRegenerateToken(modalData.token.id, expirationDate)}
303
onClose={() => setModalData(undefined)}
304
/>
305
)}
306
</>
307
);
308
}
309
310