Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitabbe548

Browse files
feat: Add SSH Keys page on /preferences/ssh-keys (#1478)
1 parent5447c4a commitabbe548

File tree

7 files changed

+284
-16
lines changed

7 files changed

+284
-16
lines changed

‎site/src/api/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,13 @@ export const updateUserRoles = async (
206206
constresponse=awaitaxios.put<TypesGen.User>(`/api/v2/users/${userId}/roles`,{ roles})
207207
returnresponse.data
208208
}
209+
210+
exportconstgetUserSSHKey=async(userId="me"):Promise<TypesGen.GitSSHKey>=>{
211+
constresponse=awaitaxios.get<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
212+
returnresponse.data
213+
}
214+
215+
exportconstregenerateUserSSHKey=async(userId="me"):Promise<TypesGen.GitSSHKey>=>{
216+
constresponse=awaitaxios.put<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
217+
returnresponse.data
218+
}

‎site/src/pages/PreferencesPages/AccountPage/SSHKeysPage.tsx

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import{fireEvent,screen,within}from"@testing-library/react"
2+
importReactfrom"react"
3+
import*asAPIfrom"../../../api/api"
4+
import{GlobalSnackbar}from"../../../components/GlobalSnackbar/GlobalSnackbar"
5+
import{MockGitSSHKey,renderWithAuth}from"../../../testHelpers/renderHelpers"
6+
import{LanguageasauthXServiceLanguage}from"../../../xServices/auth/authXService"
7+
import{LanguageasSSHKeysPageLanguage,SSHKeysPage}from"./SSHKeysPage"
8+
9+
describe("SSH Keys Page",()=>{
10+
it("shows the SSH key",async()=>{
11+
renderWithAuth(<SSHKeysPage/>)
12+
awaitscreen.findByText(MockGitSSHKey.public_key)
13+
})
14+
15+
describe("regenerate SSH key",()=>{
16+
describe("when it is success",()=>{
17+
it("shows a success message and updates the ssh key on the page",async()=>{
18+
renderWithAuth(
19+
<>
20+
<SSHKeysPage/>
21+
<GlobalSnackbar/>
22+
</>,
23+
)
24+
25+
// Wait to the ssh be rendered on the screen
26+
awaitscreen.findByText(MockGitSSHKey.public_key)
27+
28+
// Click on the "Regenerate" button to display the confirm dialog
29+
constregenerateButton=screen.getByRole("button",{name:SSHKeysPageLanguage.regenerateLabel})
30+
fireEvent.click(regenerateButton)
31+
constconfirmDialog=screen.getByRole("dialog")
32+
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage)
33+
34+
constnewUserSSHKey="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"
35+
jest.spyOn(API,"regenerateUserSSHKey").mockResolvedValueOnce({
36+
...MockGitSSHKey,
37+
public_key:newUserSSHKey,
38+
})
39+
40+
// Click on the "Confirm" button
41+
constconfirmButton=within(confirmDialog).getByRole("button",{name:SSHKeysPageLanguage.confirmLabel})
42+
fireEvent.click(confirmButton)
43+
44+
// Check if the success message is displayed
45+
awaitscreen.findByText(authXServiceLanguage.successRegenerateSSHKey)
46+
47+
// Check if the API was called correctly
48+
expect(API.regenerateUserSSHKey).toBeCalledTimes(1)
49+
50+
// Check if the SSH key is updated
51+
awaitscreen.findByText(newUserSSHKey)
52+
})
53+
})
54+
55+
describe("when it fails",()=>{
56+
it("shows an error message",async()=>{
57+
renderWithAuth(
58+
<>
59+
<SSHKeysPage/>
60+
<GlobalSnackbar/>
61+
</>,
62+
)
63+
64+
// Wait to the ssh be rendered on the screen
65+
awaitscreen.findByText(MockGitSSHKey.public_key)
66+
67+
jest.spyOn(API,"regenerateUserSSHKey").mockRejectedValueOnce({})
68+
69+
// Click on the "Regenerate" button to display the confirm dialog
70+
constregenerateButton=screen.getByRole("button",{name:SSHKeysPageLanguage.regenerateLabel})
71+
fireEvent.click(regenerateButton)
72+
constconfirmDialog=screen.getByRole("dialog")
73+
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage)
74+
75+
// Click on the "Confirm" button
76+
constconfirmButton=within(confirmDialog).getByRole("button",{name:SSHKeysPageLanguage.confirmLabel})
77+
fireEvent.click(confirmButton)
78+
79+
// Check if the error message is displayed
80+
awaitscreen.findByText(authXServiceLanguage.errorRegenerateSSHKey)
81+
82+
// Check if the API was called correctly
83+
expect(API.regenerateUserSSHKey).toBeCalledTimes(1)
84+
})
85+
})
86+
})
87+
})
Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,76 @@
1-
importReactfrom"react"
1+
importBoxfrom"@material-ui/core/Box"
2+
importButtonfrom"@material-ui/core/Button"
3+
importCircularProgressfrom"@material-ui/core/CircularProgress"
4+
import{useActor}from"@xstate/react"
5+
importReact,{useContext,useEffect}from"react"
6+
import{CodeBlock}from"../../../components/CodeBlock/CodeBlock"
7+
import{ConfirmDialog}from"../../../components/ConfirmDialog/ConfirmDialog"
28
import{Section}from"../../../components/Section/Section"
9+
import{Stack}from"../../../components/Stack/Stack"
10+
import{XServiceContext}from"../../../xServices/StateContext"
311

4-
constLanguage={
12+
exportconstLanguage={
513
title:"SSH Keys",
614
description:
715
"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.",
16+
regenerateLabel:"Regenerate",
17+
regenerateDialogTitle:"Regenerate SSH Key?",
18+
regenerateDialogMessage:
19+
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
20+
confirmLabel:"Confirm",
21+
cancelLabel:"Cancel",
822
}
923

1024
exportconstSSHKeysPage:React.FC=()=>{
11-
return<Sectiontitle={Language.title}description={Language.description}/>
25+
constxServices=useContext(XServiceContext)
26+
const[authState,authSend]=useActor(xServices.authXService)
27+
const{ sshKey}=authState.context
28+
29+
useEffect(()=>{
30+
authSend({type:"GET_SSH_KEY"})
31+
},[authSend])
32+
33+
return(
34+
<>
35+
<Sectiontitle={Language.title}description={Language.description}>
36+
{!sshKey&&(
37+
<Boxp={4}>
38+
<CircularProgresssize={26}/>
39+
</Box>
40+
)}
41+
42+
{sshKey&&(
43+
<Stack>
44+
<CodeBlocklines={[sshKey.public_key.trim()]}/>
45+
<div>
46+
<Button
47+
color="primary"
48+
onClick={()=>{
49+
authSend({type:"REGENERATE_SSH_KEY"})
50+
}}
51+
>
52+
{Language.regenerateLabel}
53+
</Button>
54+
</div>
55+
</Stack>
56+
)}
57+
</Section>
58+
59+
<ConfirmDialog
60+
type="delete"
61+
hideCancel={false}
62+
open={authState.matches("signedIn.ssh.loaded.confirmSSHKeyRegenerate")}
63+
confirmLoading={authState.matches("signedIn.ssh.loaded.regeneratingSSHKey")}
64+
title={Language.regenerateDialogTitle}
65+
confirmText={Language.confirmLabel}
66+
onConfirm={()=>{
67+
authSend({type:"CONFIRM_REGENERATE_SSH_KEY"})
68+
}}
69+
onClose={()=>{
70+
authSend({type:"CANCEL_REGENERATE_SSH_KEY"})
71+
}}
72+
description={<>{Language.regenerateDialogMessage}</>}
73+
/>
74+
</>
75+
)
1276
}

‎site/src/testHelpers/entities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,10 @@ export const MockAuthMethods: TypesGen.AuthMethods = {
205205
password:true,
206206
github:false,
207207
}
208+
209+
exportconstMockGitSSHKey:TypesGen.GitSSHKey={
210+
user_id:"1fa0200f-7331-4524-a364-35770666caa7",
211+
created_at:"2022-05-16T14:30:34.148205897Z",
212+
updated_at:"2022-05-16T15:29:10.302441433Z",
213+
public_key:"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJOQRIM7kE30rOzrfy+/+R+nQGCk7S9pioihy+2ARbq",
214+
}

‎site/src/testHelpers/handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export const handlers = [
6868

6969
returnres(ctx.status(200),ctx.json(response))
7070
}),
71+
rest.get("/api/v2/users/:userId/gitsshkey",async(req,res,ctx)=>{
72+
returnres(ctx.status(200),ctx.json(M.MockGitSSHKey))
73+
}),
7174

7275
// workspaces
7376
rest.get("/api/v2/organizations/:organizationId/workspaces/:userName/:workspaceName",(req,res,ctx)=>{

‎site/src/xServices/auth/authXService.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import{assign,createMachine}from"xstate"
22
import*asAPIfrom"../../api/api"
33
import*asTypesGenfrom"../../api/typesGenerated"
4-
import{displaySuccess}from"../../components/GlobalSnackbar/utils"
4+
import{displayError,displaySuccess}from"../../components/GlobalSnackbar/utils"
55

66
exportconstLanguage={
77
successProfileUpdate:"Updated preferences.",
8+
successRegenerateSSHKey:"SSH Key regenerated successfully",
9+
errorRegenerateSSHKey:"Error on regenerate the SSH Key",
810
}
911

1012
exportconstchecks={
@@ -31,12 +33,87 @@ export interface AuthContext {
3133
methods?:TypesGen.AuthMethods
3234
permissions?:Permissions
3335
checkPermissionsError?:Error|unknown
36+
// SSH
37+
sshKey?:TypesGen.GitSSHKey
38+
getSSHKeyError?:Error|unknown
39+
regenerateSSHKeyError?:Error|unknown
3440
}
3541

3642
exporttypeAuthEvent=
3743
|{type:"SIGN_OUT"}
3844
|{type:"SIGN_IN";email:string;password:string}
3945
|{type:"UPDATE_PROFILE";data:TypesGen.UpdateUserProfileRequest}
46+
|{type:"GET_SSH_KEY"}
47+
|{type:"REGENERATE_SSH_KEY"}
48+
|{type:"CONFIRM_REGENERATE_SSH_KEY"}
49+
|{type:"CANCEL_REGENERATE_SSH_KEY"}
50+
51+
constsshState={
52+
initial:"idle",
53+
states:{
54+
idle:{
55+
on:{
56+
GET_SSH_KEY:{
57+
target:"gettingSSHKey",
58+
},
59+
},
60+
},
61+
gettingSSHKey:{
62+
entry:"clearGetSSHKeyError",
63+
invoke:{
64+
src:"getSSHKey",
65+
onDone:[
66+
{
67+
actions:["assignSSHKey"],
68+
target:"#authState.signedIn.ssh.loaded",
69+
},
70+
],
71+
onError:[
72+
{
73+
actions:"assignGetSSHKeyError",
74+
target:"#authState.signedIn.ssh.idle",
75+
},
76+
],
77+
},
78+
},
79+
loaded:{
80+
initial:"idle",
81+
states:{
82+
idle:{
83+
on:{
84+
REGENERATE_SSH_KEY:{
85+
target:"confirmSSHKeyRegenerate",
86+
},
87+
},
88+
},
89+
confirmSSHKeyRegenerate:{
90+
on:{
91+
CANCEL_REGENERATE_SSH_KEY:"idle",
92+
CONFIRM_REGENERATE_SSH_KEY:"regeneratingSSHKey",
93+
},
94+
},
95+
regeneratingSSHKey:{
96+
entry:"clearRegenerateSSHKeyError",
97+
invoke:{
98+
src:"regenerateSSHKey",
99+
onDone:[
100+
{
101+
actions:["assignSSHKey","notifySuccessSSHKeyRegenerated"],
102+
target:"#authState.signedIn.ssh.loaded.idle",
103+
},
104+
],
105+
onError:[
106+
{
107+
actions:["assignRegenerateSSHKeyError","notifySSHKeyRegenerationError"],
108+
target:"#authState.signedIn.ssh.loaded.idle",
109+
},
110+
],
111+
},
112+
},
113+
},
114+
},
115+
},
116+
}
40117

41118
exportconstauthMachine=
42119
/**@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 =
70147
checkPermissions:{
71148
data:TypesGen.UserAuthorizationResponse
72149
}
150+
getSSHKey:{
151+
data:TypesGen.GitSSHKey
152+
}
153+
regenerateSSHKey:{
154+
data:TypesGen.GitSSHKey
155+
}
73156
},
74157
},
75158
id:"authState",
@@ -197,6 +280,7 @@ export const authMachine =
197280
},
198281
},
199282
},
283+
ssh:sshState,
200284
},
201285
on:{
202286
SIGN_OUT:{
@@ -249,6 +333,9 @@ export const authMachine =
249333
checks:permissionsToCheck,
250334
})
251335
},
336+
// SSH
337+
getSSHKey:()=>API.getUserSSHKey(),
338+
regenerateSSHKey:()=>API.regenerateUserSSHKey(),
252339
},
253340
actions:{
254341
assignMe:assign({
@@ -302,6 +389,28 @@ export const authMachine =
302389
clearGetPermissionsError:assign({
303390
checkPermissionsError:(_)=>undefined,
304391
}),
392+
// SSH
393+
assignSSHKey:assign({
394+
sshKey:(_,event)=>event.data,
395+
}),
396+
assignGetSSHKeyError:assign({
397+
getSSHKeyError:(_,event)=>event.data,
398+
}),
399+
clearGetSSHKeyError:assign({
400+
getSSHKeyError:(_)=>undefined,
401+
}),
402+
assignRegenerateSSHKeyError:assign({
403+
regenerateSSHKeyError:(_,event)=>event.data,
404+
}),
405+
clearRegenerateSSHKeyError:assign({
406+
regenerateSSHKeyError:(_)=>undefined,
407+
}),
408+
notifySuccessSSHKeyRegenerated:()=>{
409+
displaySuccess(Language.successRegenerateSSHKey)
410+
},
411+
notifySSHKeyRegenerationError:()=>{
412+
displayError(Language.errorRegenerateSSHKey)
413+
},
305414
},
306415
},
307416
)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp