Password recovery (#2568)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-01-31 15:10:44 +06:00 committed by GitHub
parent 35985ce8d6
commit 3f755682cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 415 additions and 63 deletions

6
.vscode/launch.json vendored
View File

@ -48,10 +48,12 @@
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27018",
"MONGO_URL": "mongodb://localhost:27017",
"SERVER_SECRET": "secret",
"TRANSACTOR_URL": "ws:/localhost:3333",
"ACCOUNT_PORT": "3000"
"ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080",
"SES_URL": "http://localhost:8091"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,

View File

@ -319,6 +319,7 @@ specifiers:
mini-css-extract-plugin: ^2.2.0
minio: ^7.0.26
mongodb: ^4.11.0
node-fetch: ^2.6.6
p-queue: ~7.3.0
pdfkit: ~0.13.0
postcss: ^8.4.20
@ -686,6 +687,7 @@ dependencies:
mini-css-extract-plugin: 2.6.1_webpack@5.75.0
minio: 7.0.32
mongodb: 4.11.0
node-fetch: 2.6.7
p-queue: 7.3.0
pdfkit: 0.13.0
postcss: 8.4.20
@ -3427,6 +3429,13 @@ packages:
'@types/node': 16.11.68
dev: false
/@types/node-fetch/2.6.2:
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
dependencies:
'@types/node': 16.11.68
form-data: 3.0.1
dev: false
/@types/node/12.20.24:
resolution: {integrity: sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==}
dev: false
@ -11462,13 +11471,14 @@ packages:
dev: false
file:projects/account.tgz:
resolution: {integrity: sha512-1O91BX+tpcoZjaOTfyy1CvFF8Bea7RUvkQShNBsMgMthz1S4C82Q5htsCJDYSdm7TC3qDORBJGG4VBVN/Pwcrw==, tarball: file:projects/account.tgz}
resolution: {integrity: sha512-aJKpRhl1z/VBWuJpIUi71qe6EicrFQjmyQ7wGSDBM/b1M6Sm9wKSTBOjjhknIHNwno2toNAEmyV0nkwZcit9hQ==, tarball: file:projects/account.tgz}
name: '@rush-temp/account'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@types/heft-jest': 1.0.3
'@types/minio': 7.0.14
'@types/node-fetch': 2.6.2
'@types/ws': 8.5.3
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
@ -11479,12 +11489,14 @@ packages:
eslint-plugin-promise: 6.1.1_eslint@8.27.0
minio: 7.0.32
mongodb: 4.11.0
node-fetch: 2.6.7
prettier: 2.7.1
typescript: 4.8.4
ws: 8.11.0
transitivePeerDependencies:
- aws-crt
- bufferutil
- encoding
- supports-color
- utf-8-validate
dev: false
@ -13477,7 +13489,7 @@ packages:
dev: false
file:projects/model-recruit.tgz_typescript@4.8.4:
resolution: {integrity: sha512-d2S1FnV9nadIWl5JBjdxs7lZo109fWavsI6SVq/lc6enCzQWas5m/ZVu7dF9HvT9OaD+6HHnqx3pQ/oqTl3fSw==, tarball: file:projects/model-recruit.tgz}
resolution: {integrity: sha512-KqP/U4mkxWwAYH7pD0Z//Cv+/CXMxRrwdnlWRoJOr3nKGFshVb64dRAdkgVdRYNG2rcIlNeaFrBKuqW+6rdjBw==, tarball: file:projects/model-recruit.tgz}
id: file:projects/model-recruit.tgz
name: '@rush-temp/model-recruit'
version: 0.0.0

View File

@ -58,6 +58,8 @@ services:
- MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
- FRONT_URL=http://localhost:8087
- SES_URL=http://localhost:8091
restart: unless-stopped
collaborator:
image: hardcoreeng/collaborator

View File

@ -29,6 +29,11 @@
"NotSeeingWorkspace": "Not seeing your workspace?",
"WorkspaceNameRule": "The workspace name can contains lowercase letters, numbers, and symbols !@#%&^-",
"LinkValidHours": "Link valid (hours):",
"GetLink": "Get invite link"
"GetLink": "Get invite link",
"ForgotPassword": "Forgot your password",
"KnowPassword": "Know your password?",
"Recover": "Recover",
"PasswordRecovery": "Password recovery",
"RecoveryLinkSent": "Password recovery link sent to email"
}
}

View File

@ -29,6 +29,11 @@
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символов !@#%&^-",
"LinkValidHours": "Ссылка действительна (часов):",
"GetLink": "Получить ссылку"
"GetLink": "Получить ссылку",
"ForgotPassword": "Забыли пароль?",
"KnowPassword": "Знаете пароль?",
"Recover": "Восстановить",
"PasswordRecovery": "Восстановление пароля",
"RecoveryLinkSent": "Ссылка для восстановления пароля отправлена на почту"
}
}

View File

@ -73,12 +73,16 @@
{fields}
{object}
{action}
bottomCaption={login.string.HaveWorkspace}
bottomActionLabel={login.string.SelectWorkspace}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}}
bottomActions={[
{
caption: login.string.HaveWorkspace,
i18n: login.string.SelectWorkspace,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}
}
]}
/>

View File

@ -38,15 +38,19 @@
func: () => Promise<void>
}
interface BottomAction {
i18n: IntlString
func: () => void
caption: IntlString
}
export let caption: IntlString
export let status: Status
export let fields: Field[]
export let action: Action
export let secondaryButtonLabel: IntlString | undefined = undefined
export let secondaryButtonAction: (() => void) | undefined = undefined
export let bottomCaption: IntlString | undefined = undefined
export let bottomActionLabel: IntlString | undefined = undefined
export let bottomActionFunc: (() => void) | undefined = undefined
export let bottomActions: BottomAction[] = []
export let object: any
async function validate () {
@ -152,15 +156,15 @@
{/if}
</div>
</Scroller>
{#if bottomCaption || (bottomActionLabel && bottomActionFunc)}
{#if bottomActions.length}
<div class="grow-separator" />
<div class="footer">
{#if bottomCaption}
<span><Label label={bottomCaption} /></span>
{/if}
{#if bottomActionLabel && bottomActionFunc}
<a href="." on:click|preventDefault={bottomActionFunc}><Label label={bottomActionLabel} /></a>
{/if}
{#each bottomActions as action}
<div>
<span><Label label={action.caption} /></span>
<a href="." on:click|preventDefault={action.func}><Label label={action.i18n} /></a>
</div>
{/each}
</div>
{/if}
</form>

View File

@ -84,13 +84,24 @@
}
}
$: bottomCaption = page === 'login' ? login.string.DoNotHaveAnAccount : login.string.HaveAccount
$: bottomActionLabel = page === 'login' ? login.string.SignUp : login.string.LogIn
$: bottom = page === 'login' ? [signUpAction] : [loginAction]
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : undefined
$: secondaryButtonAction = () => {
page = 'signUp'
}
const signUpAction = {
caption: login.string.DoNotHaveAnAccount,
i18n: login.string.SignUp,
func: () => (page = 'signUp')
}
const loginAction = {
caption: login.string.HaveAccount,
i18n: login.string.LogIn,
func: () => (page = 'login')
}
onMount(() => {
check()
})
@ -121,9 +132,5 @@
{action}
{secondaryButtonLabel}
{secondaryButtonAction}
{bottomCaption}
{bottomActionLabel}
bottomActionFunc={() => {
page = page === 'login' ? 'signUp' : 'login'
}}
bottomActions={bottom}
/>

View File

@ -25,6 +25,8 @@
import { onDestroy } from 'svelte'
import login from '../plugin'
import { getMetadata } from '@hcengineering/platform'
import PasswordRequest from './PasswordRequest.svelte'
import PasswordRestore from './PasswordRestore.svelte'
export let page: string = 'login'
@ -58,6 +60,10 @@
<SignupForm />
{:else if page === 'createWorkspace'}
<CreateWorkspaceForm />
{:else if page === 'password'}
<PasswordRequest />
{:else if page === 'recovery'}
<PasswordRestore />
{:else if page === 'selectWorkspace'}
<SelectWorkspace {navigateUrl} />
{:else if page === 'join'}

View File

@ -14,11 +14,11 @@
// limitations under the License.
-->
<script lang="ts">
import { OK, Status, Severity, setMetadata } from '@hcengineering/platform'
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
import Form from './Form.svelte'
import { doLogin } from '../utils'
import Form from './Form.svelte'
import login from '../plugin'
@ -63,20 +63,28 @@
}
}
}
const recoveryAction = {
caption: login.string.ForgotPassword,
i18n: login.string.Recover,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'password'
loc.path.length = 2
navigate(loc)
}
}
const signUpAction = {
caption: login.string.DoNotHaveAnAccount,
i18n: login.string.SignUp,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'signup'
loc.path.length = 2
navigate(loc)
}
}
</script>
<Form
caption={login.string.LogIn}
{status}
{fields}
{object}
{action}
bottomCaption={login.string.DoNotHaveAnAccount}
bottomActionLabel={login.string.SignUp}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'signup'
loc.path.length = 2
navigate(loc)
}}
/>
<Form caption={login.string.LogIn} {status} {fields} {object} {action} bottomActions={[recoveryAction, signUpAction]} />

View File

@ -0,0 +1,84 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { OK, Severity, Status } from '@hcengineering/platform'
import { MessageBox } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, showPopup } from '@hcengineering/ui'
import login from '../plugin'
import { requestPassword } from '../utils'
import Form from './Form.svelte'
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
const object = {
username: ''
}
let status: Status<any> = OK
const action = {
i18n: login.string.Recover,
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const loginStatus = await requestPassword(object.username)
status = loginStatus
if (loginStatus === OK) {
showPopup(
MessageBox,
{
label: login.string.PasswordRecovery,
message: login.string.RecoveryLinkSent,
canSubmit: false
},
undefined,
() => {
const loc = getCurrentLocation()
loc.path[1] = 'login'
navigate(loc)
}
)
}
}
}
const signUpAction = {
caption: login.string.DoNotHaveAnAccount,
i18n: login.string.SignUp,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'signup'
loc.path.length = 2
navigate(loc)
}
}
const bottomActions = [
{
caption: login.string.KnowPassword,
i18n: login.string.LogIn,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
}
},
signUpAction
]
</script>
<Form caption={login.string.PasswordRecovery} {status} {fields} {object} {action} {bottomActions} />

View File

@ -0,0 +1,59 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
import login from '../plugin'
import { restorePassword } from '../utils'
import Form from './Form.svelte'
const fields = [
{ id: 'new-password', name: 'password', i18n: login.string.Password, password: true },
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
const object = {
password: '',
password2: ''
}
let status: Status<any> = OK
const action = {
i18n: login.string.Recover,
func: async () => {
const location = getCurrentLocation()
if (location.query?.id === undefined || location.query?.id === null) return
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await restorePassword(location.query?.id, object.password)
status = loginStatus
if (result !== undefined) {
setMetadata(login.metadata.LoginToken, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}
}
}
</script>
<Form caption={login.string.PasswordRecovery} {status} {fields} {object} {action} />

View File

@ -67,12 +67,16 @@
{fields}
{object}
{action}
bottomCaption={login.string.HaveAccount}
bottomActionLabel={login.string.LogIn}
bottomActionFunc={() => {
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
}}
bottomActions={[
{
caption: login.string.HaveAccount,
i18n: login.string.LogIn,
func: () => {
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
}
}
]}
/>

View File

@ -50,6 +50,11 @@ export default mergeIds(loginId, login, {
ChangeAccount: '' as IntlString,
WorkspaceNameRule: '' as IntlString,
LinkValidHours: '' as IntlString,
GetLink: '' as IntlString
GetLink: '' as IntlString,
ForgotPassword: '' as IntlString,
Recover: '' as IntlString,
KnowPassword: '' as IntlString,
PasswordRecovery: '' as IntlString,
RecoveryLinkSent: '' as IntlString
}
})

View File

@ -566,3 +566,65 @@ export async function leaveWorkspace (email: string): Promise<void> {
body: serialize(request)
})
}
export async function requestPassword (email: string): Promise<Status> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
if (accountsUrl === undefined) {
throw new Error('accounts url not specified')
}
const overrideToken = getMetadata(login.metadata.OverrideLoginToken)
if (overrideToken !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) {
return OK
}
}
const request: Request<[string]> = {
method: 'requestPassword',
params: [email]
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: serialize(request)
})
const result: Response<any> = await response.json()
return result.error ?? OK
} catch (err) {
return unknownError(err)
}
}
export async function restorePassword (token: string, password: string): Promise<[Status, LoginInfo | undefined]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
if (accountsUrl === undefined) {
throw new Error('accounts url not specified')
}
const request: Request<[string]> = {
method: 'restorePassword',
params: [password]
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: serialize(request)
})
const result: Response<any> = await response.json()
return [result.error ?? OK, result.result]
} catch (err) {
return [unknownError(err), undefined]
}
}

View File

@ -13,7 +13,7 @@
"docker:build": "docker build -t hardcoreeng/account .",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/account staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/account",
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost SERVER_SECRET='secret' TRANSACTOR_URL=ws:/localhost:3333 ts-node src/__start.ts",
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmi MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost SERVER_SECRET='secret' TRANSACTOR_URL=ws:/localhost:3333 ts-node src/__start.ts",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import { AccountMethod, ACCOUNT_DB } from '@hcengineering/account'
import account, { AccountMethod, ACCOUNT_DB } from '@hcengineering/account'
import platform, { Response, serialize, setMetadata, Severity, Status } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token'
import toolPlugin from '@hcengineering/server-tool'
@ -50,6 +50,12 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
process.exit(1)
}
const ses = process.env.SES_URL
const frontURL = process.env.FRONT_URL
setMetadata(account.metadata.SES_URL, ses)
setMetadata(account.metadata.FrontURL, frontURL)
setMetadata(serverToken.metadata.Secret, serverSecret)
setMetadata(toolPlugin.metadata.Endpoint, endpointUri)
setMetadata(toolPlugin.metadata.Transactor, transactorUri)

View File

@ -24,7 +24,8 @@
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5",
"@types/ws": "^8.5.3"
"@types/ws": "^8.5.3",
"@types/node-fetch": "~2.6.2"
},
"dependencies": {
"mongodb": "^4.11.0",
@ -37,6 +38,7 @@
"@hcengineering/model": "^0.6.0",
"@hcengineering/server-tool": "^0.6.0",
"@hcengineering/server-token": "^0.6.0",
"@hcengineering/model-all": "^0.6.0"
"@hcengineering/model-all": "^0.6.0",
"node-fetch": "^2.6.6"
}
}

View File

@ -34,6 +34,7 @@ import core, {
import { MigrateOperation } from '@hcengineering/model'
import platform, {
getMetadata,
Metadata,
PlatformError,
Plugin,
plugin,
@ -47,6 +48,7 @@ import { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, { connect, initModel, upgradeModel } from '@hcengineering/server-tool'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { Binary, Db, Filter, ObjectId } from 'mongodb'
import fetch from 'node-fetch'
const WORKSPACE_COLLECTION = 'workspace'
const ACCOUNT_COLLECTION = 'account'
@ -73,6 +75,10 @@ const accountPlugin = plugin(accountId, {
AccountAlreadyExists: '' as StatusCode<{ account: string }>,
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
ProductIdMismatch: '' as StatusCode<{ productId: string }>
},
metadata: {
FrontURL: '' as Metadata<string>,
SES_URL: '' as Metadata<string>
}
})
@ -637,7 +643,7 @@ export async function replacePassword (db: Db, productId: string, email: string,
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.InvalidPassword, { account: email }))
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
const salt = randomBytes(32)
const hash = hashWithSalt(password, salt)
@ -645,6 +651,73 @@ export async function replacePassword (db: Db, productId: string, email: string,
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { salt, hash } })
}
/**
* @public
*/
export async function requestPassword (db: Db, productId: string, email: string): Promise<void> {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
if (sesURL === undefined || sesURL === '') {
throw new Error('Please provide email service url')
}
const front = getMetadata(accountPlugin.metadata.FrontURL)
if (front === undefined || front === '') {
throw new Error('Please provide front url')
}
const token = generateToken('@restore', getWorkspaceId('', productId), {
restore: email
})
const link = `${front}/login/recovery?id=${token}`
const text = `We received a request to reset the password for your account. To reset your password, please paste the following link in your web browser's address bar: ${link}. If you have not ordered a password recovery just ignore this letter.`
const html = `<p>We received a request to reset the password for your account. To reset your password, please click the link below: <a href=${link}>Reset password</a></p><p>
If the Reset password link above does not work, paste the following link in your web browser's address bar: ${link}
</p><p>If you have not ordered a password recovery just ignore this letter.</p>`
const subject = 'Password recovery'
const to = account.email
await fetch(`${sesURL}/send`, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
html,
subject,
to
})
})
}
/**
* @public
*/
export async function restorePassword (db: Db, productId: string, token: string, password: string): Promise<LoginInfo> {
const decode = decodeToken(token)
const email = decode.extra?.restore
if (email === undefined) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: accountId }))
}
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: accountId }))
}
const salt = randomBytes(32)
const hash = hashWithSalt(password, salt)
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { salt, hash } })
return await login(db, productId, email, password)
}
/**
* @public
*/
@ -868,7 +941,9 @@ export function getMethods (
leaveWorkspace: wrap(leaveWorkspace),
listWorkspaces: wrap(listWorkspaces),
changeName: wrap(changeName),
changePassword: wrap(changePassword)
changePassword: wrap(changePassword),
requestPassword: wrap(requestPassword),
restorePassword: wrap(restorePassword)
// updateAccount: wrap(updateAccount)
}
}