diff --git a/src/components/atoms/TextBox/index.tsx b/src/components/atoms/TextBox/index.tsx index 71f39f537..517fa80e8 100644 --- a/src/components/atoms/TextBox/index.tsx +++ b/src/components/atoms/TextBox/index.tsx @@ -103,44 +103,46 @@ const TextBox: React.FC = ({ }, [innerValue, onChange, throttle, throttleTimeout]); return ( - - {prefix && ( - - {prefix} - - )} - {multiline ? ( - - ) : ( - - )} - {suffix && ( - - {suffix} - - )} - +
+ + {prefix && ( + + {prefix} + + )} + {multiline ? ( + + ) : ( + + )} + {suffix && ( + + {suffix} + + )} + +
); }; diff --git a/src/components/molecules/Common/ProjectMenu/index.tsx b/src/components/molecules/Common/ProjectMenu/index.tsx index fa78219af..d1aca29aa 100644 --- a/src/components/molecules/Common/ProjectMenu/index.tsx +++ b/src/components/molecules/Common/ProjectMenu/index.tsx @@ -46,12 +46,6 @@ const ProjectMenu: React.FC = ({ currentProject, teamId }) => { text={intl.formatMessage({ defaultMessage: "Datasets" })} /> - {/* - - */} = ({ currentProject, teamId }) => { /> - - - window.open("http://docs.reearth.io", "_blank", "noopener")} text={intl.formatMessage({ defaultMessage: "Help" })} /> diff --git a/src/components/molecules/Dashboard/QuickStart.tsx b/src/components/molecules/Dashboard/QuickStart.tsx index f3c84478a..2ed5a66e0 100644 --- a/src/components/molecules/Dashboard/QuickStart.tsx +++ b/src/components/molecules/Dashboard/QuickStart.tsx @@ -48,7 +48,7 @@ const QuickStart: React.FC = ({ window.location.assign("http://docs.reearth.io")}> + onClick={() => window.open("http://docs.reearth.io")}> {intl.formatMessage({ defaultMessage: "User guide" })} diff --git a/src/components/molecules/Dashboard/Workspace.tsx b/src/components/molecules/Dashboard/Workspace.tsx index 2c46ebdfa..cb8adf7c3 100644 --- a/src/components/molecules/Dashboard/Workspace.tsx +++ b/src/components/molecules/Dashboard/Workspace.tsx @@ -71,6 +71,7 @@ const StyledLink = styled(Link)` padding: ${metricsSizes["2xs"]}px; border-radius: ${metricsSizes.xs}px; align-self: flex-end; + display: flex; &:hover { text-decoration: none; diff --git a/src/components/molecules/Settings/Account/AccountSection/index.tsx b/src/components/molecules/Settings/Account/AccountSection/index.tsx index 55656b9d6..d4fb28026 100644 --- a/src/components/molecules/Settings/Account/AccountSection/index.tsx +++ b/src/components/molecules/Settings/Account/AccountSection/index.tsx @@ -4,7 +4,9 @@ import { useIntl } from "react-intl"; import Flex from "@reearth/components/atoms/Flex"; import Icon from "@reearth/components/atoms/Icon"; import Text from "@reearth/components/atoms/Text"; -import PasswordModal from "@reearth/components/molecules/Settings/Account/PasswordModal"; +import PasswordModal, { + PasswordPolicy, +} from "@reearth/components/molecules/Settings/Account/PasswordModal"; import Field from "@reearth/components/molecules/Settings/Field"; import EditableItem from "@reearth/components/molecules/Settings/Project/EditableItem"; import Section from "@reearth/components/molecules/Settings/Section"; @@ -18,6 +20,7 @@ export type Props = { appTheme?: string; lang?: string; hasPassword: boolean; + passwordPolicy?: PasswordPolicy; updatePassword?: (password: string, passwordConfirmation: string) => void; updateLanguage?: (lang: string) => void; updateTheme?: (theme: string) => void; @@ -28,6 +31,7 @@ const ProfileSection: React.FC = ({ email, appTheme, hasPassword, + passwordPolicy, updatePassword, updateLanguage, updateTheme, @@ -111,6 +115,7 @@ const ProfileSection: React.FC = ({ setIsOpen(false)} diff --git a/src/components/molecules/Settings/Account/PasswordModal/index.tsx b/src/components/molecules/Settings/Account/PasswordModal/index.tsx index 38e616a38..1786f450f 100644 --- a/src/components/molecules/Settings/Account/PasswordModal/index.tsx +++ b/src/components/molecules/Settings/Account/PasswordModal/index.tsx @@ -2,10 +2,20 @@ import React, { useState, useCallback, useEffect } from "react"; import { useIntl } from "react-intl"; import Button from "@reearth/components/atoms/Button"; +import Flex from "@reearth/components/atoms/Flex"; import Modal from "@reearth/components/atoms/Modal"; import Text from "@reearth/components/atoms/Text"; import TextBox from "@reearth/components/atoms/TextBox"; -import { styled, useTheme } from "@reearth/theme"; +import { styled, useTheme, metricsSizes } from "@reearth/theme"; + +export type PasswordPolicy = { + tooShort?: RegExp; + tooLong?: RegExp; + whitespace?: RegExp; + lowSecurity?: RegExp; + medSecurity?: RegExp; + highSecurity?: RegExp; +}; type Props = { className?: string; @@ -22,24 +32,84 @@ type Props = { archiveProject?: (archived: boolean) => void; onClose?: () => void; hasPassword: boolean; + passwordPolicy?: PasswordPolicy; updatePassword?: (password: string, passwordConfirmation: string) => void; }; -const PasswordModal: React.FC = ({ isVisible, onClose, hasPassword, updatePassword }) => { +const PasswordModal: React.FC = ({ + isVisible, + onClose, + hasPassword, + passwordPolicy, + updatePassword, +}) => { const intl = useIntl(); const theme = useTheme(); const [password, setPassword] = useState(""); + const [regexMessage, setRegexMessage] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [disabled, setDisabled] = useState(true); + const handlePasswordChange = useCallback( + (password: string) => { + switch (true) { + case passwordPolicy?.whitespace?.test(password): + setPassword(password); + setRegexMessage( + intl.formatMessage({ + defaultMessage: "No whitespace is allowed.", + }), + ); + break; + case passwordPolicy?.tooShort?.test(password): + setPassword(password); + setRegexMessage( + intl.formatMessage({ + defaultMessage: "Too short.", + }), + ); + break; + case passwordPolicy?.tooLong?.test(password): + setPassword(password); + setRegexMessage( + intl.formatMessage({ + defaultMessage: "That is terribly long.", + }), + ); + break; + case passwordPolicy?.highSecurity?.test(password): + setPassword(password); + setRegexMessage(intl.formatMessage({ defaultMessage: "That password is great!" })); + break; + case passwordPolicy?.medSecurity?.test(password): + setPassword(password); + setRegexMessage(intl.formatMessage({ defaultMessage: "That password is better." })); + break; + case passwordPolicy?.lowSecurity?.test(password): + setPassword(password); + setRegexMessage(intl.formatMessage({ defaultMessage: "That password is okay." })); + break; + default: + setPassword(password); + setRegexMessage( + intl.formatMessage({ + defaultMessage: "That password confuses me, but might be okay.", + }), + ); + break; + } + }, + [intl, password], // eslint-disable-line react-hooks/exhaustive-deps + ); + const handleClose = useCallback(() => { setPassword(""); setPasswordConfirmation(""); onClose?.(); }, [onClose]); - const save = useCallback(() => { + const handleSave = useCallback(() => { if (password === passwordConfirmation) { updatePassword?.(password, passwordConfirmation); handleClose(); @@ -47,15 +117,20 @@ const PasswordModal: React.FC = ({ isVisible, onClose, hasPassword, updat }, [updatePassword, handleClose, password, passwordConfirmation]); useEffect(() => { - if (password !== passwordConfirmation || password === "" || passwordConfirmation === "") { + if ( + password !== passwordConfirmation || + passwordPolicy?.tooShort?.test(password) || + passwordPolicy?.tooLong?.test(password) + ) { setDisabled(true); } else { setDisabled(false); } - }, [password, passwordConfirmation]); + }, [password, passwordConfirmation]); // eslint-disable-line react-hooks/exhaustive-deps return ( = ({ isVisible, onClose, hasPassword, updat large disabled={disabled} buttonType="primary" - text={intl.formatMessage({ defaultMessage: "Change your password now" })} - onClick={save} + text={intl.formatMessage({ defaultMessage: "Change password" })} + onClick={handleSave} /> }> {hasPassword ? (
- -

+ + {intl.formatMessage({ - defaultMessage: `In order to protect your account, make sure your password:`, + defaultMessage: `In order to protect your account, make sure your password is unique and strong.`, })} -

- -
  • - {intl.formatMessage({ - defaultMessage: `Is Longer than 8 characters`, - })} -
  • -
  • - {intl.formatMessage({ - defaultMessage: `At least 2 different numbers`, - })} -
  • -
  • - {intl.formatMessage({ - defaultMessage: `Use lowercase and uppercase letters`, - })} -
  • -
    -
    - - - - + + + + {intl.formatMessage({ defaultMessage: "New password" })} + + {password ? regexMessage : undefined} + + + + {intl.formatMessage({ defaultMessage: "New password (for confirmation)" })} + + +
    ) : (
    - - {intl.formatMessage({ defaultMessage: "New password" })} + @@ -131,7 +196,7 @@ const PasswordModal: React.FC = ({ isVisible, onClose, hasPassword, updat disabled={disabled} buttonType="primary" text={intl.formatMessage({ defaultMessage: "Set your password now" })} - onClick={save} + onClick={handleSave} />
    )} @@ -139,16 +204,18 @@ const PasswordModal: React.FC = ({ isVisible, onClose, hasPassword, updat ); }; -const StyledTextBox = styled(TextBox)` - padding: 0; - margin: 20px -5px 40px; +const SubText = styled.div` + margin: ${({ theme }) => `${theme.metrics["xl"]}px auto`}; `; -const Label = styled(Text)` - margin: 20px auto; +const PasswordField = styled(Flex)` + height: 75px; `; -const StyledList = styled.ul` - margin: 20px auto; +const PasswordMessage = styled(Text)` + margin-left: ${metricsSizes.m}px; + margin-top: ${metricsSizes["2xs"]}px; + font-style: italic; `; + export default PasswordModal; diff --git a/src/components/molecules/Settings/WorkspaceList/WorkspaceCell/index.tsx b/src/components/molecules/Settings/WorkspaceList/WorkspaceCell/index.tsx index e71c743f7..148082975 100644 --- a/src/components/molecules/Settings/WorkspaceList/WorkspaceCell/index.tsx +++ b/src/components/molecules/Settings/WorkspaceList/WorkspaceCell/index.tsx @@ -36,7 +36,7 @@ const WorkspaceCell: React.FC = ({ className, team, personal, onSelect }) {intl.formatMessage({ defaultMessage: - "This is your personal workspace, your projects and resource will be managed in this workspace.", + "This is your personal workspace. Your projects and resources will be managed in this workspace.", })} ) : ( diff --git a/src/components/molecules/TopPage/index.tsx b/src/components/molecules/TopPage/index.tsx index d71b3115e..02fe84b8a 100644 --- a/src/components/molecules/TopPage/index.tsx +++ b/src/components/molecules/TopPage/index.tsx @@ -22,7 +22,7 @@ const TopPage: React.FC = ({ children, login }) => { large buttonType="secondary" onClick={login} - text={intl.formatMessage({ defaultMessage: "Sign in" })} + text={intl.formatMessage({ defaultMessage: "Log in" })} margin="40px" /> diff --git a/src/components/organisms/Settings/Account/hooks.ts b/src/components/organisms/Settings/Account/hooks.ts index 4f1a132ca..f4ebf752b 100644 --- a/src/components/organisms/Settings/Account/hooks.ts +++ b/src/components/organisms/Settings/Account/hooks.ts @@ -22,6 +22,7 @@ export default () => { const me = profileData?.me; const auths = profileData?.me?.auths; const hasPassword = auths?.includes("auth0") ?? false; + const passwordPolicy = window.REEARTH_CONFIG?.passwordPolicy; const [updateMeMutation] = useUpdateMeMutation(); @@ -89,6 +90,7 @@ export default () => { currentProject, me, hasPassword, + passwordPolicy, updateName, updatePassword, updateLanguage, diff --git a/src/components/organisms/Settings/Account/index.tsx b/src/components/organisms/Settings/Account/index.tsx index 653bdef64..1812e9147 100644 --- a/src/components/organisms/Settings/Account/index.tsx +++ b/src/components/organisms/Settings/Account/index.tsx @@ -17,6 +17,7 @@ const Account: React.FC = () => { currentProject, me, hasPassword, + passwordPolicy, updateName, updatePassword, updateLanguage, @@ -33,6 +34,7 @@ const Account: React.FC = () => { lang={me?.lang} appTheme={me?.theme ? me.theme.toUpperCase() : "DARK"} hasPassword={hasPassword} + passwordPolicy={passwordPolicy} updatePassword={updatePassword} updateLanguage={updateLanguage} updateTheme={updateTheme} diff --git a/src/components/pages/TopPage/index.tsx b/src/components/pages/TopPage/index.tsx index 0f0b21964..f8ae87b3f 100644 --- a/src/components/pages/TopPage/index.tsx +++ b/src/components/pages/TopPage/index.tsx @@ -2,7 +2,6 @@ import React from "react"; import Loading from "@reearth/components/atoms/Loading"; import MoleculeTopPage from "@reearth/components/molecules/TopPage"; -import NotificationBanner from "@reearth/components/organisms/Notification"; import useHooks from "./hooks"; @@ -13,13 +12,7 @@ export type Props = { const TopPage: React.FC = () => { const { isLoading, isAuthenticated, login } = useHooks(); - return isLoading ? ( - - ) : !isAuthenticated ? ( - - - - ) : null; + return isLoading ? : !isAuthenticated ? : null; }; export default TopPage; diff --git a/src/config.ts b/src/config.ts index b55d89671..f83172e4b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,8 +10,15 @@ export type Config = { googleClientId?: string; sentryDsn?: string; sentryEnv?: string; + passwordPolicy?: { + tooShort?: RegExp; + tooLong?: RegExp; + whitespace?: RegExp; + lowSecurity?: RegExp; + medSecurity?: RegExp; + highSecurity?: RegExp; + }; }; - declare global { interface Window { REEARTH_CONFIG?: Config; @@ -31,4 +38,17 @@ export default async function loadConfig() { ...defaultConfig, ...(await (await fetch("/reearth_config.json")).json()), }; + + if (!window.REEARTH_CONFIG?.passwordPolicy) return; + + window.REEARTH_CONFIG.passwordPolicy = Object.entries( + Object.values(window.REEARTH_CONFIG.passwordPolicy).map((k, v) => { + if (typeof v !== "string") return undefined; + try { + return [k, new RegExp(v)]; + } catch { + return undefined; + } + }), + ) as Config["passwordPolicy"]; } diff --git a/translations/en.yml b/translations/en.yml index 453520b49..f0ab52678 100644 --- a/translations/en.yml +++ b/translations/en.yml @@ -59,7 +59,6 @@ src: '3528672691': Cancel '4120989039': Create ProjectMenu: - '1936593459': Top page '2222696742': Help '2661706411': Datasets '3029638665': Project settings @@ -252,15 +251,21 @@ src: Light theme is still in beta. Some UI may still not be supported (ie. public projects will not use light theme). PasswordModal: + '264517228': That password is better. + '324893002': No whitespace is allowed. '539787951': Set your password now + '619506016': That password is okay. '740918511': New password (for confirmation) - '1000887516': 'In order to protect your account, make sure your password:' - '1068339636': Change your password now - '1515529938': Is Longer than 8 characters + '1234886189': >- + In order to protect your account, make sure your password is + unique and strong. '2039038750': New password '2521568990': Change Password - '3546326308': Use lowercase and uppercase letters - '4270021428': At least 2 different numbers + '3497016136': Too short. + '3627357388': That password is great! + '3907506757': 'That password confuses me, but might be okay.' + '4066780408': That is terribly long. + '4188749693': Change password ProfileSection: '636461959': Name '1534468327': Profile @@ -412,15 +417,15 @@ src: WorkspaceList: WorkspaceCell: '808766300': No Title Workspace - '974692228': >- - This is your personal workspace, your projects and resource will - be managed in this workspace. '3230245813': 'Members:' + '4106929473': >- + This is your personal workspace. Your projects and resources will + be managed in this workspace. WorkspaceList: '3435539653': All workspaces '3828443189': New Workspace TopPage: - '1405865627': Sign in + '1135777802': Log in Visualizer: Block: Text: diff --git a/translations/ja.yml b/translations/ja.yml index 25f497e95..7eb272724 100644 --- a/translations/ja.yml +++ b/translations/ja.yml @@ -51,7 +51,6 @@ src: '3528672691': キャンセル '4120989039': 作成 ProjectMenu: - '1936593459': トップ '2222696742': ヘルプ '2661706411': データセット '3029638665': プロジェクト設定 @@ -226,15 +225,19 @@ src: '3198092477': アカウント '3923994153': ライトテーマはまだベータ版です。サポートされていない機能があります。(例:ライトテーマで公開版プロジェクトは作成できません) PasswordModal: + '264517228': '' + '324893002': '' '539787951': 現在のパスワード + '619506016': '' '740918511': 新しいパスワード - '1000887516': 'アカウント保護のためパスワードを確認:' - '1068339636': パスワードを変更 - '1515529938': 8文字以上です + '1234886189': '' '2039038750': 新しいパスワード '2521568990': パスワードを変更 - '3546326308': 大文字小文字を使用できます - '4270021428': 最低2種類の数字が必要です + '3497016136': '' + '3627357388': '' + '3907506757': '' + '4066780408': '' + '4188749693': '' ProfileSection: '636461959': 名前 '1534468327': プロフィール @@ -366,13 +369,13 @@ src: WorkspaceList: WorkspaceCell: '808766300': 無題のワークスペース - '974692228': これはあなたのパーソナルワークスペースです。あなたのプロジェクトやリソースはこのワークスペースで管理されます。 '3230245813': メンバーズ + '4106929473': '' WorkspaceList: '3435539653': すべてのワークスペース '3828443189': 新規ワークスペース TopPage: - '1405865627': サインイン + '1135777802': '' Visualizer: Block: Text: