Commit 005e866a authored by JzoNg's avatar JzoNg

feat: reset password

parent ddf35c06
.logo-icon { .logo-icon {
background: url(../assets/logo-icon.png) center center no-repeat; background: url(../assets/logo-icon.png) center center no-repeat;
background-size: contain; background-size: 32px;
box-shadow: 0px 4px 6px -1px rgba(0, 0, 0, 0.05), 0px 2px 4px -2px rgba(0, 0, 0, 0.05); box-shadow: 0px 4px 6px -1px rgba(0, 0, 0, 0.05), 0px 2px 4px -2px rgba(0, 0, 0, 0.05);
} }
......
...@@ -34,7 +34,7 @@ export default function AccountAbout({ ...@@ -34,7 +34,7 @@ export default function AccountAbout({
<div> <div>
<div className={classNames( <div className={classNames(
s['logo-icon'], s['logo-icon'],
'mx-auto mb-3 w-12 h-12 bg-white rounded border border-gray-200', 'mx-auto mb-3 w-12 h-12 bg-white rounded-xl border border-gray-200',
)} /> )} />
<div className={classNames( <div className={classNames(
s['logo-text'], s['logo-text'],
......
...@@ -25,13 +25,19 @@ const inputClassName = ` ...@@ -25,13 +25,19 @@ const inputClassName = `
text-sm font-normal text-gray-800 text-sm font-normal text-gray-800
` `
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() { export default function AccountPage() {
const { t } = useTranslation()
const { mutateUserProfile, userProfile, apps } = useAppContext() const { mutateUserProfile, userProfile, apps } = useAppContext()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false) const [editNameModalVisible, setEditNameModalVisible] = useState(false)
const [editName, setEditName] = useState('') const [editName, setEditName] = useState('')
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const { t } = useTranslation() const [editPasswordModalVisible, setEditPasswordModalVisible] = useState(false)
const [currentPassword, setCurrentPassword] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const handleEditName = () => { const handleEditName = () => {
setEditNameModalVisible(true) setEditNameModalVisible(true)
...@@ -52,6 +58,56 @@ export default function AccountPage() { ...@@ -52,6 +58,56 @@ export default function AccountPage() {
setEditing(false) setEditing(false)
} }
} }
const showErrorMessage = (message: string) => {
notify({
type: 'error',
message,
})
}
const valid = () => {
if (!password.trim()) {
showErrorMessage(t('login.error.passwordEmpty'))
return false
}
if (!validPassword.test(password))
showErrorMessage(t('login.error.passwordInvalid'))
if (password !== confirmPassword)
showErrorMessage(t('common.account.notEqual'))
return true
}
const resetPasswordForm = () => {
setCurrentPassword('')
setPassword('')
setConfirmPassword('')
}
const handleSavePassowrd = async () => {
if (!valid())
return
try {
setEditing(true)
await updateUserProfile({
url: 'account/password',
body: {
password: currentPassword,
new_password: password,
repeat_new_password: confirmPassword,
},
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutateUserProfile()
setEditPasswordModalVisible(false)
resetPasswordForm()
setEditing(false)
}
catch (e) {
notify({ type: 'error', message: (e as Error).message })
setEditPasswordModalVisible(false)
setEditing(false)
}
}
const renderAppItem = (item: IItem) => { const renderAppItem = (item: IItem) => {
return ( return (
<div className='flex px-3 py-1'> <div className='flex px-3 py-1'>
...@@ -80,51 +136,105 @@ export default function AccountPage() { ...@@ -80,51 +136,105 @@ export default function AccountPage() {
<div className={titleClassName}>{t('common.account.email')}</div> <div className={titleClassName}>{t('common.account.email')}</div>
<div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div> <div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div>
</div> </div>
{ <div className='mb-8'>
!!apps.length && ( <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div>
<> <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div>
<div className='mb-6 border-[0.5px] border-gray-100' /> <Button className='font-medium !text-gray-700 !px-3 !py-[7px] !text-[13px]' onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
<div className='mb-8'> </div>
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div> {!!apps.length && (
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div> <>
<Collapse <div className='mb-6 border-[0.5px] border-gray-100' />
title={`${t('common.account.showAppLength', { length: apps.length })}`} <div className='mb-8'>
items={apps.map(app => ({ key: app.id, name: app.name }))} <div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
renderItem={renderAppItem} <div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
wrapperClassName='mt-2' <Collapse
/> title={`${t('common.account.showAppLength', { length: apps.length })}`}
</div> items={apps.map(app => ({ key: app.id, name: app.name }))}
</> renderItem={renderAppItem}
) wrapperClassName='mt-2'
}
{
editNameModalVisible && (
<Modal
isShow
onClose={() => setEditNameModalVisible(false)}
className={s.modal}
>
<div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div>
<div className={titleClassName}>{t('common.account.name')}</div>
<input
className={inputClassName}
value={editName}
onChange={e => setEditName(e.target.value)}
/> />
<div className='flex justify-end mt-10'> </div>
<Button className='mr-2 text-sm font-medium' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button> </>
<Button )}
disabled={editing || !editName} {editNameModalVisible && (
type='primary' <Modal
className='text-sm font-medium' isShow
onClick={handleSaveName} onClose={() => setEditNameModalVisible(false)}
> className={s.modal}
{t('common.operation.save')} >
</Button> <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div>
</div> <div className={titleClassName}>{t('common.account.name')}</div>
</Modal> <input
) className={inputClassName}
} value={editName}
onChange={e => setEditName(e.target.value)}
/>
<div className='flex justify-end mt-10'>
<Button className='mr-2 text-sm font-medium' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
<Button
disabled={editing || !editName}
type='primary'
className='text-sm font-medium'
onClick={handleSaveName}
>
{t('common.operation.save')}
</Button>
</div>
</Modal>
)}
{editPasswordModalVisible && (
<Modal
isShow
onClose={() => {
setEditPasswordModalVisible(false)
resetPasswordForm()
}}
className={s.modal}
>
<div className='mb-6 text-lg font-medium text-gray-900'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
<input
type="password"
className={inputClassName}
value={currentPassword}
onChange={e => setCurrentPassword(e.target.value)}
/>
</>
)}
<div className='mt-8 text-sm font-medium text-gray-900'>
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
</div>
<input
type="password"
className={inputClassName}
value={password}
onChange={e => setPassword(e.target.value)}
/>
<div className='mt-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div>
<input
type="password"
className={inputClassName}
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
/>
<div className='flex justify-end mt-10'>
<Button className='mr-2 text-sm font-medium' onClick={() => {
setEditPasswordModalVisible(false)
resetPasswordForm()
}}>{t('common.operation.cancel')}</Button>
<Button
disabled={editing}
type='primary'
className='text-sm font-medium'
onClick={handleSavePassowrd}
>
{userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
</Button>
</div>
</Modal>
)}
</> </>
) )
} }
'use client' 'use client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useState } from 'react' import { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import { AtSymbolIcon, CubeTransparentIcon, GlobeAltIcon, UserIcon, UsersIcon, XMarkIcon } from '@heroicons/react/24/outline' import { AtSymbolIcon, CubeTransparentIcon, GlobeAltIcon, UserIcon, UsersIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { GlobeAltIcon as GlobalAltIconSolid, UserIcon as UserIconSolid, UsersIcon as UsersIconSolid } from '@heroicons/react/24/solid' import { GlobeAltIcon as GlobalAltIconSolid, UserIcon as UserIconSolid, UsersIcon as UsersIconSolid } from '@heroicons/react/24/solid'
import AccountPage from './account-page' import AccountPage from './account-page'
...@@ -18,6 +19,10 @@ const iconClassName = ` ...@@ -18,6 +19,10 @@ const iconClassName = `
w-4 h-4 ml-3 mr-2 w-4 h-4 ml-3 mr-2
` `
const scrolledClassName = `
border-b shadow-xs bg-white/[.98]
`
type IAccountSettingProps = { type IAccountSettingProps = {
onCancel: () => void onCancel: () => void
activeTab?: string activeTab?: string
...@@ -78,6 +83,22 @@ export default function AccountSetting({ ...@@ -78,6 +83,22 @@ export default function AccountSetting({
], ],
}, },
] ]
const scrollRef = useRef<HTMLDivElement>(null)
const [scrolled, setScrolled] = useState(false)
const scrollHandle = (e: any) => {
if (e.target.scrollTop > 0)
setScrolled(true)
else
setScrolled(false)
}
useEffect(() => {
const targetElement = scrollRef.current
targetElement?.addEventListener('scroll', scrollHandle)
return () => {
targetElement?.removeEventListener('scroll', scrollHandle)
}
}, [])
return ( return (
<Modal <Modal
...@@ -115,29 +136,19 @@ export default function AccountSetting({ ...@@ -115,29 +136,19 @@ export default function AccountSetting({
} }
</div> </div>
</div> </div>
<div className='w-[520px] h-[580px] px-6 py-4 overflow-y-auto'> <div ref={scrollRef} className='relative w-[520px] h-[580px] pb-4 overflow-y-auto'>
<div className='flex items-center justify-between h-6 mb-8 text-base font-medium text-gray-900 '> <div className={cn('sticky top-0 px-6 py-4 flex items-center justify-between h-14 mb-4 bg-white text-base font-medium text-gray-900', scrolled && scrolledClassName)}>
{[...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)?.name} {[...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)?.name}
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} /> <XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} />
</div> </div>
{ <div className='px-6'>
activeMenu === 'account' && <AccountPage /> {activeMenu === 'account' && <AccountPage />}
} {activeMenu === 'members' && <MembersPage />}
{ {activeMenu === 'integrations' && <IntegrationsPage />}
activeMenu === 'members' && <MembersPage /> {activeMenu === 'language' && <LanguagePage />}
} {activeMenu === 'provider' && <ProviderPage />}
{ {activeMenu === 'data-source' && <DataSourcePage />}
activeMenu === 'integrations' && <IntegrationsPage /> </div>
}
{
activeMenu === 'language' && <LanguagePage />
}
{
activeMenu === 'provider' && <ProviderPage />
}
{
activeMenu === 'data-source' && <DataSourcePage />
}
</div> </div>
</div> </div>
</Modal> </Modal>
......
'use client' 'use client'
import { createContext, useContext, useContextSelector } from 'use-context-selector' import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { FC, PropsWithChildren } from 'react'
import { createRef } from 'react'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import type { UserProfileResponse } from '@/models/common' import type { UserProfileResponse } from '@/models/common'
import { createRef, FC, PropsWithChildren } from 'react'
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export const useSelector = <T extends any>(selector: (value: AppContextValue) => T): T => export const useSelector = <T extends any>(selector: (value: AppContextValue) => T): T =>
useContextSelector(AppContext, selector); // eslint-disable-next-line @typescript-eslint/no-use-before-define
useContextSelector(AppContext, selector)
export type AppContextValue = { export type AppContextValue = {
apps: App[] apps: App[]
mutateApps: () => void mutateApps: () => void
userProfile: UserProfileResponse userProfile: UserProfileResponse
mutateUserProfile: () => void mutateUserProfile: () => void
pageContainerRef: React.RefObject<HTMLDivElement>, pageContainerRef: React.RefObject<HTMLDivElement>
useSelector: typeof useSelector, useSelector: typeof useSelector
} }
const AppContext = createContext<AppContextValue>({ const AppContext = createContext<AppContextValue>({
...@@ -24,6 +27,8 @@ const AppContext = createContext<AppContextValue>({ ...@@ -24,6 +27,8 @@ const AppContext = createContext<AppContextValue>({
id: '', id: '',
name: '', name: '',
email: '', email: '',
avatar: '',
is_password_set: false,
}, },
mutateUserProfile: () => { }, mutateUserProfile: () => { },
pageContainerRef: createRef(), pageContainerRef: createRef(),
......
...@@ -14,6 +14,7 @@ const translation = { ...@@ -14,6 +14,7 @@ const translation = {
edit: 'Edit', edit: 'Edit',
add: 'Add', add: 'Add',
refresh: 'Restart', refresh: 'Restart',
reset: 'Reset',
search: 'Search', search: 'Search',
change: 'Change', change: 'Change',
remove: 'Remove', remove: 'Remove',
...@@ -93,6 +94,14 @@ const translation = { ...@@ -93,6 +94,14 @@ const translation = {
avatar: 'Avatar', avatar: 'Avatar',
name: 'Name', name: 'Name',
email: 'Email', email: 'Email',
password: 'Password',
passwordTip: 'You can set a permanent password if you don’t want to use temporary login codes',
setPassword: 'Set a password',
resetPassword: 'Reset password',
currentPassword: 'Current password',
newPassword: 'New password',
confirmPassword: 'Confirm password',
notEqual: 'Two passwords are different.',
langGeniusAccount: 'Dify account', langGeniusAccount: 'Dify account',
langGeniusAccountTip: 'Your Dify account and associated user data.', langGeniusAccountTip: 'Your Dify account and associated user data.',
editName: 'Edit Name', editName: 'Edit Name',
......
...@@ -14,6 +14,7 @@ const translation = { ...@@ -14,6 +14,7 @@ const translation = {
edit: '编辑', edit: '编辑',
add: '添加', add: '添加',
refresh: '重新开始', refresh: '重新开始',
reset: '重置',
search: '搜索', search: '搜索',
change: '更改', change: '更改',
remove: '移除', remove: '移除',
...@@ -93,7 +94,14 @@ const translation = { ...@@ -93,7 +94,14 @@ const translation = {
avatar: '头像', avatar: '头像',
name: '用户名', name: '用户名',
email: '邮箱', email: '邮箱',
edit: '编辑', password: '密码',
passwordTip: '如果您不想使用验证码登录,可以设置永久密码',
setPassword: '设置密码',
resetPassword: '重置密码',
currentPassword: '原密码',
newPassword: '新密码',
notEqual: '两个密码不相同',
confirmPassword: '确认密码',
langGeniusAccount: 'Dify 账号', langGeniusAccount: 'Dify 账号',
langGeniusAccountTip: '您的 Dify 账号和相关的用户数据。', langGeniusAccountTip: '您的 Dify 账号和相关的用户数据。',
editName: '编辑名字', editName: '编辑名字',
......
...@@ -10,6 +10,8 @@ export type UserProfileResponse = { ...@@ -10,6 +10,8 @@ export type UserProfileResponse = {
id: string id: string
name: string name: string
email: string email: string
avatar: string
is_password_set: boolean
interface_language?: string interface_language?: string
interface_theme?: string interface_theme?: string
timezone?: string timezone?: string
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment