From 5fbc47b32930163e8fb1a240062f67fc2a51d869 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Thu, 10 Jul 2025 15:15:36 +0800 Subject: [PATCH 1/7] change email styling --- .../account-page/email-change-modal.tsx | 218 ++++++++++++++++++ web/app/account/account-page/index.module.css | 9 - web/app/account/account-page/index.tsx | 28 ++- web/i18n/en-US/common.ts | 22 ++ web/i18n/zh-Hans/common.ts | 22 ++ 5 files changed, 285 insertions(+), 14 deletions(-) create mode 100644 web/app/account/account-page/email-change-modal.tsx delete mode 100644 web/app/account/account-page/index.module.css diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx new file mode 100644 index 0000000000..c9b0108e23 --- /dev/null +++ b/web/app/account/account-page/email-change-modal.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import { noop } from 'lodash-es' + +type Props = { + show: boolean + onClose: () => void + email: string +} + +enum STEP { + start = 'start', + verifyOrigin = 'verifyOrigin', + newEmail = 'newEmail', + verifyNew = 'verifyNew', +} + +const EmailChangeModal = ({ onClose, email, show }: Props) => { + const { t } = useTranslation() + const [step, setStep] = useState(STEP.start) + const [code, setCode] = useState('') + const [mail, setMail] = useState('jin_zehong@qq.com') + const [time, setTime] = useState(0) + + const startCount = () => { + setTime(60) + const timer = setInterval(() => { + setTime((prev) => { + if (prev <= 0) { + clearInterval(timer) + return 0 + } + return prev - 1 + }) + }, 1000) + } + return ( + +
+ +
+ {step === STEP.start && ( + <> +
{t('common.account.changeEmail.title')}
+
+
{t('common.account.changeEmail.authTip')}
+
+ }} + values={{ email }} + /> +
+
+
+
+ + +
+ + )} + {step === STEP.verifyOrigin && ( + <> +
{t('common.account.changeEmail.verifyEmail')}
+
+
+ }} + values={{ email }} + /> +
+
+
+
{t('common.account.changeEmail.codeLabel')}
+ setCode(e.target.value)} + maxLength={6} + /> +
+
+ + +
+
+ {t('common.account.changeEmail.resendTip')} + {time > 0 && ( + {t('common.account.changeEmail.resendCount', { count: time })} + )} + {!time && ( + {t('common.account.changeEmail.resend')} + )} +
+ + )} + {step === STEP.newEmail && ( + <> +
{t('common.account.changeEmail.newEmail')}
+
+
{t('common.account.changeEmail.content3')}
+
+
+
{t('common.account.changeEmail.emailLabel')}
+ setMail(e.target.value)} + destructive + /> +
{t('common.account.changeEmail.existingEmail')}
+
+
+ + +
+ + )} + {step === STEP.verifyNew && ( + <> +
{t('common.account.changeEmail.verifyNew')}
+
+
+ }} + values={{ email: mail }} + /> +
+
+
+
{t('common.account.changeEmail.codeLabel')}
+ setCode(e.target.value)} + maxLength={6} + /> +
+
+ + +
+
+ {t('common.account.changeEmail.resendTip')} + {time > 0 && ( + {t('common.account.changeEmail.resendCount', { count: time })} + )} + {!time && ( + {t('common.account.changeEmail.resend')} + )} +
+ + )} +
+ ) +} + +export default EmailChangeModal diff --git a/web/app/account/account-page/index.module.css b/web/app/account/account-page/index.module.css deleted file mode 100644 index 949d1257e9..0000000000 --- a/web/app/account/account-page/index.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.modal { - padding: 24px 32px !important; - width: 400px !important; -} - -.bg { - background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; -} - diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 19c1e44236..1b4ebea78f 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -6,7 +6,6 @@ import { } from '@remixicon/react' import { useContext } from 'use-context-selector' import DeleteAccount from '../delete-account' -import s from './index.module.css' import AvatarWithEdit from './AvatarWithEdit' import Collapse from '@/app/components/header/account-setting/collapse' import type { IItem } from '@/app/components/header/account-setting/collapse' @@ -21,6 +20,7 @@ import { IS_CE_EDITION } from '@/config' import Input from '@/app/components/base/input' import PremiumBadge from '@/app/components/base/premium-badge' import { useGlobalPublicStore } from '@/context/global-public-context' +import EmailChangeModal from './email-change-modal' const titleClassName = ` system-sm-semibold text-text-secondary @@ -48,6 +48,7 @@ export default function AccountPage() { const [showCurrentPassword, setShowCurrentPassword] = useState(false) const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const [showUpdateEmail, setShowUpdateEmail] = useState(false) const handleEditName = () => { setEditNameModalVisible(true) @@ -123,10 +124,17 @@ export default function AccountPage() { } const renderAppItem = (item: IItem) => { + const { icon, icon_background, icon_type, icon_url } = item as any return (
- +
{item.name}
@@ -170,6 +178,9 @@ export default function AccountPage() {
{userProfile.email}
+
setShowUpdateEmail(true)}> + {t('common.operation.change')} +
{ @@ -190,7 +201,7 @@ export default function AccountPage() { {!!apps.length && ( ({ key: app.id, name: app.name }))} + items={apps.map(app => ({ ...app, key: app.id, name: app.name }))} renderItem={renderAppItem} wrapperClassName='mt-2' /> @@ -202,7 +213,7 @@ export default function AccountPage() { setEditNameModalVisible(false)} - className={s.modal} + className='!w-[420px] !p-6' >
{t('common.account.editName')}
{t('common.account.name')}
@@ -231,7 +242,7 @@ export default function AccountPage() { setEditPasswordModalVisible(false) resetPasswordForm() }} - className={s.modal} + className='!w-[420px] !p-6' >
{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}
{userProfile.is_password_set && ( @@ -316,6 +327,13 @@ export default function AccountPage() { /> ) } + {showUpdateEmail && ( + setShowUpdateEmail(false)} + email={userProfile.email} + /> + )} ) } diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 0823c6129c..1eb4aa2327 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -233,6 +233,28 @@ const translation = { editWorkspaceInfo: 'Edit Workspace Info', workspaceName: 'Workspace Name', workspaceIcon: 'Workspace Icon', + changeEmail: { + title: 'Change Email', + verifyEmail: 'Verify your current email', + newEmail: 'Set up a new email address', + verifyNew: 'Verify your new email', + authTip: 'Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.', + content1: 'If you continue, we\'ll send a verification code to {{email}} for re-authentication.', + content2: 'Your current email is {{email}} . Verification code has been sent to this email address.', + content3: 'Enter a new email and we will send you a verification code.', + content4: 'We just sent you a temporary verification code to {{email}}.', + codeLabel: 'Verification code', + codePlaceholder: 'Paste the 6-digit code', + emailLabel: 'New email', + emailPlaceholder: 'Enter new email', + existingEmail: 'A user with this email already exists.', + sendVerifyCode: 'Send verification code', + continue: 'Continue', + changeTo: 'Change to {{email}}', + resendTip: 'Didn\'t receive a code?', + resendCount: 'Resend in {{count}}s', + resend: 'Resend', + }, }, members: { team: 'Team', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 39964bb6b0..02d98e36d6 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -233,6 +233,28 @@ const translation = { editWorkspaceInfo: '编辑工作空间信息', workspaceName: '工作空间名称', workspaceIcon: '工作空间图标', + changeEmail: { + title: '更改邮箱', + verifyEmail: '验证当前邮箱', + newEmail: '设置新邮箱', + verifyNew: '验证新邮箱', + authTip: '一旦您的电子邮件地址更改,链接到您旧电子邮件地址的 Google 或 GitHub 帐户将无法再登录该帐户。', + content1: '如果您继续,我们将向 {{email}} 发送验证码以进行重新验证。', + content2: '你的当前邮箱是 {{email}} 。验证码已发送至该邮箱。', + content3: '输入新的电子邮件,我们将向您发送验证码。', + content4: '我们已将验证码发送至 {{email}} 。', + codeLabel: '验证码', + codePlaceholder: '输入 6 位数字验证码', + emailLabel: '新邮箱', + emailPlaceholder: '输入新邮箱', + existingEmail: '该邮箱已存在', + sendVerifyCode: '发送验证码', + continue: '继续', + changeTo: '更改为 {{email}}', + resendTip: '没有收到验证码?', + resendCount: '请在 {{count}} 秒后重新发送', + resend: '重新发送', + }, }, members: { team: '团队', From fe6e3ac41e987667b88ff8935beb99522ee5836e Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sat, 12 Jul 2025 09:28:33 +0800 Subject: [PATCH 2/7] add api of email change --- web/app/account/account-page/index.tsx | 5 +++++ web/service/common.ts | 12 ++++++++++++ web/types/feature.ts | 2 ++ 3 files changed, 19 insertions(+) diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 1b4ebea78f..b3106ec111 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -178,6 +178,11 @@ export default function AccountPage() {
{userProfile.email}
+ {systemFeatures.enable_change_email && ( +
setShowUpdateEmail(true)}> + {t('common.operation.change')} +
+ )}
setShowUpdateEmail(true)}> {t('common.operation.change')}
diff --git a/web/service/common.ts b/web/service/common.ts index 700cd4bf51..bdb8437da4 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -376,3 +376,15 @@ export const submitDeleteAccountFeedback = (body: { feedback: string; email: str export const getDocDownloadUrl = (doc_name: string) => get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true }) + +export const sendVerifyCode = (body: { email: string; phase: string; token?: string }) => + post('/account/change-email', { body }) + +export const verifyEmail = (body: { email: string; code: string; token: string }) => + post('/account/validity', { body }) + +export const resetEmail = (body: { new_email: string; token: string }) => + post('/account/change-email/reset', { body }) + +export const checkEmailExisted = (body: { email: string }) => + post('/account/change-email/check-email-unique', { body }) diff --git a/web/types/feature.ts b/web/types/feature.ts index 5787c2661f..088317d7fd 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -35,6 +35,7 @@ export type SystemFeatures = { sso_enforced_for_web: boolean sso_enforced_for_web_protocol: SSOProtocol | '' enable_marketplace: boolean + enable_change_email: boolean enable_email_code_login: boolean enable_email_password_login: boolean enable_social_oauth_login: boolean @@ -70,6 +71,7 @@ export const defaultSystemFeatures: SystemFeatures = { sso_enforced_for_web: false, sso_enforced_for_web_protocol: '', enable_marketplace: false, + enable_change_email: false, enable_email_code_login: false, enable_email_password_login: false, enable_social_oauth_login: false, From c6bf5d1864f8aaf97399c58e88d7d2dbcf15ee4c Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 14 Jul 2025 10:58:56 +0800 Subject: [PATCH 3/7] reset email --- .../account-page/email-change-modal.tsx | 172 +++++++++++++++++- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx index c9b0108e23..1ec37cdf58 100644 --- a/web/app/account/account-page/email-change-modal.tsx +++ b/web/app/account/account-page/email-change-modal.tsx @@ -1,9 +1,19 @@ import React, { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { useRouter } from 'next/navigation' +import { useContext } from 'use-context-selector' +import { ToastContext } from '@/app/components/base/toast' import { RiCloseLine } from '@remixicon/react' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' +import { + checkEmailExisted, + logout, + resetEmail, + sendVerifyCode, + verifyEmail, +} from '@/service/common' import { noop } from 'lodash-es' type Props = { @@ -21,10 +31,14 @@ enum STEP { const EmailChangeModal = ({ onClose, email, show }: Props) => { const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const router = useRouter() const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') const [mail, setMail] = useState('jin_zehong@qq.com') const [time, setTime] = useState(0) + const [stepToken, setStepToken] = useState('') + const [newEmailExited, setNewEmailExited] = useState(false) const startCount = () => { setTime(60) @@ -38,6 +52,146 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { }) }, 1000) } + + const sendEmail = async (email: string, isOrigin: boolean, token?: string) => { + try { + const res = await sendVerifyCode({ + email, + phase: isOrigin ? 'old_email' : 'new_email', + token, + }) + startCount() + if (res.data) + setStepToken(res.data) + } + catch (error) { + notify({ + type: 'error', + message: `Error sending verification code: ${error ? (error as any).message : ''}`, + }) + } + } + + const verifyEmailAddress = async (email: string, code: string, token: string, callback?: () => void) => { + try { + const res = await verifyEmail({ + email, + code, + token, + }) + if (res.is_valid) { + setStepToken(res.token) + callback?.() + } + else { + notify({ + type: 'error', + message: 'Verifying email failed', + }) + } + } + catch (error) { + notify({ + type: 'error', + message: `Error verifying email: ${error ? (error as any).message : ''}`, + }) + } + } + + const sendCodeToOriginEmail = async () => { + await sendEmail( + email, + true, + ) + setStep(STEP.verifyOrigin) + } + + const handleVerifyOriginEmail = async () => { + await verifyEmailAddress(email, code, stepToken, () => setStep(STEP.newEmail)) + setCode('') + } + + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return emailRegex.test(email) + } + + const checkNewEmailExisted = async (email: string) => { + try { + await checkEmailExisted({ + email, + }) + setNewEmailExited(false) + } + catch (error) { + setNewEmailExited(false) + if ((error as any)?.code === 'email_already_in_use') { + setNewEmailExited(true) + } + else { + notify({ + type: 'error', + message: `Error checking email existence: ${error ? (error as any).message : ''}`, + }) + } + } + } + + const handleNewEmailValueChange = (mailAddress: string) => { + setMail(mailAddress) + if (isValidEmail(mailAddress)) + checkNewEmailExisted(mailAddress) + } + + const sendCodeToNewEmail = async () => { + if (!isValidEmail(mail)) { + notify({ + type: 'error', + message: 'Invalid email format', + }) + return + } + await sendEmail( + mail, + false, + stepToken, + ) + setStep(STEP.verifyNew) + } + + const handleLogout = async () => { + await logout({ + url: '/logout', + params: {}, + }) + + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') + + router.push('/signin') + } + + const updateEmail = async () => { + try { + await resetEmail({ + new_email: mail, + token: stepToken, + }) + handleLogout() + } + catch (error) { + notify({ + type: 'error', + message: `Error changing email: ${error ? (error as any).message : ''}`, + }) + } + } + + const submitNewEmail = async () => { + await verifyEmailAddress(email, code, stepToken, () => updateEmail()) + } + return ( { @@ -105,7 +259,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { disabled={code.length !== 6} className='!w-full' variant='primary' - onClick={() => setStep(STEP.newEmail)} + onClick={handleVerifyOriginEmail} > {t('common.account.changeEmail.continue')} @@ -122,7 +276,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { {t('common.account.changeEmail.resendCount', { count: time })} )} {!time && ( - {t('common.account.changeEmail.resend')} + {t('common.account.changeEmail.resend')} )} @@ -139,17 +293,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { className='!w-full' placeholder={t('common.account.changeEmail.emailPlaceholder')} value={mail} - onChange={e => setMail(e.target.value)} + onChange={e => handleNewEmailValueChange(e.target.value)} destructive /> -
{t('common.account.changeEmail.existingEmail')}
+ {newEmailExited && ( +
{t('common.account.changeEmail.existingEmail')}
+ )}
@@ -189,7 +345,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { disabled={code.length !== 6} className='!w-full' variant='primary' - onClick={onClose} + onClick={submitNewEmail} > {t('common.account.changeEmail.changeTo', { email: mail })} @@ -206,7 +362,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { {t('common.account.changeEmail.resendCount', { count: time })} )} {!time && ( - {t('common.account.changeEmail.resend')} + {t('common.account.changeEmail.resend')} )}
From 5afadb1540a23b1367b8d66626758bd5c19a8f74 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 14 Jul 2025 11:00:13 +0800 Subject: [PATCH 4/7] add judgement of system feature --- web/app/account/account-page/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index b3106ec111..a469286900 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -183,9 +183,6 @@ export default function AccountPage() { {t('common.operation.change')} )} -
setShowUpdateEmail(true)}> - {t('common.operation.change')} -
{ From 22232fd91d1ffaa6a45ba2fce07146df61c8a79f Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 14 Jul 2025 13:05:18 +0800 Subject: [PATCH 5/7] fix: api address of verify email --- web/service/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/service/common.ts b/web/service/common.ts index bdb8437da4..35f2bd29e2 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -381,7 +381,7 @@ export const sendVerifyCode = (body: { email: string; phase: string; token?: str post('/account/change-email', { body }) export const verifyEmail = (body: { email: string; code: string; token: string }) => - post('/account/validity', { body }) + post('/account/change-email/validity', { body }) export const resetEmail = (body: { new_email: string; token: string }) => post('/account/change-email/reset', { body }) From eec04ac5bd6d77f454fef8ab48740b2ebadba58a Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 14 Jul 2025 14:07:29 +0800 Subject: [PATCH 6/7] fix style of new email input --- web/app/account/account-page/email-change-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx index 1ec37cdf58..22620c9a6a 100644 --- a/web/app/account/account-page/email-change-modal.tsx +++ b/web/app/account/account-page/email-change-modal.tsx @@ -35,7 +35,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { const router = useRouter() const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') - const [mail, setMail] = useState('jin_zehong@qq.com') + const [mail, setMail] = useState('') const [time, setTime] = useState(0) const [stepToken, setStepToken] = useState('') const [newEmailExited, setNewEmailExited] = useState(false) @@ -294,7 +294,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { placeholder={t('common.account.changeEmail.emailPlaceholder')} value={mail} onChange={e => handleNewEmailValueChange(e.target.value)} - destructive + destructive={newEmailExited} /> {newEmailExited && (
{t('common.account.changeEmail.existingEmail')}
From 4ccfd15dfff35874a7b4eadcb8da8eb33f5c459f Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 14 Jul 2025 16:21:08 +0800 Subject: [PATCH 7/7] fix: new email value --- web/app/account/account-page/email-change-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx index 22620c9a6a..8d26ca66f3 100644 --- a/web/app/account/account-page/email-change-modal.tsx +++ b/web/app/account/account-page/email-change-modal.tsx @@ -189,7 +189,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { } const submitNewEmail = async () => { - await verifyEmailAddress(email, code, stepToken, () => updateEmail()) + await verifyEmailAddress(mail, code, stepToken, () => updateEmail()) } return (