Skip to content
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

feat(web): add account page to change password #1155

Merged
merged 24 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
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
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" />
airslice marked this conversation as resolved.
Show resolved Hide resolved
<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,214 @@
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" weight="regular">
{t(
`In order to protect your account, make sure your password is unique and strong.`
)}
</Typography>
<PasswordField direction="column">
<Typography size="body" weight="bold">
{t("New password")}
</Typography>
<TextInput
value={password}
onChange={handlePasswordChange}
type="password"
/>
{password ? (
<PasswordMessage
size="body"
weight="bold"
color={regexMessageColor}
>
{regexMessage}
</PasswordMessage>
) : undefined}
</PasswordField>
<PasswordField direction="column">
<Typography size="body" weight="bold">
mkumbobeaty marked this conversation as resolved.
Show resolved Hide resolved
{t("New password (for confirmation)")}
</Typography>
<TextInput
value={passwordConfirmation}
onChange={setPasswordConfirmation}
type="password"
/>
{isMatchPassword ? (
<PasswordMessage
size="body"
weight="regular"
color={theme.dangerous.main}
>
<span>
mkumbobeaty marked this conversation as resolved.
Show resolved Hide resolved
<Icon
icon="warning"
size="large"
color={theme.dangerous.main}
/>
</span>
{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)`
height: 50px;
transition: all 0.2s;
&:has(p ~ p) {
height: 68px;
}
ZTongci marked this conversation as resolved.
Show resolved Hide resolved
`;

const PasswordMessage = styled(Typography)`
margin-top: ${metricsSizes["s"]}px;
font-style: italic;
display: flex;
`;

export default PasswordModal;
Loading
Loading