mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 03:40:48 +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",
|
||||
"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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@
|
||||
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
|
||||
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символов !@#%&^-",
|
||||
"LinkValidHours": "Ссылка действительна (часов):",
|
||||
"GetLink": "Получить ссылку"
|
||||
"GetLink": "Получить ссылку",
|
||||
"ForgotPassword": "Забыли пароль?",
|
||||
"KnowPassword": "Знаете пароль?",
|
||||
"Recover": "Восстановить",
|
||||
"PasswordRecovery": "Восстановление пароля",
|
||||
"RecoveryLinkSent": "Ссылка для восстановления пароля отправлена на почту"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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'}
|
||||
|
@ -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]} />
|
||||
|
@ -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}
|
||||
{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)
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user