UBER-221 Confirm registration (#3254)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-05-29 12:20:59 +06:00 committed by GitHub
parent 89ecba6b9a
commit 0d70ba8363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 363 additions and 49 deletions

View File

@ -17,6 +17,7 @@
import { import {
ACCOUNT_DB, ACCOUNT_DB,
assignWorkspace, assignWorkspace,
confirmEmail,
createAccount, createAccount,
createWorkspace, createWorkspace,
dropAccount, dropAccount,
@ -371,6 +372,16 @@ export function devTool (
) )
}) })
program
.command('confirm-email <email>')
.description('confirm user email')
.action(async (email: string, cmd) => {
const { mongodbUri } = prepareTools()
return await withDatabase(mongodbUri, async (db) => {
await confirmEmail(db, email)
})
})
program program
.command('diff-workspace <workspace>') .command('diff-workspace <workspace>')
.description('restore workspace transactions and minio resources from previous dump.') .description('restore workspace transactions and minio resources from previous dump.')

View File

@ -16,7 +16,7 @@
import { popupstore as modal } from '../popups' import { popupstore as modal } from '../popups'
import PopupInstance from './PopupInstance.svelte' import PopupInstance from './PopupInstance.svelte'
export let contentPanel: HTMLElement export let contentPanel: HTMLElement | undefined = undefined
const instances: PopupInstance[] = [] const instances: PopupInstance[] = []

View File

@ -27,7 +27,7 @@
export let zIndex: number export let zIndex: number
export let top: boolean export let top: boolean
export let close: () => void export let close: () => void
export let contentPanel: HTMLElement export let contentPanel: HTMLElement | undefined
let modalHTML: HTMLElement let modalHTML: HTMLElement
let componentInstance: any let componentInstance: any
@ -68,7 +68,11 @@
_close(undefined) _close(undefined)
} }
const fitPopup = (modalHTML: HTMLElement, element: PopupAlignment | undefined, contentPanel: HTMLElement): void => { const fitPopup = (
modalHTML: HTMLElement,
element: PopupAlignment | undefined,
contentPanel: HTMLElement | undefined
): void => {
if ((fullSize || docSize) && (element === 'float' || element === 'centered')) { if ((fullSize || docSize) && (element === 'float' || element === 'centered')) {
options = fitPopupElement(modalHTML, 'full', contentPanel) options = fitPopupElement(modalHTML, 'full', contentPanel)
options.props.maxHeight = '100vh' options.props.maxHeight = '100vh'

View File

@ -39,6 +39,8 @@
"InviteLimit": "Invite limit:", "InviteLimit": "Invite limit:",
"GetLink": "Get invite link", "GetLink": "Get invite link",
"NoLimit": "No limit", "NoLimit": "No limit",
"AlreadyJoined": "Already joined?" "AlreadyJoined": "Already joined?",
"ConfirmationSent": "A message has been sent to your email containing a link to confirm the your address.",
"ConfirmationSent2": "Please follow the link to complete your sign up."
} }
} }

View File

@ -39,6 +39,8 @@
"InviteLimit": "Предел использований:", "InviteLimit": "Предел использований:",
"GetLink": "Получить ссылку", "GetLink": "Получить ссылку",
"NoLimit": "Без предела использований", "NoLimit": "Без предела использований",
"AlreadyJoined": "Уже подключены?" "AlreadyJoined": "Уже подключены?",
"ConfirmationSent": "На Вашу почту отправлено сообщение, c ссылкой для подтверждения email.",
"ConfirmationSent2": "Пожалуйста, перейдите по ссылке для завершения регистрации."
} }
} }

View File

@ -0,0 +1,50 @@
<!--
// 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 { confirm } from '../utils'
import presentation from '@hcengineering/presentation'
import { onMount } from 'svelte'
let status: Status<any> = OK
async function check () {
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 confirm(location.query?.id)
status = loginStatus
if (result !== undefined) {
setMetadata(presentation.metadata.Token, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
const loc = getCurrentLocation()
loc.query = undefined
loc.path[1] = 'selectWorkspace'
loc.path.length = 2
navigate(loc)
}
}
onMount(() => {
check()
})
</script>

View File

@ -0,0 +1,27 @@
<!--
// 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 { Label } from '@hcengineering/ui'
import login from '../plugin'
</script>
<div class="flex-center h-full p-10">
<div class="flex-col-center text-center">
<h4>
<Label label={login.string.ConfirmationSent} />
</h4>
<Label label={login.string.ConfirmationSent2} />
</div>
</div>

View File

@ -17,11 +17,12 @@
import { Status, Severity, OK, setMetadata } from '@hcengineering/platform' import { Status, Severity, OK, setMetadata } from '@hcengineering/platform'
import Form from './Form.svelte' import Form from './Form.svelte'
import { createWorkspace } from '../utils' import { createWorkspace, getAccount } from '../utils'
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui' import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
import login from '../plugin' import login from '../plugin'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import presentation from '@hcengineering/presentation' import presentation from '@hcengineering/presentation'
import { onMount } from 'svelte'
const fields = [ const fields = [
{ {
@ -38,6 +39,16 @@
let status: Status<any> = OK let status: Status<any> = OK
onMount(async () => {
const account = await getAccount()
if (account?.confirmed !== true) {
const loc = getCurrentLocation()
loc.path[1] = 'confirmationSend'
loc.path.length = 2
navigate(loc)
}
})
const action = { const action = {
i18n: login.string.CreateWorkspace, i18n: login.string.CreateWorkspace,
func: async () => { func: async () => {

View File

@ -27,6 +27,8 @@
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import PasswordRequest from './PasswordRequest.svelte' import PasswordRequest from './PasswordRequest.svelte'
import PasswordRestore from './PasswordRestore.svelte' import PasswordRestore from './PasswordRestore.svelte'
import Confirmation from './Confirmation.svelte'
import ConfirmationSend from './ConfirmationSend.svelte'
export let page: string = 'login' export let page: string = 'login'
@ -68,6 +70,10 @@
<SelectWorkspace {navigateUrl} /> <SelectWorkspace {navigateUrl} />
{:else if page === 'join'} {:else if page === 'join'}
<Join /> <Join />
{:else if page === 'confirm'}
<Confirmation />
{:else if page === 'confirmationSend'}
<ConfirmationSend />
{/if} {/if}
</div> </div>
<Intro landscape={$deviceInfo.docWidth <= 768} mini={$deviceInfo.docWidth <= 480} /> <Intro landscape={$deviceInfo.docWidth <= 768} mini={$deviceInfo.docWidth <= 480} />

View File

@ -75,7 +75,7 @@
} }
} }
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace' loc.path[1] = result.confirmed ? 'selectWorkspace' : 'confirmationSend'
loc.path.length = 2 loc.path.length = 2
if (navigateUrl !== undefined) { if (navigateUrl !== undefined) {
loc.query = { ...loc.query, navigateUrl } loc.query = { ...loc.query, navigateUrl }

View File

@ -26,14 +26,19 @@
setMetadataLocalStorage setMetadataLocalStorage
} from '@hcengineering/ui' } from '@hcengineering/ui'
import login from '../plugin' import login from '../plugin'
import { getWorkspaces, navigateToWorkspace, selectWorkspace } from '../utils' import { getAccount, getWorkspaces, navigateToWorkspace, selectWorkspace } from '../utils'
import StatusControl from './StatusControl.svelte' import StatusControl from './StatusControl.svelte'
import { Workspace } from '@hcengineering/login' import { LoginInfo, Workspace } from '@hcengineering/login'
import { onMount } from 'svelte'
export let navigateUrl: string | undefined = undefined export let navigateUrl: string | undefined = undefined
let status = OK let status = OK
let account: LoginInfo | undefined = undefined
onMount(async () => (account = await getAccount()))
async function select (workspace: string) { async function select (workspace: string) {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {}) status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
@ -88,7 +93,7 @@
{workspace.workspace} {workspace.workspace}
</div> </div>
{/each} {/each}
{#if !workspaces.length} {#if !workspaces.length && account?.confirmed === true}
<div class="form-row send"> <div class="form-row send">
<Button label={login.string.CreateWorkspace} kind={'primary'} width="100%" on:click={createWorkspace} /> <Button label={login.string.CreateWorkspace} kind={'primary'} width="100%" on:click={createWorkspace} />
</div> </div>

View File

@ -14,13 +14,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Status, Severity, OK, setMetadata } from '@hcengineering/platform' import { OK, Severity, Status } from '@hcengineering/platform'
import Form from './Form.svelte' import { getCurrentLocation, navigate } from '@hcengineering/ui'
import { signUp } from '../utils'
import login from '../plugin' import login from '../plugin'
import { getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui' import { signUp } from '../utils'
import presentation from '@hcengineering/presentation' import Form from './Form.svelte'
const fields = [ const fields = [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true }, { id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
@ -50,11 +49,8 @@
status = loginStatus status = loginStatus
if (result !== undefined) { if (result !== undefined) {
setMetadata(presentation.metadata.Token, result.token)
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[1] = 'selectWorkspace' loc.path[1] = 'confirmationSend'
loc.path.length = 2 loc.path.length = 2
navigate(loc) navigate(loc)
} }

View File

@ -56,6 +56,8 @@ export default mergeIds(loginId, login, {
RecoveryLinkSent: '' as IntlString, RecoveryLinkSent: '' as IntlString,
UseWorkspaceInviteSettings: '' as IntlString, UseWorkspaceInviteSettings: '' as IntlString,
GetLink: '' as IntlString, GetLink: '' as IntlString,
AlreadyJoined: '' as IntlString AlreadyJoined: '' as IntlString,
ConfirmationSent: '' as IntlString,
ConfirmationSent2: '' as IntlString
} }
}) })

View File

@ -49,7 +49,7 @@ export async function doLogin (email: string, password: string): Promise<[Status
if (token !== undefined) { if (token !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token, endpoint, email }] return [OK, { token, endpoint, email, confirmed: true }]
} }
} }
@ -91,7 +91,7 @@ export async function signUp (
if (token !== undefined) { if (token !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token, endpoint, email }] return [OK, { token, endpoint, email, confirmed: true }]
} }
} }
@ -127,7 +127,7 @@ export async function createWorkspace (workspace: string): Promise<[Status, Logi
if (overrideToken !== undefined) { if (overrideToken !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token: overrideToken, endpoint, email }] return [OK, { token: overrideToken, endpoint, email, confirmed: true }]
} }
} }
@ -213,6 +213,53 @@ export async function getWorkspaces (): Promise<Workspace[]> {
} }
} }
export async function getAccount (): Promise<LoginInfo | undefined> {
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)
const email = fetchMetadataLocalStorage(login.metadata.LoginEmail) ?? ''
if (endpoint !== undefined) {
return { token: overrideToken, endpoint, email, confirmed: true }
}
}
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
return
}
const request = {
method: 'getAccountInfoByToken',
params: [] as any[]
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
const result = await response.json()
if (result.error != null) {
throw new PlatformError(result.error)
}
return result.result
} catch (err) {}
}
export async function selectWorkspace (workspace: string): Promise<[Status, WorkspaceLoginInfo | undefined]> { export async function selectWorkspace (workspace: string): Promise<[Status, WorkspaceLoginInfo | undefined]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl) const accountsUrl = getMetadata(login.metadata.AccountsUrl)
@ -225,7 +272,7 @@ export async function selectWorkspace (workspace: string): Promise<[Status, Work
if (overrideToken !== undefined) { if (overrideToken !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token: overrideToken, endpoint, email, workspace }] return [OK, { token: overrideToken, endpoint, email, workspace, confirmed: true }]
} }
} }
@ -306,7 +353,7 @@ export async function checkJoined (inviteId: string): Promise<[Status, Workspace
if (overrideToken !== undefined) { if (overrideToken !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token: overrideToken, endpoint, email, workspace: DEV_WORKSPACE }] return [OK, { token: overrideToken, endpoint, email, workspace: DEV_WORKSPACE, confirmed: true }]
} }
} }
@ -390,7 +437,7 @@ export async function join (
if (token !== undefined) { if (token !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE }] return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE, confirmed: true }]
} }
} }
@ -431,7 +478,7 @@ export async function signUpJoin (
if (token !== undefined) { if (token !== undefined) {
const endpoint = getMetadata(login.metadata.OverrideEndpoint) const endpoint = getMetadata(login.metadata.OverrideEndpoint)
if (endpoint !== undefined) { if (endpoint !== undefined) {
return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE }] return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE, confirmed: true }]
} }
} }
@ -617,6 +664,40 @@ export async function requestPassword (email: string): Promise<Status> {
} }
} }
export async function confirm (email: string): Promise<[Status, LoginInfo | undefined]> {
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, { token: overrideToken, endpoint, email, confirmed: true }]
}
}
const request = {
method: 'confirm',
params: [email]
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
const result = await response.json()
return [result.error ?? OK, result.result]
} catch (err) {
return [unknownError(err), undefined]
}
}
export async function restorePassword (token: string, password: string): Promise<[Status, LoginInfo | undefined]> { export async function restorePassword (token: string, password: string): Promise<[Status, LoginInfo | undefined]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl) const accountsUrl = getMetadata(login.metadata.AccountsUrl)

View File

@ -42,6 +42,7 @@ export interface WorkspaceLoginInfo extends LoginInfo {
export interface LoginInfo { export interface LoginInfo {
token: string token: string
endpoint: string endpoint: string
confirmed: boolean
email: string email: string
} }

View File

@ -112,6 +112,7 @@ export interface Account {
workspaces: ObjectId[] workspaces: ObjectId[]
// Defined for server admins only // Defined for server admins only
admin?: boolean admin?: boolean
confirmed?: boolean
} }
/** /**
@ -222,6 +223,15 @@ async function getAccountInfo (db: Db, email: string, password: string): Promise
return toAccountInfo(account) return toAccountInfo(account)
} }
async function getAccountInfoByToken (db: Db, productId: string, token: string): Promise<AccountInfo> {
const { email } = decodeToken(token)
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
return toAccountInfo(account)
}
/** /**
* @public * @public
* @param db - * @param db -
@ -236,19 +246,22 @@ export async function login (db: Db, productId: string, email: string, password:
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId('', productId), getAdminExtra(info)) confirmed: info.confirmed ?? true,
token: generateToken(email, getWorkspaceId('', productId), getExtra(info))
} }
return result return result
} }
/** /**
* Will add admin=='true' in case of user is server admin * Will add extra props
*/ */
function getAdminExtra ( function getExtra (info: Account | AccountInfo | null, rec?: Record<string, any>): Record<string, any> | undefined {
info: Account | AccountInfo | null, const res = rec ?? {}
rec?: Record<string, string> if (info?.admin === true) {
): Record<string, string> | undefined { res.admin = 'true'
return info?.admin === true ? { ...rec, admin: 'true' } : rec }
res.confirmed = info?.confirmed ?? true
return res
} }
/** /**
@ -270,7 +283,7 @@ export async function selectWorkspace (
return { return {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId(workspace, productId), getAdminExtra(accountInfo)), token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
workspace, workspace,
productId productId
} }
@ -286,7 +299,7 @@ export async function selectWorkspace (
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId(workspace, productId), getAdminExtra(accountInfo)), token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(accountInfo)),
workspace, workspace,
productId productId
} }
@ -349,6 +362,80 @@ export async function join (
return result return result
} }
/**
* @public
*/
export async function confirmEmail (db: Db, email: string): Promise<Account> {
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: accountId }))
}
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { confirmed: true } })
account.confirmed = true
return account
}
/**
* @public
*/
export async function confirm (db: Db, productId: string, token: string): Promise<LoginInfo> {
const decode = decodeToken(token)
const email = decode.extra?.confirm
if (email === undefined) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: accountId }))
}
const account = await confirmEmail(db, email)
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, getWorkspaceId('', productId), getExtra(account))
}
return result
}
async function sendConfirmation (productId: string, account: Account): Promise<void> {
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(
'@confirm',
getWorkspaceId('', productId),
getExtra(account, {
confirm: account.email
})
)
const link = concatLink(front, `/login/confirm?id=${token}`)
const text = `To confirm your email, please paste the following link in your web browser's address bar: ${link}. If you did not make this request, please ignore this email.`
const html = `<p>To confirm your email, please click the link below: <a href=${link}>Confirm Your Email</a></p><p>
If the link above does not work, paste the following link in your web browser's address bar: ${link}
</p><p>If you did not make this request, please ignore this email.</p>`
const subject = 'Confirm Your Email'
const to = account.email
await fetch(concatLink(sesURL, '/send'), {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
html,
subject,
to
})
})
}
/** /**
* @public * @public
*/ */
@ -363,7 +450,7 @@ export async function signUpJoin (
): Promise<WorkspaceLoginInfo> { ): Promise<WorkspaceLoginInfo> {
const invite = await getInvite(db, inviteId) const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email) const workspace = await checkInvite(invite, email)
await createAccount(db, productId, email, password, first, last) await createAcc(db, productId, email, password, first, last, invite?.emailMask === email)
await assignWorkspace(db, productId, email, workspace.name) await assignWorkspace(db, productId, email, workspace.name)
const token = (await login(db, productId, email, password)).token const token = (await login(db, productId, email, password)).token
@ -372,17 +459,15 @@ export async function signUpJoin (
return result return result
} }
/** async function createAcc (
* @public
*/
export async function createAccount (
db: Db, db: Db,
productId: string, productId: string,
email: string, email: string,
password: string, password: string,
first: string, first: string,
last: string last: string,
): Promise<LoginInfo> { confirmed: boolean = false
): Promise<Account> {
const salt = randomBytes(32) const salt = randomBytes(32)
const hash = hashWithSalt(password, salt) const hash = hashWithSalt(password, salt)
@ -402,13 +487,37 @@ export async function createAccount (
salt, salt,
first, first,
last, last,
confirmed,
workspaces: [] workspaces: []
}) })
const newAccount = await getAccount(db, email)
if (newAccount === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountAlreadyExists, { account: email }))
}
if (!confirmed) {
await sendConfirmation(productId, newAccount)
}
return newAccount
}
/**
* @public
*/
export async function createAccount (
db: Db,
productId: string,
email: string,
password: string,
first: string,
last: string
): Promise<LoginInfo> {
const account = await createAcc(db, productId, email, password, first, last, false)
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId('', productId), getAdminExtra(account)) token: generateToken(email, getWorkspaceId('', productId), getExtra(account))
} }
return result return result
} }
@ -493,7 +602,10 @@ export async function upgradeWorkspace (
export const createUserWorkspace = export const createUserWorkspace =
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) => (version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => { async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
const { email } = decodeToken(token) const { email, extra } = decodeToken(token)
if (extra?.confirmed === false) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '') await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
await assignWorkspace(db, productId, email, workspace) await assignWorkspace(db, productId, email, workspace)
await setRole(email, workspace, productId, AccountRole.Owner) await setRole(email, workspace, productId, AccountRole.Owner)
@ -501,7 +613,7 @@ export const createUserWorkspace =
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
token: generateToken(email, getWorkspaceId(workspace, productId), getAdminExtra(info)), token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)),
productId productId
} }
return result return result
@ -736,7 +848,7 @@ export async function requestPassword (db: Db, productId: string, email: string)
const token = generateToken( const token = generateToken(
'@restore', '@restore',
getWorkspaceId('', productId), getWorkspaceId('', productId),
getAdminExtra(account, { getExtra(account, {
restore: email restore: email
}) })
) )
@ -1069,7 +1181,9 @@ export function getMethods (
changePassword: wrap(changePassword), changePassword: wrap(changePassword),
requestPassword: wrap(requestPassword), requestPassword: wrap(requestPassword),
restorePassword: wrap(restorePassword), restorePassword: wrap(restorePassword),
sendInvite: wrap(sendInvite) sendInvite: wrap(sendInvite),
confirm: wrap(confirm),
getAccountInfoByToken: wrap(getAccountInfoByToken)
// updateAccount: wrap(updateAccount) // updateAccount: wrap(updateAccount)
} }
} }

View File

@ -9,7 +9,7 @@ import serverPlugin from './plugin'
export interface Token { export interface Token {
email: string email: string
workspace: WorkspaceId workspace: WorkspaceId
extra?: Record<string, string> extra?: Record<string, any>
} }
const getSecret = (): string => { const getSecret = (): string => {

View File

@ -11,6 +11,7 @@ export SERVER_SECRET=secret
node ../dev/tool/bundle.js create-workspace sanity-ws -o SanityTest node ../dev/tool/bundle.js create-workspace sanity-ws -o SanityTest
# Create user record in accounts # Create user record in accounts
node ../dev/tool/bundle.js create-account user1 -f John -l Appleseed -p 1234 node ../dev/tool/bundle.js create-account user1 -f John -l Appleseed -p 1234
node ../dev/tool/bundle.js confirm-email user1
# Restore workspace contents in mongo/elastic # Restore workspace contents in mongo/elastic

View File

@ -8,5 +8,6 @@ docker-compose -p sanity up -d --force-recreate --renew-anon-volumes
./tool.sh create-workspace sanity-ws -o SanityTest ./tool.sh create-workspace sanity-ws -o SanityTest
# Create user record in accounts # Create user record in accounts
./tool.sh create-account user1 -f John -l Appleseed -p 1234 ./tool.sh create-account user1 -f John -l Appleseed -p 1234
./tool.sh confirm-email user1
./restore-workspace.sh ./restore-workspace.sh