- Notifications
You must be signed in to change notification settings - Fork928
feat: Add SSH Keys page on /preferences/ssh-keys#1478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { fireEvent, screen, within } from "@testing-library/react" | ||
import React from "react" | ||
import * as API from "../../../api/api" | ||
import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" | ||
import { MockGitSSHKey, renderWithAuth } from "../../../testHelpers/renderHelpers" | ||
import { Language as authXServiceLanguage } from "../../../xServices/auth/authXService" | ||
import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage" | ||
describe("SSH Keys Page", () => { | ||
it("shows the SSH key", async () => { | ||
renderWithAuth(<SSHKeysPage />) | ||
await screen.findByText(MockGitSSHKey.public_key) | ||
}) | ||
describe("regenerate SSH key", () => { | ||
describe("when it is success", () => { | ||
it("shows a success message and updates the ssh key on the page", async () => { | ||
renderWithAuth( | ||
<> | ||
<SSHKeysPage /> | ||
<GlobalSnackbar /> | ||
</>, | ||
) | ||
// Wait to the ssh be rendered on the screen | ||
await screen.findByText(MockGitSSHKey.public_key) | ||
// Click on the "Regenerate" button to display the confirm dialog | ||
const regenerateButton = screen.getByRole("button", { name: SSHKeysPageLanguage.regenerateLabel }) | ||
fireEvent.click(regenerateButton) | ||
const confirmDialog = screen.getByRole("dialog") | ||
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage) | ||
const newUserSSHKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66" | ||
jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({ | ||
...MockGitSSHKey, | ||
public_key: newUserSSHKey, | ||
}) | ||
// Click on the "Confirm" button | ||
const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel }) | ||
fireEvent.click(confirmButton) | ||
// Check if the success message is displayed | ||
await screen.findByText(authXServiceLanguage.successRegenerateSSHKey) | ||
// Check if the API was called correctly | ||
expect(API.regenerateUserSSHKey).toBeCalledTimes(1) | ||
// Check if the SSH key is updated | ||
await screen.findByText(newUserSSHKey) | ||
}) | ||
}) | ||
describe("when it fails", () => { | ||
it("shows an error message", async () => { | ||
renderWithAuth( | ||
<> | ||
<SSHKeysPage /> | ||
<GlobalSnackbar /> | ||
</>, | ||
) | ||
// Wait to the ssh be rendered on the screen | ||
await screen.findByText(MockGitSSHKey.public_key) | ||
jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce({}) | ||
// Click on the "Regenerate" button to display the confirm dialog | ||
const regenerateButton = screen.getByRole("button", { name: SSHKeysPageLanguage.regenerateLabel }) | ||
fireEvent.click(regenerateButton) | ||
const confirmDialog = screen.getByRole("dialog") | ||
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage) | ||
// Click on the "Confirm" button | ||
const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel }) | ||
fireEvent.click(confirmButton) | ||
// Check if the error message is displayed | ||
await screen.findByText(authXServiceLanguage.errorRegenerateSSHKey) | ||
// Check if the API was called correctly | ||
expect(API.regenerateUserSSHKey).toBeCalledTimes(1) | ||
}) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,76 @@ | ||
import Box from "@material-ui/core/Box" | ||
import Button from "@material-ui/core/Button" | ||
import CircularProgress from "@material-ui/core/CircularProgress" | ||
import { useActor } from "@xstate/react" | ||
import React, { useContext, useEffect } from "react" | ||
import { CodeBlock } from "../../../components/CodeBlock/CodeBlock" | ||
import { ConfirmDialog } from "../../../components/ConfirmDialog/ConfirmDialog" | ||
import { Section } from "../../../components/Section/Section" | ||
import { Stack } from "../../../components/Stack/Stack" | ||
import { XServiceContext } from "../../../xServices/StateContext" | ||
exportconst Language = { | ||
title: "SSH Keys", | ||
description: | ||
"Coder automatically inserts a private key into every workspace; you can add the corresponding public key to any services (such as Git) that you need access to from your workspace.", | ||
regenerateLabel: "Regenerate", | ||
regenerateDialogTitle: "Regenerate SSH Key?", | ||
regenerateDialogMessage: | ||
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.", | ||
confirmLabel: "Confirm", | ||
cancelLabel: "Cancel", | ||
} | ||
export const SSHKeysPage: React.FC = () => { | ||
const xServices = useContext(XServiceContext) | ||
const [authState, authSend] = useActor(xServices.authXService) | ||
const { sshKey } = authState.context | ||
useEffect(() => { | ||
authSend({ type: "GET_SSH_KEY" }) | ||
}, [authSend]) | ||
return ( | ||
<> | ||
<Section title={Language.title} description={Language.description}> | ||
{!sshKey && ( | ||
<Box p={4}> | ||
<CircularProgress size={26} /> | ||
</Box> | ||
)} | ||
{sshKey && ( | ||
<Stack> | ||
<CodeBlock lines={[sshKey.public_key.trim()]} /> | ||
<div> | ||
<Button | ||
color="primary" | ||
onClick={() => { | ||
authSend({ type: "REGENERATE_SSH_KEY" }) | ||
}} | ||
> | ||
{Language.regenerateLabel} | ||
</Button> | ||
</div> | ||
</Stack> | ||
)} | ||
</Section> | ||
<ConfirmDialog | ||
type="delete" | ||
hideCancel={false} | ||
open={authState.matches("signedIn.ssh.loaded.confirmSSHKeyRegenerate")} | ||
confirmLoading={authState.matches("signedIn.ssh.loaded.regeneratingSSHKey")} | ||
title={Language.regenerateDialogTitle} | ||
confirmText={Language.confirmLabel} | ||
onConfirm={() => { | ||
authSend({ type: "CONFIRM_REGENERATE_SSH_KEY" }) | ||
}} | ||
onClose={() => { | ||
authSend({ type: "CANCEL_REGENERATE_SSH_KEY" }) | ||
}} | ||
description={<>{Language.regenerateDialogMessage}</>} | ||
/> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
import { assign, createMachine } from "xstate" | ||
import * as API from "../../api/api" | ||
import * as TypesGen from "../../api/typesGenerated" | ||
import {displayError,displaySuccess } from "../../components/GlobalSnackbar/utils" | ||
export const Language = { | ||
successProfileUpdate: "Updated preferences.", | ||
successRegenerateSSHKey: "SSH Key regenerated successfully", | ||
errorRegenerateSSHKey: "Error on regenerate the SSH Key", | ||
} | ||
export const checks = { | ||
@@ -31,12 +33,87 @@ export interface AuthContext { | ||
methods?: TypesGen.AuthMethods | ||
permissions?: Permissions | ||
checkPermissionsError?: Error | unknown | ||
// SSH | ||
sshKey?: TypesGen.GitSSHKey | ||
getSSHKeyError?: Error | unknown | ||
regenerateSSHKeyError?: Error | unknown | ||
} | ||
export type AuthEvent = | ||
| { type: "SIGN_OUT" } | ||
| { type: "SIGN_IN"; email: string; password: string } | ||
| { type: "UPDATE_PROFILE"; data: TypesGen.UpdateUserProfileRequest } | ||
| { type: "GET_SSH_KEY" } | ||
| { type: "REGENERATE_SSH_KEY" } | ||
| { type: "CONFIRM_REGENERATE_SSH_KEY" } | ||
| { type: "CANCEL_REGENERATE_SSH_KEY" } | ||
Comment on lines +46 to +49 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Should we rename this | ||
const sshState = { | ||
initial: "idle", | ||
states: { | ||
idle: { | ||
on: { | ||
GET_SSH_KEY: { | ||
target: "gettingSSHKey", | ||
}, | ||
}, | ||
}, | ||
gettingSSHKey: { | ||
entry: "clearGetSSHKeyError", | ||
invoke: { | ||
src: "getSSHKey", | ||
onDone: [ | ||
{ | ||
actions: ["assignSSHKey"], | ||
target: "#authState.signedIn.ssh.loaded", | ||
}, | ||
], | ||
onError: [ | ||
{ | ||
actions: "assignGetSSHKeyError", | ||
target: "#authState.signedIn.ssh.idle", | ||
}, | ||
], | ||
}, | ||
}, | ||
loaded: { | ||
initial: "idle", | ||
states: { | ||
idle: { | ||
on: { | ||
REGENERATE_SSH_KEY: { | ||
target: "confirmSSHKeyRegenerate", | ||
}, | ||
}, | ||
}, | ||
confirmSSHKeyRegenerate: { | ||
on: { | ||
CANCEL_REGENERATE_SSH_KEY: "idle", | ||
CONFIRM_REGENERATE_SSH_KEY: "regeneratingSSHKey", | ||
}, | ||
}, | ||
regeneratingSSHKey: { | ||
entry: "clearRegenerateSSHKeyError", | ||
invoke: { | ||
src: "regenerateSSHKey", | ||
onDone: [ | ||
{ | ||
actions: ["assignSSHKey", "notifySuccessSSHKeyRegenerated"], | ||
target: "#authState.signedIn.ssh.loaded.idle", | ||
}, | ||
], | ||
onError: [ | ||
{ | ||
actions: ["assignRegenerateSSHKeyError", "notifySSHKeyRegenerationError"], | ||
target: "#authState.signedIn.ssh.loaded.idle", | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
export const authMachine = | ||
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogDsABgCshOQA4AzABY5ARgBMUgJxrtcgDQgAnok0zNSwkpkrNKvQDY3aqS6kBfb6bRYuPhEpBQk5FAM5LQQIkThAG58ANYhZORRYvyCwqJIEohyMlKE2mrFKo7aLq5SJuaILtq2mjZqrboy2s4+fiABOHgExOnhkdFgAE6TfJOEPAA2+ABmswC2IxSZ+dkkQiJikgiyCsrqWroGRqYWCHoq2oQyDpouXa1qGmq+-hiDwYQYOghBEAKqwKYxOKERIpIhAgCyYCyAj2uUOiF0mieTik3UqMhqSleN0QhgUOhUbzUKhk9jKch+-T+QWGQJBUHBkKmMzmixW60BYHQSJROQO+SOUmxVKUniUcjkUgVLjlpOOCqecj0LyKmmVeiUTIGrPhwo5SKwfAgsChlBh5CSqSFIuFmGt8B2qP2eVARxUeNKCo02k0cllTRc6qUalsCtU731DikvV+gSGZuBY0t7pttB5s3mS3Qq0mG0Rbo9YrREr9iHUMkIUnKHSklQVKnqtz0BkImjUbmeShcVmejL6Jozm0oECi8xmyxIC3iEGXtFBAAUACIAQQAKgBRNgbgBKVAAYgwADIH6s+jEIOV6Qgj1oxnTBkfq7qNlxyT4aC4saaPcVLGiyU6hDOc48AuS5EKgPAQPgYwbnBa6xPasLOpOAJQZAMHoQhSEoREaF8Iuy4ILCADGKEiAA2jIAC6d7opKiBPi+rRtB+-5fg0CDqM+YafG8+jWLI2jgemeHpAR5DzhR8GEIhyEcuRlFgPm0yFvyJaCrhwz4bOimwcpy6qSRGlEdRjp8HRPpMaxXrir6BSPvo3Fvu0zT8Zo6oDmoTb-p8cjaFcsjqDJ-zGfJpn0Mw7BUKCe5sbWHm6CU2jWKGnhaFSeLqv2jZKMOehOE49i0v+MWmtOYw0OgdrxPZzpQU16XuUcAC0-aPGGSgVQOTS4ko2jfjGhC0gB1WeDIBguHVkGjBETU6byRYCmW06da5NbdZiKalLl+p-k4XgTYJNhxuVNKGCB77fEy5DWnAYhGWkFDUBgXUPoqLiKOoxIqGVejNKoxXPCUMgjZ8zj6sORoThBclhBE2y8N67F1o+xIvhoejavqEX-gFgl6IGFWOC2bzWNqy0AuyYxcpMf0cQghjqn+JT6GJeJynIqrI2msWZhalY2uzuN9dojwpn+epDvqHjRkUL7KBDEljiLzKyXF32mUpWkwquRCvQeuls-t94c606s8c0A5C00UjqqDja5cUXQRXiDiMwb0FmURpuWQW1tY25D7242jsxn+bi6IFhh2K4qjKP+fsqAHX1B8bKkkGb0seR0gNx87idu4JtIwzoxTdELzbheOov1SZhEWcR6moURxdHCOgPhsBoNDRDKju84hCfHS4ZAXPqg59OCn58ufeFODQPD2DY-FUqGsASmdShgvKP67nClrwgQu2EPIPb2V4+CVNf7w5TOphs22en2LDVrb9Ns4w8j1coU8iafDKJVWQagDDqmOsOKBVhXDgweNJb+ppL59TcH2ZQw1E5jSurcYK8CHCODfE0B4vRfBAA */ | ||
@@ -70,6 +147,12 @@ export const authMachine = | ||
checkPermissions: { | ||
data: TypesGen.UserAuthorizationResponse | ||
} | ||
getSSHKey: { | ||
data: TypesGen.GitSSHKey | ||
} | ||
regenerateSSHKey: { | ||
data: TypesGen.GitSSHKey | ||
} | ||
}, | ||
}, | ||
id: "authState", | ||
@@ -197,6 +280,7 @@ export const authMachine = | ||
}, | ||
}, | ||
}, | ||
ssh: sshState, | ||
}, | ||
on: { | ||
SIGN_OUT: { | ||
@@ -249,6 +333,9 @@ export const authMachine = | ||
checks: permissionsToCheck, | ||
}) | ||
}, | ||
// SSH | ||
getSSHKey: () => API.getUserSSHKey(), | ||
regenerateSSHKey: () => API.regenerateUserSSHKey(), | ||
}, | ||
actions: { | ||
assignMe: assign({ | ||
@@ -302,6 +389,28 @@ export const authMachine = | ||
clearGetPermissionsError: assign({ | ||
checkPermissionsError: (_) => undefined, | ||
}), | ||
// SSH | ||
assignSSHKey: assign({ | ||
sshKey: (_, event) => event.data, | ||
}), | ||
assignGetSSHKeyError: assign({ | ||
getSSHKeyError: (_, event) => event.data, | ||
}), | ||
clearGetSSHKeyError: assign({ | ||
getSSHKeyError: (_) => undefined, | ||
}), | ||
assignRegenerateSSHKeyError: assign({ | ||
regenerateSSHKeyError: (_, event) => event.data, | ||
}), | ||
clearRegenerateSSHKeyError: assign({ | ||
regenerateSSHKeyError: (_) => undefined, | ||
}), | ||
notifySuccessSSHKeyRegenerated: () => { | ||
displaySuccess(Language.successRegenerateSSHKey) | ||
}, | ||
notifySSHKeyRegenerationError: () => { | ||
displayError(Language.errorRegenerateSSHKey) | ||
}, | ||
}, | ||
}, | ||
) |