-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): add account page to change password (#1155)
Co-authored-by: tcsola <[email protected]> Co-authored-by: lby <[email protected]>
- Loading branch information
1 parent
6b598a7
commit c47bb83
Showing
14 changed files
with
706 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { useMeFetcher } from "@reearth/services/api"; | ||
import { useCallback } from "react"; | ||
|
||
export type UpdatePasswordType = { | ||
password: string; | ||
passwordConfirmation: string; | ||
}; | ||
|
||
export default () => { | ||
const { useMeQuery, useUpdatePassword, useDeleteUser } = useMeFetcher(); | ||
|
||
const passwordPolicy = window.REEARTH_CONFIG?.passwordPolicy; | ||
|
||
const { me: data } = useMeQuery(); | ||
|
||
const handleUpdateUserPassword = useCallback( | ||
async ({ password, passwordConfirmation }: UpdatePasswordType) => { | ||
try { | ||
await useUpdatePassword({ password, passwordConfirmation }); | ||
} catch (error) { | ||
console.error("Failed to update password:", error); | ||
} | ||
}, | ||
[useUpdatePassword] | ||
); | ||
|
||
const handleDeleteUser = useCallback(async () => { | ||
try { | ||
const userId = data.id; | ||
if (userId) await useDeleteUser({ userId }); | ||
} catch (error) { | ||
console.error("Failed to delete user:", error); | ||
} | ||
}, [data.id, useDeleteUser]); | ||
|
||
return { | ||
meData: data, | ||
passwordPolicy, | ||
handleUpdateUserPassword, | ||
handleDeleteUser | ||
}; | ||
}; |
132 changes: 132 additions & 0 deletions
132
web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { | ||
DEFAULT_SIDEBAR_WIDTH, | ||
SidebarMenuItem, | ||
SidebarSection, | ||
SidebarVersion, | ||
SidebarWrapper | ||
} from "@reearth/beta/ui/components/Sidebar"; | ||
import { useT } from "@reearth/services/i18n"; | ||
import { styled } from "@reearth/services/theme"; | ||
import { FC, useMemo } from "react"; | ||
|
||
import Navbar from "../Navbar"; | ||
|
||
import useHook from "./hooks"; | ||
import AccountSetting from "./innerPages/AccountSetting"; | ||
|
||
type Props = { | ||
sceneId?: string; | ||
projectId?: string; | ||
workspaceId?: string; | ||
tab: string; | ||
}; | ||
|
||
export const accountSettingTabs = [ | ||
{ id: "account", text: "Account", icon: "user" }, | ||
{ id: "workspace", text: "Workspace", icon: "users" }, | ||
{ id: "members", text: "Members", icon: "usersFour" } | ||
] as const; | ||
|
||
const AccountAndWorkSpaceSetting: FC<Props> = ({ tab }) => { | ||
const t = useT(); | ||
const tabs = useMemo( | ||
() => | ||
accountSettingTabs.map((tab) => ({ | ||
id: tab.id, | ||
icon: tab.icon, | ||
text: t(tab.text), | ||
path: `/settings/${tab.id}` | ||
})), | ||
[t] | ||
); | ||
const { meData, passwordPolicy, handleUpdateUserPassword } = useHook(); | ||
const { name, email } = meData; | ||
|
||
return ( | ||
<Wrapper> | ||
<Navbar workspaceId={meData.myTeam?.id} page="settings" /> | ||
<MainSection> | ||
<LeftSidePanel> | ||
<SidebarWrapper> | ||
<SidebarSection> | ||
{tabs?.map((t) => ( | ||
<SidebarMenuItem | ||
key={t.id} | ||
path={t.path} | ||
text={t.text} | ||
active={t.id === tab} | ||
icon={t.icon} | ||
/> | ||
))} | ||
</SidebarSection> | ||
<SidebarVersion /> | ||
</SidebarWrapper> | ||
</LeftSidePanel> | ||
<Content> | ||
{tab === "account" && ( | ||
<AccountSetting | ||
onUpdateUserPassword={handleUpdateUserPassword} | ||
passwordPolicy={passwordPolicy} | ||
informationData={{ name, email }} | ||
/> | ||
)} | ||
</Content> | ||
</MainSection> | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
const Wrapper = styled("div")(({ theme }) => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
height: "100%", | ||
width: "100%", | ||
color: theme.content.main, | ||
backgroundColor: theme.bg[0], | ||
["*"]: { | ||
boxSizing: "border-box" | ||
}, | ||
["* ::-webkit-scrollbar"]: { | ||
width: "8px" | ||
}, | ||
["* ::-webkit-scrollbar-track"]: { | ||
background: theme.relative.darker, | ||
borderRadius: theme.radius.large | ||
}, | ||
["* ::-webkit-scrollbar-thumb"]: { | ||
background: theme.relative.light, | ||
borderRadius: theme.radius.small | ||
}, | ||
["* ::-webkit-scrollbar-thumb:hover"]: { | ||
background: theme.relative.lighter | ||
} | ||
})); | ||
|
||
const MainSection = styled("div")(() => ({ | ||
display: "flex", | ||
flex: 1, | ||
overflow: "auto", | ||
position: "relative" | ||
})); | ||
|
||
const LeftSidePanel = styled("div")(({ theme }) => ({ | ||
width: DEFAULT_SIDEBAR_WIDTH, | ||
height: "100%", | ||
backgroundColor: theme.bg[1], | ||
display: "flex", | ||
padding: `${theme.spacing.large}px 0`, | ||
boxSizing: "border-box" | ||
})); | ||
|
||
const Content = styled("div")(({ theme }) => ({ | ||
position: "relative", | ||
display: "flex", | ||
flexDirection: "column", | ||
width: "100%", | ||
height: "100%", | ||
alignItems: "center", | ||
overflow: "auto", | ||
padding: `${theme.spacing.super}px` | ||
})); | ||
|
||
export default AccountAndWorkSpaceSetting; |
208 changes: 208 additions & 0 deletions
208
...eta/features/AccountAndWorkSpaceSetting/innerPages/AccountSetting/PasswordModal/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import { Flex } from "@aws-amplify/ui-react"; | ||
import { | ||
Modal, | ||
Button, | ||
ModalPanel, | ||
Typography, | ||
TextInput, | ||
Icon | ||
} from "@reearth/beta/lib/reearth-ui"; | ||
import { metricsSizes } from "@reearth/beta/utils/metrics"; | ||
import { useT } from "@reearth/services/i18n"; | ||
import { styled, useTheme } from "@reearth/services/theme"; | ||
import React, { useState, useCallback, useEffect } from "react"; | ||
|
||
export type PasswordPolicy = { | ||
tooShort?: RegExp; | ||
tooLong?: RegExp; | ||
whitespace?: RegExp; | ||
lowSecurity?: RegExp; | ||
medSecurity?: RegExp; | ||
highSecurity?: RegExp; | ||
}; | ||
|
||
type Props = { | ||
isVisible: boolean; | ||
onClose?: () => void; | ||
passwordPolicy?: PasswordPolicy; | ||
onPasswordUpdate?: ({ | ||
password, | ||
passwordConfirmation | ||
}: { | ||
password: string; | ||
passwordConfirmation: string; | ||
}) => void; | ||
}; | ||
|
||
const PasswordModal: React.FC<Props> = ({ | ||
isVisible, | ||
onClose, | ||
passwordPolicy, | ||
onPasswordUpdate | ||
}) => { | ||
const t = useT(); | ||
const theme = useTheme(); | ||
|
||
const [password, setPassword] = useState(""); | ||
const [regexMessage, setRegexMessage] = useState<string | undefined | null>(); | ||
const [regexMessageColor, setRegexMessageColor] = useState< | ||
string | undefined | ||
>(); | ||
const [passwordConfirmation, setPasswordConfirmation] = useState<string>(); | ||
const [disabled, setDisabled] = useState(true); | ||
|
||
const handlePasswordChange = useCallback( | ||
(password: string | undefined) => { | ||
setPassword(password ?? ""); | ||
switch (true) { | ||
case passwordPolicy?.whitespace?.test(password ?? ""): | ||
setRegexMessage(t("No whitespace is allowed.")); | ||
setRegexMessageColor(theme.warning.main); | ||
break; | ||
case passwordPolicy?.tooShort?.test(password ?? ""): | ||
setRegexMessage(t("Too short.")); | ||
setRegexMessageColor(theme.warning.main); | ||
break; | ||
case passwordPolicy?.tooLong?.test(password ?? ""): | ||
setRegexMessage(t("That is terribly long.")); | ||
setRegexMessageColor(theme.warning.main); | ||
break; | ||
case passwordPolicy?.highSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password is great!")); | ||
setRegexMessageColor(theme.primary.main); | ||
break; | ||
case passwordPolicy?.medSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password need more security.")); | ||
setRegexMessageColor(theme.warning.main); | ||
break; | ||
case passwordPolicy?.lowSecurity?.test(password ?? ""): | ||
setRegexMessage(t("That password is low security.")); | ||
setRegexMessageColor(theme.dangerous.main); | ||
break; | ||
default: | ||
setRegexMessage(t("That password confuses me, but might be okay.")); | ||
break; | ||
} | ||
}, | ||
[t, passwordPolicy, theme] | ||
); | ||
|
||
const handleClose = useCallback(() => { | ||
setPassword(""); | ||
setPasswordConfirmation(""); | ||
onClose?.(); | ||
}, [onClose]); | ||
|
||
const handleSave = useCallback(() => { | ||
if (password === passwordConfirmation) { | ||
onPasswordUpdate?.({ password, passwordConfirmation }); | ||
handleClose(); | ||
} | ||
}, [onPasswordUpdate, handleClose, password, passwordConfirmation]); | ||
|
||
useEffect(() => { | ||
if ( | ||
password !== passwordConfirmation || | ||
(passwordPolicy?.highSecurity && | ||
!passwordPolicy.highSecurity.test(password)) || | ||
passwordPolicy?.tooShort?.test(password) || | ||
passwordPolicy?.tooLong?.test(password) | ||
) { | ||
setDisabled(true); | ||
} else { | ||
setDisabled(false); | ||
} | ||
}, [password, passwordConfirmation, passwordPolicy]); | ||
|
||
const isMatchPassword = | ||
password !== passwordConfirmation && passwordConfirmation; | ||
|
||
return ( | ||
<Modal size="small" visible={isVisible}> | ||
<ModalPanel | ||
title={t("Change password")} | ||
onCancel={handleClose} | ||
actions={[ | ||
<Button | ||
key="Change password" | ||
title={t("Change password")} | ||
appearance="dangerous" | ||
disabled={disabled} | ||
onClick={handleSave} | ||
/> | ||
]} | ||
> | ||
<ModalContentWrapper> | ||
<Typography size="body"> | ||
{t( | ||
`In order to protect your account, make sure your password is unique and strong.` | ||
)} | ||
</Typography> | ||
<PasswordField direction="column"> | ||
<Typography size="body">{t("New password")}</Typography> | ||
<TextInput | ||
value={password} | ||
onChange={handlePasswordChange} | ||
type="password" | ||
/> | ||
{password ? ( | ||
<PasswordMessage size="body" color={regexMessageColor}> | ||
{regexMessage} | ||
</PasswordMessage> | ||
) : undefined} | ||
</PasswordField> | ||
<PasswordField direction="column"> | ||
<Typography size="body"> | ||
{t("New password (for confirmation)")} | ||
</Typography> | ||
<TextInput | ||
value={passwordConfirmation} | ||
onChange={setPasswordConfirmation} | ||
type="password" | ||
/> | ||
{isMatchPassword ? ( | ||
<PasswordMessage | ||
size="body" | ||
weight="regular" | ||
color={theme.dangerous.main} | ||
> | ||
<Icon | ||
icon="warning" | ||
size="large" | ||
color={theme.dangerous.main} | ||
/> | ||
{t('"repeatPassword" Passwords need to match')} | ||
</PasswordMessage> | ||
) : undefined} | ||
</PasswordField> | ||
</ModalContentWrapper> | ||
</ModalPanel> | ||
</Modal> | ||
); | ||
}; | ||
|
||
const ModalContentWrapper = styled("div")(({ theme }) => ({ | ||
display: "flex", | ||
flexDirection: "column", | ||
gap: theme.spacing.large, | ||
padding: theme.spacing.large, | ||
background: theme.bg[1] | ||
})); | ||
|
||
const PasswordField = styled(Flex)(({ theme }) => ({ | ||
height: "50px", | ||
transition: "all 0.2s", | ||
"& > *:first-child": { | ||
marginBottom: theme.spacing.small | ||
}, | ||
"&:has(p ~ p)": { | ||
height: "68px" | ||
} | ||
})); | ||
|
||
const PasswordMessage = styled(Typography)` | ||
margin-top: ${metricsSizes["s"]}px; | ||
display: flex; | ||
`; | ||
|
||
export default PasswordModal; |
Oops, something went wrong.