mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-05 15:24:22 +00:00
Password recovery (#2568)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
35985ce8d6
commit
3f755682cf
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@ -48,10 +48,12 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": ["src/__start.ts"],
|
"args": ["src/__start.ts"],
|
||||||
"env": {
|
"env": {
|
||||||
"MONGO_URL": "mongodb://localhost:27018",
|
"MONGO_URL": "mongodb://localhost:27017",
|
||||||
"SERVER_SECRET": "secret",
|
"SERVER_SECRET": "secret",
|
||||||
"TRANSACTOR_URL": "ws:/localhost:3333",
|
"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"],
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
|
@ -319,6 +319,7 @@ specifiers:
|
|||||||
mini-css-extract-plugin: ^2.2.0
|
mini-css-extract-plugin: ^2.2.0
|
||||||
minio: ^7.0.26
|
minio: ^7.0.26
|
||||||
mongodb: ^4.11.0
|
mongodb: ^4.11.0
|
||||||
|
node-fetch: ^2.6.6
|
||||||
p-queue: ~7.3.0
|
p-queue: ~7.3.0
|
||||||
pdfkit: ~0.13.0
|
pdfkit: ~0.13.0
|
||||||
postcss: ^8.4.20
|
postcss: ^8.4.20
|
||||||
@ -686,6 +687,7 @@ dependencies:
|
|||||||
mini-css-extract-plugin: 2.6.1_webpack@5.75.0
|
mini-css-extract-plugin: 2.6.1_webpack@5.75.0
|
||||||
minio: 7.0.32
|
minio: 7.0.32
|
||||||
mongodb: 4.11.0
|
mongodb: 4.11.0
|
||||||
|
node-fetch: 2.6.7
|
||||||
p-queue: 7.3.0
|
p-queue: 7.3.0
|
||||||
pdfkit: 0.13.0
|
pdfkit: 0.13.0
|
||||||
postcss: 8.4.20
|
postcss: 8.4.20
|
||||||
@ -3427,6 +3429,13 @@ packages:
|
|||||||
'@types/node': 16.11.68
|
'@types/node': 16.11.68
|
||||||
dev: false
|
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:
|
/@types/node/12.20.24:
|
||||||
resolution: {integrity: sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==}
|
resolution: {integrity: sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -11462,13 +11471,14 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/account.tgz:
|
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'
|
name: '@rush-temp/account'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rushstack/heft': 0.47.11
|
'@rushstack/heft': 0.47.11
|
||||||
'@types/heft-jest': 1.0.3
|
'@types/heft-jest': 1.0.3
|
||||||
'@types/minio': 7.0.14
|
'@types/minio': 7.0.14
|
||||||
|
'@types/node-fetch': 2.6.2
|
||||||
'@types/ws': 8.5.3
|
'@types/ws': 8.5.3
|
||||||
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
|
||||||
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
|
'@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
|
eslint-plugin-promise: 6.1.1_eslint@8.27.0
|
||||||
minio: 7.0.32
|
minio: 7.0.32
|
||||||
mongodb: 4.11.0
|
mongodb: 4.11.0
|
||||||
|
node-fetch: 2.6.7
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
ws: 8.11.0
|
ws: 8.11.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- bufferutil
|
- bufferutil
|
||||||
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
dev: false
|
dev: false
|
||||||
@ -13477,7 +13489,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/model-recruit.tgz_typescript@4.8.4:
|
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
|
id: file:projects/model-recruit.tgz
|
||||||
name: '@rush-temp/model-recruit'
|
name: '@rush-temp/model-recruit'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
@ -58,6 +58,8 @@ services:
|
|||||||
- MINIO_ENDPOINT=minio
|
- MINIO_ENDPOINT=minio
|
||||||
- MINIO_ACCESS_KEY=minioadmin
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
- MINIO_SECRET_KEY=minioadmin
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- FRONT_URL=http://localhost:8087
|
||||||
|
- SES_URL=http://localhost:8091
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
collaborator:
|
collaborator:
|
||||||
image: hardcoreeng/collaborator
|
image: hardcoreeng/collaborator
|
||||||
|
@ -29,6 +29,11 @@
|
|||||||
"NotSeeingWorkspace": "Not seeing your workspace?",
|
"NotSeeingWorkspace": "Not seeing your workspace?",
|
||||||
"WorkspaceNameRule": "The workspace name can contains lowercase letters, numbers, and symbols !@#%&^-",
|
"WorkspaceNameRule": "The workspace name can contains lowercase letters, numbers, and symbols !@#%&^-",
|
||||||
"LinkValidHours": "Link valid (hours):",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,6 +29,11 @@
|
|||||||
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
||||||
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символов !@#%&^-",
|
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символов !@#%&^-",
|
||||||
"LinkValidHours": "Ссылка действительна (часов):",
|
"LinkValidHours": "Ссылка действительна (часов):",
|
||||||
"GetLink": "Получить ссылку"
|
"GetLink": "Получить ссылку",
|
||||||
|
"ForgotPassword": "Забыли пароль?",
|
||||||
|
"KnowPassword": "Знаете пароль?",
|
||||||
|
"Recover": "Восстановить",
|
||||||
|
"PasswordRecovery": "Восстановление пароля",
|
||||||
|
"RecoveryLinkSent": "Ссылка для восстановления пароля отправлена на почту"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -73,12 +73,16 @@
|
|||||||
{fields}
|
{fields}
|
||||||
{object}
|
{object}
|
||||||
{action}
|
{action}
|
||||||
bottomCaption={login.string.HaveWorkspace}
|
bottomActions={[
|
||||||
bottomActionLabel={login.string.SelectWorkspace}
|
{
|
||||||
bottomActionFunc={() => {
|
caption: login.string.HaveWorkspace,
|
||||||
const loc = getCurrentLocation()
|
i18n: login.string.SelectWorkspace,
|
||||||
loc.path[1] = 'selectWorkspace'
|
func: () => {
|
||||||
loc.path.length = 2
|
const loc = getCurrentLocation()
|
||||||
navigate(loc)
|
loc.path[1] = 'selectWorkspace'
|
||||||
}}
|
loc.path.length = 2
|
||||||
|
navigate(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -38,15 +38,19 @@
|
|||||||
func: () => Promise<void>
|
func: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BottomAction {
|
||||||
|
i18n: IntlString
|
||||||
|
func: () => void
|
||||||
|
caption: IntlString
|
||||||
|
}
|
||||||
|
|
||||||
export let caption: IntlString
|
export let caption: IntlString
|
||||||
export let status: Status
|
export let status: Status
|
||||||
export let fields: Field[]
|
export let fields: Field[]
|
||||||
export let action: Action
|
export let action: Action
|
||||||
export let secondaryButtonLabel: IntlString | undefined = undefined
|
export let secondaryButtonLabel: IntlString | undefined = undefined
|
||||||
export let secondaryButtonAction: (() => void) | undefined = undefined
|
export let secondaryButtonAction: (() => void) | undefined = undefined
|
||||||
export let bottomCaption: IntlString | undefined = undefined
|
export let bottomActions: BottomAction[] = []
|
||||||
export let bottomActionLabel: IntlString | undefined = undefined
|
|
||||||
export let bottomActionFunc: (() => void) | undefined = undefined
|
|
||||||
export let object: any
|
export let object: any
|
||||||
|
|
||||||
async function validate () {
|
async function validate () {
|
||||||
@ -152,15 +156,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
{#if bottomCaption || (bottomActionLabel && bottomActionFunc)}
|
{#if bottomActions.length}
|
||||||
<div class="grow-separator" />
|
<div class="grow-separator" />
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
{#if bottomCaption}
|
{#each bottomActions as action}
|
||||||
<span><Label label={bottomCaption} /></span>
|
<div>
|
||||||
{/if}
|
<span><Label label={action.caption} /></span>
|
||||||
{#if bottomActionLabel && bottomActionFunc}
|
<a href="." on:click|preventDefault={action.func}><Label label={action.i18n} /></a>
|
||||||
<a href="." on:click|preventDefault={bottomActionFunc}><Label label={bottomActionLabel} /></a>
|
</div>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
@ -84,13 +84,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: bottomCaption = page === 'login' ? login.string.DoNotHaveAnAccount : login.string.HaveAccount
|
$: bottom = page === 'login' ? [signUpAction] : [loginAction]
|
||||||
$: bottomActionLabel = page === 'login' ? login.string.SignUp : login.string.LogIn
|
|
||||||
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : undefined
|
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : undefined
|
||||||
$: secondaryButtonAction = () => {
|
$: secondaryButtonAction = () => {
|
||||||
page = 'signUp'
|
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(() => {
|
onMount(() => {
|
||||||
check()
|
check()
|
||||||
})
|
})
|
||||||
@ -121,9 +132,5 @@
|
|||||||
{action}
|
{action}
|
||||||
{secondaryButtonLabel}
|
{secondaryButtonLabel}
|
||||||
{secondaryButtonAction}
|
{secondaryButtonAction}
|
||||||
{bottomCaption}
|
bottomActions={bottom}
|
||||||
{bottomActionLabel}
|
|
||||||
bottomActionFunc={() => {
|
|
||||||
page = page === 'login' ? 'signUp' : 'login'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
import login from '../plugin'
|
import login from '../plugin'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
|
import PasswordRequest from './PasswordRequest.svelte'
|
||||||
|
import PasswordRestore from './PasswordRestore.svelte'
|
||||||
|
|
||||||
export let page: string = 'login'
|
export let page: string = 'login'
|
||||||
|
|
||||||
@ -58,6 +60,10 @@
|
|||||||
<SignupForm />
|
<SignupForm />
|
||||||
{:else if page === 'createWorkspace'}
|
{:else if page === 'createWorkspace'}
|
||||||
<CreateWorkspaceForm />
|
<CreateWorkspaceForm />
|
||||||
|
{:else if page === 'password'}
|
||||||
|
<PasswordRequest />
|
||||||
|
{:else if page === 'recovery'}
|
||||||
|
<PasswordRestore />
|
||||||
{:else if page === 'selectWorkspace'}
|
{:else if page === 'selectWorkspace'}
|
||||||
<SelectWorkspace {navigateUrl} />
|
<SelectWorkspace {navigateUrl} />
|
||||||
{:else if page === 'join'}
|
{:else if page === 'join'}
|
||||||
|
@ -14,11 +14,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||||
|
|
||||||
import Form from './Form.svelte'
|
|
||||||
import { doLogin } from '../utils'
|
import { doLogin } from '../utils'
|
||||||
|
import Form from './Form.svelte'
|
||||||
|
|
||||||
import login from '../plugin'
|
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>
|
</script>
|
||||||
|
|
||||||
<Form
|
<Form caption={login.string.LogIn} {status} {fields} {object} {action} bottomActions={[recoveryAction, signUpAction]} />
|
||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
@ -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} />
|
@ -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} />
|
@ -67,12 +67,16 @@
|
|||||||
{fields}
|
{fields}
|
||||||
{object}
|
{object}
|
||||||
{action}
|
{action}
|
||||||
bottomCaption={login.string.HaveAccount}
|
bottomActions={[
|
||||||
bottomActionLabel={login.string.LogIn}
|
{
|
||||||
bottomActionFunc={() => {
|
caption: login.string.HaveAccount,
|
||||||
const loc = getCurrentLocation()
|
i18n: login.string.LogIn,
|
||||||
loc.path[1] = 'login'
|
func: () => {
|
||||||
loc.path.length = 2
|
const loc = getCurrentLocation()
|
||||||
navigate(loc)
|
loc.path[1] = 'login'
|
||||||
}}
|
loc.path.length = 2
|
||||||
|
navigate(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -50,6 +50,11 @@ export default mergeIds(loginId, login, {
|
|||||||
ChangeAccount: '' as IntlString,
|
ChangeAccount: '' as IntlString,
|
||||||
WorkspaceNameRule: '' as IntlString,
|
WorkspaceNameRule: '' as IntlString,
|
||||||
LinkValidHours: '' 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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -566,3 +566,65 @@ export async function leaveWorkspace (email: string): Promise<void> {
|
|||||||
body: serialize(request)
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"docker:build": "docker build -t hardcoreeng/account .",
|
"docker:build": "docker build -t hardcoreeng/account .",
|
||||||
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/account staging",
|
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/account staging",
|
||||||
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/account",
|
"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",
|
"lint": "eslint src",
|
||||||
"format": "prettier --write src && eslint --fix src"
|
"format": "prettier --write src && eslint --fix src"
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// 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 platform, { Response, serialize, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||||
import serverToken from '@hcengineering/server-token'
|
import serverToken from '@hcengineering/server-token'
|
||||||
import toolPlugin from '@hcengineering/server-tool'
|
import toolPlugin from '@hcengineering/server-tool'
|
||||||
@ -50,6 +50,12 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
|
|||||||
process.exit(1)
|
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(serverToken.metadata.Secret, serverSecret)
|
||||||
setMetadata(toolPlugin.metadata.Endpoint, endpointUri)
|
setMetadata(toolPlugin.metadata.Endpoint, endpointUri)
|
||||||
setMetadata(toolPlugin.metadata.Transactor, transactorUri)
|
setMetadata(toolPlugin.metadata.Transactor, transactorUri)
|
||||||
|
@ -24,7 +24,8 @@
|
|||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"@rushstack/heft": "^0.47.9",
|
"@rushstack/heft": "^0.47.9",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"@types/ws": "^8.5.3"
|
"@types/ws": "^8.5.3",
|
||||||
|
"@types/node-fetch": "~2.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mongodb": "^4.11.0",
|
"mongodb": "^4.11.0",
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"@hcengineering/model": "^0.6.0",
|
"@hcengineering/model": "^0.6.0",
|
||||||
"@hcengineering/server-tool": "^0.6.0",
|
"@hcengineering/server-tool": "^0.6.0",
|
||||||
"@hcengineering/server-token": "^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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import core, {
|
|||||||
import { MigrateOperation } from '@hcengineering/model'
|
import { MigrateOperation } from '@hcengineering/model'
|
||||||
import platform, {
|
import platform, {
|
||||||
getMetadata,
|
getMetadata,
|
||||||
|
Metadata,
|
||||||
PlatformError,
|
PlatformError,
|
||||||
Plugin,
|
Plugin,
|
||||||
plugin,
|
plugin,
|
||||||
@ -47,6 +48,7 @@ import { decodeToken, generateToken } from '@hcengineering/server-token'
|
|||||||
import toolPlugin, { connect, initModel, upgradeModel } from '@hcengineering/server-tool'
|
import toolPlugin, { connect, initModel, upgradeModel } from '@hcengineering/server-tool'
|
||||||
import { pbkdf2Sync, randomBytes } from 'crypto'
|
import { pbkdf2Sync, randomBytes } from 'crypto'
|
||||||
import { Binary, Db, Filter, ObjectId } from 'mongodb'
|
import { Binary, Db, Filter, ObjectId } from 'mongodb'
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
|
||||||
const WORKSPACE_COLLECTION = 'workspace'
|
const WORKSPACE_COLLECTION = 'workspace'
|
||||||
const ACCOUNT_COLLECTION = 'account'
|
const ACCOUNT_COLLECTION = 'account'
|
||||||
@ -73,6 +75,10 @@ const accountPlugin = plugin(accountId, {
|
|||||||
AccountAlreadyExists: '' as StatusCode<{ account: string }>,
|
AccountAlreadyExists: '' as StatusCode<{ account: string }>,
|
||||||
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
|
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
|
||||||
ProductIdMismatch: '' as StatusCode<{ productId: 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)
|
const account = await getAccount(db, email)
|
||||||
|
|
||||||
if (account === null) {
|
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 salt = randomBytes(32)
|
||||||
const hash = hashWithSalt(password, salt)
|
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 } })
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -868,7 +941,9 @@ export function getMethods (
|
|||||||
leaveWorkspace: wrap(leaveWorkspace),
|
leaveWorkspace: wrap(leaveWorkspace),
|
||||||
listWorkspaces: wrap(listWorkspaces),
|
listWorkspaces: wrap(listWorkspaces),
|
||||||
changeName: wrap(changeName),
|
changeName: wrap(changeName),
|
||||||
changePassword: wrap(changePassword)
|
changePassword: wrap(changePassword),
|
||||||
|
requestPassword: wrap(requestPassword),
|
||||||
|
restorePassword: wrap(restorePassword)
|
||||||
// updateAccount: wrap(updateAccount)
|
// updateAccount: wrap(updateAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user