Skip to content

Commit

Permalink
feat(web): add account page to change password (#1155)
Browse files Browse the repository at this point in the history
Co-authored-by: tcsola <[email protected]>
Co-authored-by: lby <[email protected]>
  • Loading branch information
3 people authored and tomokazu tantaka committed Oct 2, 2024
1 parent 6b598a7 commit c47bb83
Show file tree
Hide file tree
Showing 14 changed files with 706 additions and 27 deletions.
42 changes: 42 additions & 0 deletions web/src/beta/features/AccountAndWorkSpaceSetting/hooks.ts
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 web/src/beta/features/AccountAndWorkSpaceSetting/index.tsx
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;
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;
Loading

0 comments on commit c47bb83

Please sign in to comment.