Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e9ba719

Browse files
committed
Add basic flow using mocks
1 parent 43bca98 commit e9ba719

File tree

4 files changed

+218
-20
lines changed

4 files changed

+218
-20
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,5 @@
212212
// We often use a version of TypeScript that's ahead of the version shipped
213213
// with VS Code.
214214
"typescript.tsdk": "./site/node_modules/typescript/lib",
215-
"prettier.prettierPath": "./node_modules/prettier"
215+
"prettier.prettierPath": "./site/node_modules/prettier"
216216
}

site/src/components/SettingsAccountForm/SettingsAccountForm.test.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ describe("AccountForm", () => {
2424
onSubmit={() => {
2525
return
2626
}}
27+
onChangeToOIDCAuth={() => {
28+
return
29+
}}
2730
/>,
2831
)
2932

@@ -54,6 +57,9 @@ describe("AccountForm", () => {
5457
onSubmit={() => {
5558
return
5659
}}
60+
onChangeToOIDCAuth={() => {
61+
return
62+
}}
5763
/>,
5864
)
5965

site/src/components/SettingsAccountForm/SettingsAccountForm.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { LoadingButton } from "../LoadingButton/LoadingButton"
1111
import { ErrorAlert } from "components/Alert/ErrorAlert"
1212
import { Form, FormFields } from "components/Form/Form"
13+
import { Stack } from "components/Stack/Stack"
14+
import Button from "@mui/material/Button"
1315

1416
export interface AccountFormValues {
1517
username: string
@@ -34,6 +36,7 @@ export interface AccountFormProps {
3436
updateProfileError?: Error | unknown
3537
// initialTouched is only used for testing the error state of the form.
3638
initialTouched?: FormikTouched<AccountFormValues>
39+
onChangeToOIDCAuth: () => void
3740
}
3841

3942
export const AccountForm: FC<React.PropsWithChildren<AccountFormProps>> = ({
@@ -44,6 +47,7 @@ export const AccountForm: FC<React.PropsWithChildren<AccountFormProps>> = ({
4447
initialValues,
4548
updateProfileError,
4649
initialTouched,
50+
onChangeToOIDCAuth
4751
}) => {
4852
const form: FormikContextType<AccountFormValues> =
4953
useFormik<AccountFormValues>({
@@ -80,7 +84,7 @@ export const AccountForm: FC<React.PropsWithChildren<AccountFormProps>> = ({
8084
label={Language.usernameLabel}
8185
/>
8286

83-
<div>
87+
<Stack direction="row">
8488
<LoadingButton
8589
loading={isLoading}
8690
aria-disabled={!editable}
@@ -90,7 +94,9 @@ export const AccountForm: FC<React.PropsWithChildren<AccountFormProps>> = ({
9094
>
9195
{isLoading ? "" : Language.updateSettings}
9296
</LoadingButton>
93-
</div>
97+
98+
<Button type="button" onClick={onChangeToOIDCAuth}>Use OIDC to authenticate</Button>
99+
</Stack>
94100
</FormFields>
95101
</Form>
96102
</>

site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx

+203-17
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,221 @@
1-
import { FC } from "react"
1+
import { FC, useState } from "react"
22
import { Section } from "../../../components/SettingsLayout/Section"
33
import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm"
44
import { useAuth } from "components/AuthProvider/AuthProvider"
55
import { useMe } from "hooks/useMe"
66
import { usePermissions } from "hooks/usePermissions"
7+
import { Dialog } from "components/Dialogs/Dialog"
8+
import TextField from "@mui/material/TextField"
9+
import { FormFields, VerticalForm } from "components/Form/Form"
10+
import { LoadingButton } from "components/LoadingButton/LoadingButton"
11+
import Box from "@mui/material/Box"
12+
import GitHubIcon from "@mui/icons-material/GitHub"
13+
import KeyIcon from "@mui/icons-material/VpnKey"
14+
import Button from "@mui/material/Button"
15+
import { MockAuthMethods } from "testHelpers/entities"
16+
import CircularProgress from "@mui/material/CircularProgress"
17+
import { useLocation } from "react-router-dom"
18+
import { retrieveRedirect } from "utils/redirect"
19+
import Typography from "@mui/material/Typography"
20+
21+
type OIDCState =
22+
| { status: "closed" }
23+
| { status: "confirmPassword"; error?: unknown }
24+
| { status: "confirmingPassword" }
25+
| { status: "selectOIDCProvider" }
26+
| { status: "updatingProvider" }
727

828
export const AccountPage: FC = () => {
929
const [authState, authSend] = useAuth()
1030
const me = useMe()
1131
const permissions = usePermissions()
1232
const { updateProfileError } = authState.context
1333
const canEditUsers = permissions && permissions.updateUsers
34+
const [OIDCState, setOIDCState] = useState<OIDCState>({
35+
status: "closed",
36+
})
37+
const location = useLocation()
38+
const redirectTo = retrieveRedirect(location.search)
1439

1540
return (
16-
<Section title="Account" description="Update your account info">
17-
<AccountForm
18-
editable={Boolean(canEditUsers)}
19-
email={me.email}
20-
updateProfileError={updateProfileError}
21-
isLoading={authState.matches("signedIn.profile.updatingProfile")}
22-
initialValues={{
23-
username: me.username,
24-
}}
25-
onSubmit={(data) => {
26-
authSend({
27-
type: "UPDATE_PROFILE",
28-
data,
29-
})
30-
}}
41+
<>
42+
<Section title="Account" description="Update your account info">
43+
<AccountForm
44+
editable={Boolean(canEditUsers)}
45+
email={me.email}
46+
updateProfileError={updateProfileError}
47+
isLoading={authState.matches("signedIn.profile.updatingProfile")}
48+
initialValues={{
49+
username: me.username,
50+
}}
51+
onSubmit={(data) => {
52+
authSend({
53+
type: "UPDATE_PROFILE",
54+
data,
55+
})
56+
}}
57+
onChangeToOIDCAuth={() => {
58+
setOIDCState({ status: "confirmPassword" })
59+
}}
60+
/>
61+
</Section>
62+
<OIDCChangeModal
63+
redirectTo={redirectTo}
64+
state={OIDCState}
65+
onChangeState={setOIDCState}
3166
/>
32-
</Section>
67+
</>
68+
)
69+
}
70+
71+
const OIDCChangeModal = ({
72+
state,
73+
onChangeState,
74+
redirectTo,
75+
}: {
76+
redirectTo: string
77+
state: OIDCState
78+
onChangeState: (newState: OIDCState) => void
79+
}) => {
80+
const authMethods = MockAuthMethods
81+
82+
const updateProvider = (provider: string) => {
83+
onChangeState({ status: "updatingProvider" })
84+
setTimeout(() => {
85+
window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=something&redirect=${encodeURIComponent(
86+
redirectTo,
87+
)}`
88+
}, 1000)
89+
}
90+
91+
return (
92+
<Dialog
93+
open={state.status !== "closed"}
94+
onClose={() => onChangeState({ status: "closed" })}
95+
sx={{
96+
"& .MuiPaper-root": {
97+
padding: (theme) => theme.spacing(5),
98+
backgroundColor: (theme) => theme.palette.background.paper,
99+
border: (theme) => `1px solid ${theme.palette.divider}`,
100+
width: 440,
101+
},
102+
}}
103+
>
104+
{(state.status === "confirmPassword" ||
105+
state.status === "confirmingPassword") && (
106+
<div>
107+
<Typography component="h3" sx={{ fontSize: 20 }}>
108+
Confirm password
109+
</Typography>
110+
<Typography
111+
component="p"
112+
sx={{
113+
color: (theme) => theme.palette.text.secondary,
114+
mt: 1,
115+
mb: 3,
116+
}}
117+
>
118+
We need to confirm your identity in order to proceed with the
119+
authentication changes
120+
</Typography>
121+
<VerticalForm
122+
onSubmit={async (e) => {
123+
e.preventDefault()
124+
onChangeState({ status: "confirmingPassword" })
125+
await new Promise((resolve) => setTimeout(resolve, 1000))
126+
onChangeState({ status: "selectOIDCProvider" })
127+
}}
128+
>
129+
<FormFields>
130+
<TextField
131+
type="password"
132+
label="Password"
133+
name="password"
134+
autoFocus
135+
required
136+
/>
137+
<LoadingButton
138+
size="large"
139+
type="submit"
140+
variant="contained"
141+
loading={state.status === "confirmingPassword"}
142+
>
143+
Confirm password
144+
</LoadingButton>
145+
</FormFields>
146+
</VerticalForm>
147+
</div>
148+
)}
149+
150+
{(state.status === "selectOIDCProvider" ||
151+
state.status === "updatingProvider") && (
152+
<div>
153+
<Typography component="h3" sx={{ fontSize: 20 }}>
154+
Select a provider
155+
</Typography>
156+
<Typography
157+
component="p"
158+
sx={{
159+
color: (theme) => theme.palette.text.secondary,
160+
mt: 1,
161+
mb: 3,
162+
}}
163+
>
164+
After selecting the provider, you will be redirected to the
165+
provider&lsquo;s authentication page.
166+
</Typography>
167+
<Box display="grid" gap="16px">
168+
<Button
169+
disabled={state.status === "updatingProvider"}
170+
onClick={() => updateProvider("github")}
171+
startIcon={<GitHubIcon sx={{ width: 16, height: 16 }} />}
172+
fullWidth
173+
type="submit"
174+
size="large"
175+
>
176+
GitHub
177+
</Button>
178+
<Button
179+
disabled={state.status === "updatingProvider"}
180+
onClick={() => updateProvider("oidc")}
181+
size="large"
182+
startIcon={
183+
authMethods.oidc.iconUrl ? (
184+
<Box
185+
component="img"
186+
alt="Open ID Connect icon"
187+
src={authMethods.oidc.iconUrl}
188+
sx={{ width: 16, height: 16 }}
189+
/>
190+
) : (
191+
<KeyIcon sx={{ width: 16, height: 16 }} />
192+
)
193+
}
194+
fullWidth
195+
type="submit"
196+
>
197+
{authMethods.oidc.signInText || "OpenID Connect"}
198+
</Button>
199+
</Box>
200+
{state.status === "updatingProvider" && (
201+
<Box
202+
sx={{
203+
display: "flex",
204+
alignItems: "center",
205+
justifyContent: "center",
206+
mt: (theme) => theme.spacing(2),
207+
gap: 1,
208+
fontSize: 13,
209+
color: (theme) => theme.palette.text.secondary,
210+
}}
211+
>
212+
<CircularProgress size={12} />
213+
Updating authentication method...
214+
</Box>
215+
)}
216+
</div>
217+
)}
218+
</Dialog>
33219
)
34220
}
35221

0 commit comments

Comments
 (0)