Skip to content

feat(site): add change OIDC UI #8182

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
End
  • Loading branch information
BrunoQuaresma authored and Emyrk committed Jun 28, 2023
commit 251b41bbdb8ada03e7ee09f71c0a47ab3a3ba3d1
11 changes: 11 additions & 0 deletions site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getFormHelpers } from "../../utils/formUtils"
import { LoadingButton } from "../LoadingButton/LoadingButton"
import { ErrorAlert } from "components/Alert/ErrorAlert"
import { Form, FormFields } from "components/Form/Form"
import { Alert } from "components/Alert/Alert"

interface SecurityFormValues {
old_password: string
Expand Down Expand Up @@ -41,6 +42,7 @@ const validationSchema = Yup.object({
})

export interface SecurityFormProps {
disabled?: boolean
isLoading: boolean
initialValues: SecurityFormValues
onSubmit: (values: SecurityFormValues) => void
Expand All @@ -50,6 +52,7 @@ export interface SecurityFormProps {
}

export const SecurityForm: FC<SecurityFormProps> = ({
disabled,
isLoading,
onSubmit,
initialValues,
Expand All @@ -68,6 +71,14 @@ export const SecurityForm: FC<SecurityFormProps> = ({
updateSecurityError,
)

if (disabled) {
return (
<Alert severity="info">
Password changes are only allowed for password based accounts.
</Alert>
)
}

return (
<>
<Form onSubmit={form.handleSubmit}>
Expand Down
263 changes: 14 additions & 249 deletions site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -1,270 +1,35 @@
import { ComponentProps, FC, useState } from "react"
import { FC } from "react"
import { Section } from "../../../components/SettingsLayout/Section"
import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm"
import { useAuth } from "components/AuthProvider/AuthProvider"
import { useMe } from "hooks/useMe"
import { usePermissions } from "hooks/usePermissions"
import TextField from "@mui/material/TextField"
import Box from "@mui/material/Box"
import GitHubIcon from "@mui/icons-material/GitHub"
import KeyIcon from "@mui/icons-material/VpnKey"
import Button from "@mui/material/Button"
import { useLocation } from "react-router-dom"
import { retrieveRedirect } from "utils/redirect"
import Typography from "@mui/material/Typography"
import { convertToOAUTH, getAuthMethods } from "api/api"
import { AuthMethods, LoginType } from "api/typesGenerated"
import Skeleton from "@mui/material/Skeleton"
import { Stack } from "components/Stack/Stack"
import { useMutation, useQuery } from "@tanstack/react-query"
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { getErrorMessage } from "api/errors"

type LoginTypeConfirmation =
| {
open: false
selectedType: undefined
}
| {
open: true
selectedType: LoginType
}

export const AccountPage: FC = () => {
const [authState, authSend] = useAuth()
const me = useMe()
const permissions = usePermissions()
const { updateProfileError } = authState.context
const canEditUsers = permissions && permissions.updateUsers
const location = useLocation()
const redirectTo = retrieveRedirect(location.search)
const [loginTypeConfirmation, setLoginTypeConfirmation] =
useState<LoginTypeConfirmation>({ open: false, selectedType: undefined })
const { data: authMethods } = useQuery({
queryKey: ["authMethods"],
queryFn: getAuthMethods,
})
const loginTypeMutation = useMutation(convertToOAUTH, {
onSuccess: (data) => {
window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=${
data.state_string
}&redirect=${encodeURIComponent(redirectTo)}`
},
})

return (
<Stack spacing={8}>
<Section title="Account" description="Update your account info">
<AccountForm
editable={Boolean(canEditUsers)}
email={me.email}
updateProfileError={updateProfileError}
isLoading={authState.matches("signedIn.profile.updatingProfile")}
initialValues={{
username: me.username,
}}
onSubmit={(data) => {
authSend({
type: "UPDATE_PROFILE",
data,
})
}}
/>
</Section>

<Section
title="Single Sign On"
description="Authenticate in Coder using one-click"
>
<Box display="grid" gap="16px">
{authMethods ? (
authMethods.me_login_type === "password" ? (
<>
{authMethods.github.enabled && (
<GitHubButton
disabled={loginTypeMutation.isLoading}
onClick={() =>
setLoginTypeConfirmation({
open: true,
selectedType: "github",
})
}
>
GitHub
</GitHubButton>
)}
{authMethods.oidc.enabled && (
<OIDCButton
authMethods={authMethods}
disabled={loginTypeMutation.isLoading}
onClick={() =>
setLoginTypeConfirmation({
open: true,
selectedType: "oidc",
})
}
>
{getOIDCLabel(authMethods)}
</OIDCButton>
)}
</>
) : (
<>
{authMethods.me_login_type === "github" && (
<GitHubButton disabled>
Authenticated with GitHub
</GitHubButton>
)}

{authMethods.me_login_type === "oidc" && (
<OIDCButton authMethods={authMethods} disabled>
Authenticated with {getOIDCLabel(authMethods)}
</OIDCButton>
)}
</>
)
) : (
<>
<Skeleton
variant="rectangular"
sx={{ height: 40, borderRadius: 1 }}
/>
<Skeleton
variant="rectangular"
sx={{ height: 40, borderRadius: 1 }}
/>
</>
)}
</Box>
</Section>

<ConfirmLoginTypeChangeModal
open={loginTypeConfirmation.open}
error={loginTypeMutation.error}
// We still want to show it loading when it is success so the modal is
// not going to close or change until the oauth redirect
loading={loginTypeMutation.isLoading || loginTypeMutation.isSuccess}
onClose={() => {
setLoginTypeConfirmation({ open: false, selectedType: undefined })
loginTypeMutation.reset()
<Section title="Account" description="Update your account info">
<AccountForm
editable={Boolean(canEditUsers)}
email={me.email}
updateProfileError={updateProfileError}
isLoading={authState.matches("signedIn.profile.updatingProfile")}
initialValues={{
username: me.username,
}}
onConfirm={(password) => {
if (!loginTypeConfirmation.selectedType) {
throw new Error("No login type selected")
}
loginTypeMutation.mutate({
to_login_type: loginTypeConfirmation.selectedType,
email: me.email,
password,
onSubmit={(data) => {
authSend({
type: "UPDATE_PROFILE",
data,
})
}}
/>
</Stack>
)
}

const GitHubButton = (props: ComponentProps<typeof Button>) => {
return (
<Button
startIcon={<GitHubIcon sx={{ width: 16, height: 16 }} />}
fullWidth
type="submit"
size="large"
{...props}
/>
)
}

const OIDCButton = ({
authMethods,
...buttonProps
}: ComponentProps<typeof Button> & { authMethods: AuthMethods }) => {
return (
<Button
size="large"
startIcon={
authMethods.oidc.iconUrl ? (
<Box
component="img"
alt="Open ID Connect icon"
src={authMethods.oidc.iconUrl}
sx={{ width: 16, height: 16 }}
/>
) : (
<KeyIcon sx={{ width: 16, height: 16 }} />
)
}
fullWidth
type="submit"
{...buttonProps}
/>
)
}

const getOIDCLabel = (authMethods: AuthMethods) => {
return authMethods.oidc.signInText || "OpenID Connect"
}

const ConfirmLoginTypeChangeModal = ({
open,
loading,
error,
onClose,
onConfirm,
}: {
open: boolean
loading: boolean
error: unknown
onClose: () => void
onConfirm: (password: string) => void
}) => {
const [password, setPassword] = useState("")

const handleConfirm = () => {
onConfirm(password)
}

return (
<ConfirmDialog
open={open}
onClose={() => {
onClose()
}}
onConfirm={handleConfirm}
hideCancel={false}
cancelText="Cancel"
confirmText="Update"
title="Change login type"
confirmLoading={loading}
description={
<Stack>
<Typography>
After changing your login type, you will not be able to change it
again. Are you sure you want to proceed and change your login type?
</Typography>
<TextField
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter") {
handleConfirm()
}
}}
error={Boolean(error)}
helperText={
error
? getErrorMessage(error, "Your password is incorrect")
: undefined
}
name="confirm-password"
id="confirm-password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
label="Confirm your password"
type="password"
/>
</Stack>
}
/>
</Section>
)
}

Expand Down
55 changes: 38 additions & 17 deletions site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { FC } from "react"
import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService"
import { Section } from "../../../components/SettingsLayout/Section"
import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm"

export const Language = {
title: "Security",
}
import { useQuery } from "@tanstack/react-query"
import { getAuthMethods } from "api/api"
import {
SingleSignOnSection,
useSingleSignOnSection,
} from "./SingleSignOnSection"
import { Loader } from "components/Loader/Loader"
import { Stack } from "components/Stack/Stack"

export const SecurityPage: FC = () => {
const me = useMe()
Expand All @@ -20,21 +24,38 @@ export const SecurityPage: FC = () => {
},
)
const { error } = securityState.context
const { data: authMethods } = useQuery({
queryKey: ["authMethods"],
queryFn: getAuthMethods,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any error handling for this query?

})
const singleSignOnSection = useSingleSignOnSection()

if (!authMethods) {
return <Loader />
}

return (
<Section title={Language.title} description="Update your account password">
<SecurityForm
updateSecurityError={error}
isLoading={securityState.matches("updatingSecurity")}
initialValues={{ old_password: "", password: "", confirm_password: "" }}
onSubmit={(data) => {
securitySend({
type: "UPDATE_SECURITY",
data,
})
}}
/>
</Section>
<Stack spacing={6}>
<Section title="Security" description="Update your account password">
<SecurityForm
disabled={authMethods.me_login_type !== "password"}
updateSecurityError={error}
isLoading={securityState.matches("updatingSecurity")}
initialValues={{
old_password: "",
password: "",
confirm_password: "",
}}
onSubmit={(data) => {
securitySend({
type: "UPDATE_SECURITY",
data,
})
}}
/>
</Section>
<SingleSignOnSection authMethods={authMethods} {...singleSignOnSection} />
</Stack>
)
}

Expand Down
Loading