mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 13:35:04 +02:00
Add login and signup by code (#6097)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -8,8 +8,9 @@ import account, {
|
||||
UpgradeWorker,
|
||||
accountId,
|
||||
cleanInProgressWorkspaces,
|
||||
getAllTransactors,
|
||||
getMethods
|
||||
getMethods,
|
||||
cleanExpiredOtp,
|
||||
getAllTransactors
|
||||
} from '@hcengineering/account'
|
||||
import accountEn from '@hcengineering/account/lang/en.json'
|
||||
import accountRu from '@hcengineering/account/lang/ru.json'
|
||||
@@ -88,6 +89,8 @@ export function serveAccount (
|
||||
setMetadata(account.metadata.Transactors, transactorUri)
|
||||
setMetadata(platform.metadata.locale, lang)
|
||||
setMetadata(account.metadata.ProductName, productName)
|
||||
setMetadata(account.metadata.OtpTimeToLiveSec, parseInt(process.env.OTP_TIME_TO_LIVE ?? '60'))
|
||||
setMetadata(account.metadata.OtpRetryDelaySec, parseInt(process.env.OTP_RETRY_DELAY ?? '60'))
|
||||
setMetadata(account.metadata.SES_URL, ses)
|
||||
setMetadata(account.metadata.FrontURL, frontURL)
|
||||
|
||||
@@ -105,10 +108,17 @@ export function serveAccount (
|
||||
|
||||
let client: MongoClient | Promise<MongoClient> = MongoClient.connect(dbUri)
|
||||
|
||||
let worker: UpgradeWorker | undefined
|
||||
|
||||
const app = new Koa()
|
||||
const router = new Router()
|
||||
|
||||
let worker: UpgradeWorker | undefined
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true
|
||||
})
|
||||
)
|
||||
app.use(bodyParser())
|
||||
|
||||
void client.then(async (p: MongoClient) => {
|
||||
const db = p.db(ACCOUNT_DB)
|
||||
@@ -117,6 +127,13 @@ export function serveAccount (
|
||||
// We need to clean workspace with creating === true, since server is restarted.
|
||||
void cleanInProgressWorkspaces(db, productId)
|
||||
|
||||
setInterval(
|
||||
() => {
|
||||
void cleanExpiredOtp(db)
|
||||
},
|
||||
3 * 60 * 1000
|
||||
)
|
||||
|
||||
const performUpgrade = (process.env.PERFORM_UPGRADE ?? 'true') === 'true'
|
||||
if (performUpgrade) {
|
||||
await measureCtx.with('upgrade-all-models', {}, async (ctx) => {
|
||||
@@ -241,12 +258,6 @@ export function serveAccount (
|
||||
ctx.body = result
|
||||
})
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true
|
||||
})
|
||||
)
|
||||
app.use(bodyParser())
|
||||
app.use(router.routes()).use(router.allowedMethods())
|
||||
|
||||
const server = app.listen(ACCOUNT_PORT, () => {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "Password recovery",
|
||||
"InviteText": "You were invited to {ws}. To join, please paste the following link into your browser's URL bar: {link}. The link is valid for {expHours} hours.",
|
||||
"InviteHTML": "<p>You were invited to {ws}. To join, please click the link below: <a href={link}>Join</a></p><p>If the invite link above does not work, paste the following link into your browser's URL bar: {link}</p><p>The link is valid for {expHours} hours.</p>",
|
||||
"InviteSubject": "Invitation to {ws}"
|
||||
"InviteSubject": "Invitation to {ws}",
|
||||
"OtpText": "Confirm your email address to access {app}!\n\nYour confirmation code is below - enter it in the window where you've started signing in for {app}.\n\n{code}\n\nIf you didn’t request this email, there’s nothing to worry about — you can safely ignore it.",
|
||||
"OtpHTML": "<h3>Confirm your email address to access {app}!</h3><p>Your confirmation code is below - enter it in the window where you've started signing in for {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>If you didn’t request this email, there’s nothing to worry about — you can safely ignore it.</p>",
|
||||
"OtpSubject": "{app} confirmation code: {code}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "Recuperación de Contraseña",
|
||||
"InviteText": "Te invitaron a {ws}. Para unirte, pega el siguiente enlace en la barra de URL de tu navegador web: {link}. Enlace válido por {expHours} horas.",
|
||||
"InviteHTML": "<p>Te invitaron a {ws}. Para unirte, haz clic en el enlace a continuación: <a href={link}>Unirse</a></p><p>Si el enlace de invitación anterior no funciona, pega el siguiente enlace en la barra de URL de tu navegador web: {link}</p><p>Enlace válido por {expHours} horas.</p>",
|
||||
"InviteSubject": "Invitación a {ws}"
|
||||
"InviteSubject": "Invitación a {ws}",
|
||||
"OtpText": "¡Confirma tu dirección de correo electrónico para acceder a {app}!\n\nTu código de confirmación está a continuación: ingrésalo en la ventana donde iniciaste sesión en {app}.\n\n{code}\n\nSi no solicitaste este correo electrónico, no hay de qué preocuparse; puedes ignorarlo con seguridad.",
|
||||
"OtpHTML": "<h3>¡Confirma tu dirección de correo electrónico para acceder a {app}!</h3><p>Tu código de confirmación está a continuación: ingrésalo en la ventana donde iniciaste sesión en {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Si no solicitaste este correo electrónico, no hay de qué preocuparse; puedes ignorarlo con seguridad.</p>",
|
||||
"OtpSubject": "Código de confirmación de {app}: {code}"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "Réinitialisation du mot de passe",
|
||||
"InviteText": "Vous avez été invité à {ws}. Pour rejoindre, veuillez coller le lien suivant dans la barre d'adresse de votre navigateur web : {link}. Le lien est valable pendant {expHours} heures.",
|
||||
"InviteHTML": "<p>Vous avez été invité à {ws}. Pour rejoindre, veuillez cliquer sur le lien ci-dessous : <a href={link}>Rejoindre</a></p><p>Si le lien d'invitation ci-dessus ne fonctionne pas, collez le lien suivant dans la barre d'adresse de votre navigateur web : {link}</p><p>Le lien est valable pendant {expHours} heures.</p>",
|
||||
"InviteSubject": "Invitation à {ws}"
|
||||
"InviteSubject": "Invitation à {ws}",
|
||||
"OtpText": "Confirmez votre adresse e-mail pour accéder à {app}!\n\nVotre code de confirmation est ci-dessous - entrez-le dans la fenêtre où vous avez commencé à vous connecter pour {app}.\n\n{code}\n\nSi vous n'avez pas demandé cet e-mail, ne vous inquiétez pas - vous pouvez l'ignorer en toute sécurité.",
|
||||
"OtpHTML": "<h3>Confirmez votre adresse e-mail pour accéder à {app}!</h3><p>Votre code de confirmation est ci-dessous - entrez-le dans la fenêtre où vous avez commencé à vous connecter pour {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Si vous n'avez pas demandé cet e-mail, ne vous inquiétez pas - vous pouvez l'ignorer en toute sécurité.</p>",
|
||||
"OtpSubject": "Code de confirmation {app} : {code}"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "Recuperação de Senha",
|
||||
"InviteText": "Foi convidado para {ws}. Para se juntar, cole o seguinte link na barra de URL do seu navegador web: {link}. Link válido por {expHours} horas.",
|
||||
"InviteHTML": "<p>Foi convidado para {ws}. Para se juntar, clique no link abaixo: <a href={link}>Juntar-se</a></p><p>Se o link de convite acima não funcionar, cole o seguinte link na barra de URL do seu navegador web: {link}</p><p>Link válido por {expHours} horas.</p>",
|
||||
"InviteSubject": "Convite para {ws}"
|
||||
"InviteSubject": "Convite para {ws}",
|
||||
"OtpText": "Confirme o seu endereço de email para aceder a {app}!\n\nO seu código de confirmação está abaixo - introduza-o na janela onde começou a iniciar sessão para {app}.\n\n{code}\n\nSe não solicitou este email, não há motivo para preocupação - pode ignorá-lo com segurança.",
|
||||
"OtpHTML": "<h3>Confirme o seu endereço de email para aceder a {app}!</h3><p>O seu código de confirmação está abaixo - introduza-o na janela onde começou a iniciar sessão para {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Se não solicitou este email, não há motivo para preocupação - pode ignorá-lo com segurança.</p>",
|
||||
"OtpSubject": "Código de confirmação {app}: {code}"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "Восстановление пароля",
|
||||
"InviteText": "Вы были приглашены в {ws}. Чтобы присоединиться, пожалуйста, скопируйте ссылку ниже в адресную строку вашего браузера: {link}. Ссылка действительна в течение {expHours} ч.",
|
||||
"InviteHTML": "<p>Вы были приглашены в {ws}. Чтобы присоединиться, пожалуйста, скопируйте ссылку ниже в адресную строку вашего браузера: <a href={link}>Присоединиться</a></p><p>Если ссылка выше не работает, скопируйте следующую ссылку в адресную строку вашего браузера: {link}</p><p>Ссылка действительна в течение {expHours} ч.</p>",
|
||||
"InviteSubjectRU": "Приглашение в {ws}"
|
||||
"InviteSubjectRU": "Приглашение в {ws}",
|
||||
"OtpText": "Подтвердите адрес электронной почты для доступа к {app}!\n\nВаш код подтверждения ниже - введите его в окне, где вы начали входить в {app}.\n\n{code}\n\nЕсли вы не запрашивали это письмо, не беспокойтесь - вы можете спокойно проигнорировать его.",
|
||||
"OtpHTML": "<h3>Подтвердите адрес электронной почты для доступа к {app}!</h3><p>Ваш код подтверждения ниже - введите его в окне, где вы начали входить в {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Если вы не запрашивали это письмо, не беспокойтесь - вы можете спокойно проигнорировать его.</p>",
|
||||
"OtpSubject": "Код подтверждения {app}: {code}"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"RecoverySubject": "密码恢复",
|
||||
"InviteText": "您被邀请加入 {ws}。要加入,请将以下链接粘贴到您的浏览器地址栏中:{link}。链接有效期为 {expHours} 小时。",
|
||||
"InviteHTML": "<p>您被邀请加入 {ws}。要加入,请点击以下链接:<a href={link}>加入</a></p><p>如果上面的邀请链接不起作用,请将以下链接粘贴到您的浏览器地址栏中:{link}</p><p>链接有效期为 {expHours} 小时。</p>",
|
||||
"InviteSubject": "邀请加入 {ws}"
|
||||
"InviteSubject": "邀请加入 {ws}",
|
||||
"OtpText": "确认您的电子邮件地址以访问 {app}!\n\n您的确认码如下 - 请在您开始登录 {app} 的窗口中输入。\n\n{code}\n\n如果您没有请求此邮件,不用担心 — 您可以放心地忽略它。",
|
||||
"OtpHTML": "<h3>确认您的电子邮件地址以访问 {app}!</h3><p>您的确认码如下 - 请在您开始登录 {app} 的窗口中输入。<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>如果您没有请求此邮件,不用担心 — 您可以放心地忽略它。</p>",
|
||||
"OtpSubject": "{app} 确认码:{code}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"@types/node-fetch": "~2.6.2",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5"
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/otp-generator": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongodb": "^6.8.0",
|
||||
@@ -45,6 +46,7 @@
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/client-resources": "^0.6.27",
|
||||
"@hcengineering/client": "^0.6.18",
|
||||
"otp-generator": "^4.0.1",
|
||||
"ws": "^8.18.0",
|
||||
"@hcengineering/model": "^0.6.11",
|
||||
"@hcengineering/server-backup": "^0.6.0",
|
||||
|
||||
@@ -45,6 +45,7 @@ import core, {
|
||||
Version,
|
||||
versionToString,
|
||||
WorkspaceId,
|
||||
Timestamp,
|
||||
WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
} from '@hcengineering/core'
|
||||
@@ -62,6 +63,7 @@ import toolPlugin, {
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto'
|
||||
import { Binary, Db, Filter, ObjectId, type MongoClient } from 'mongodb'
|
||||
import fetch from 'node-fetch'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import {
|
||||
DummyFullTextAdapter,
|
||||
Pipeline,
|
||||
@@ -76,10 +78,12 @@ import {
|
||||
registerServerPlugins,
|
||||
registerStringLoaders
|
||||
} from '@hcengineering/server-pipeline'
|
||||
|
||||
import { accountPlugin } from './plugin'
|
||||
|
||||
const WORKSPACE_COLLECTION = 'workspace'
|
||||
const ACCOUNT_COLLECTION = 'account'
|
||||
const OTP_COLLECTION = 'otp'
|
||||
const INVITE_COLLECTION = 'invite'
|
||||
/**
|
||||
* @public
|
||||
@@ -200,6 +204,18 @@ export interface Workspace extends BaseWorkspaceInfo {
|
||||
transactor?: string // Transactor group name
|
||||
}
|
||||
|
||||
export interface OtpRecord {
|
||||
account: ObjectId
|
||||
otp: string
|
||||
expires: Timestamp
|
||||
createdOn: Timestamp
|
||||
}
|
||||
|
||||
export interface OtpInfo {
|
||||
sent: boolean
|
||||
retryOn: Timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -343,7 +359,36 @@ async function getAccountInfo (
|
||||
return toAccountInfo(account)
|
||||
}
|
||||
|
||||
async function getAccountInfoByToken (
|
||||
async function sendOtpEmail (branding: Branding | null, otp: string, email: string): Promise<void> {
|
||||
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
|
||||
if (sesURL === undefined || sesURL === '') {
|
||||
console.info('Please provide email service url to enable email otp.')
|
||||
return
|
||||
}
|
||||
|
||||
const lang = branding?.language
|
||||
const app = branding?.title ?? getMetadata(accountPlugin.metadata.ProductName)
|
||||
|
||||
const text = await translate(accountPlugin.string.OtpText, { code: otp, app }, lang)
|
||||
const html = await translate(accountPlugin.string.OtpHTML, { code: otp, app }, lang)
|
||||
const subject = await translate(accountPlugin.string.OtpSubject, { code: otp, app }, lang)
|
||||
|
||||
const to = email
|
||||
await fetch(concatLink(sesURL, '/send'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
html,
|
||||
subject,
|
||||
to
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAccountInfoByToken (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
@@ -409,6 +454,117 @@ export async function login (
|
||||
}
|
||||
}
|
||||
|
||||
async function getNewOtp (db: Db): Promise<string> {
|
||||
let otp = otpGenerator.generate(6, {
|
||||
upperCaseAlphabets: false,
|
||||
lowerCaseAlphabets: false,
|
||||
specialChars: false
|
||||
})
|
||||
|
||||
let exist = await db.collection<OtpRecord>(OTP_COLLECTION).findOne({ otp })
|
||||
|
||||
while (exist != null) {
|
||||
otp = otpGenerator.generate(6, {
|
||||
lowerCaseAlphabets: false
|
||||
})
|
||||
exist = await db.collection<OtpRecord>(OTP_COLLECTION).findOne({ otp })
|
||||
}
|
||||
|
||||
return otp
|
||||
}
|
||||
|
||||
export async function sendOtp (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
branding: Branding | null,
|
||||
_email: string
|
||||
): Promise<OtpInfo> {
|
||||
const email = cleanEmail(_email)
|
||||
const account = await getAccount(db, email)
|
||||
|
||||
if (account == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const otpData = (
|
||||
await db
|
||||
.collection<OtpRecord>(OTP_COLLECTION)
|
||||
.find({ account: account._id })
|
||||
.sort({ createdOn: -1 })
|
||||
.limit(1)
|
||||
.toArray()
|
||||
)[0]
|
||||
|
||||
const retryDelay = getMetadata(accountPlugin.metadata.OtpRetryDelaySec) ?? 30
|
||||
const isValid = otpData !== undefined && otpData.expires > now && otpData.createdOn + retryDelay * 1000 > now
|
||||
|
||||
if (isValid) {
|
||||
return { sent: true, retryOn: otpData.createdOn + retryDelay * 1000 }
|
||||
}
|
||||
const secs = getMetadata(accountPlugin.metadata.OtpTimeToLiveSec) ?? 60
|
||||
const timeToLive = secs * 1000
|
||||
const expires = now + timeToLive
|
||||
const otp = await getNewOtp(db)
|
||||
|
||||
await sendOtpEmail(branding, otp, email)
|
||||
await db.collection<OtpRecord>(OTP_COLLECTION).insertOne({ account: account._id, otp, expires, createdOn: now })
|
||||
|
||||
return { sent: true, retryOn: now + retryDelay * 1000 }
|
||||
}
|
||||
|
||||
async function isOtpValid (db: Db, account: Account, otp: string): Promise<boolean> {
|
||||
const now = Date.now()
|
||||
const otpData = (await db.collection<OtpRecord>(OTP_COLLECTION).findOne({ account: account._id, otp })) ?? undefined
|
||||
|
||||
return otpData !== undefined && otpData.expires > now
|
||||
}
|
||||
|
||||
export async function validateOtp (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
branding: Branding | null,
|
||||
_email: string,
|
||||
otp: string
|
||||
): Promise<LoginInfo> {
|
||||
const email = cleanEmail(_email)
|
||||
const account = await getAccount(db, email)
|
||||
|
||||
if (account == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||
}
|
||||
|
||||
const isValid = await isOtpValid(db, account, otp)
|
||||
|
||||
if (!isValid) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidOtp, {}))
|
||||
}
|
||||
|
||||
try {
|
||||
const info = toAccountInfo(account)
|
||||
|
||||
if (account.confirmed !== true) {
|
||||
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { confirmed: true } })
|
||||
}
|
||||
|
||||
const result = {
|
||||
endpoint: '',
|
||||
email,
|
||||
confirmed: true,
|
||||
token: generateToken(email, getWorkspaceId('', productId), getExtra(info))
|
||||
}
|
||||
await db.collection<OtpRecord>(OTP_COLLECTION).deleteMany({ account: account._id })
|
||||
ctx.info('otp login success', { email, productId })
|
||||
return result
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('otp login failed', { email, productId, _email, err })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add extra props
|
||||
*/
|
||||
@@ -824,6 +980,25 @@ export async function createAccount (
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function signUpOtp (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
branding: Branding | null,
|
||||
_email: string
|
||||
): Promise<OtpInfo> {
|
||||
const email = cleanEmail(_email)
|
||||
const first = email.split('@', 1)[0] ?? ''
|
||||
const last = ''
|
||||
|
||||
await createAcc(ctx, db, productId, branding, email, null, first, last, false)
|
||||
|
||||
return await sendOtp(ctx, db, productId, branding, _email)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -894,6 +1069,10 @@ export async function cleanInProgressWorkspaces (db: Db, productId: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanExpiredOtp (db: Db): Promise<void> {
|
||||
await db.collection<OtpRecord>(OTP_COLLECTION).deleteMany({ expires: { $lte: Date.now() } })
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -2425,6 +2604,29 @@ export async function loginWithProvider (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function changeUsername (
|
||||
ctx: MeasureContext,
|
||||
db: Db,
|
||||
productId: string,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
first: string,
|
||||
last: string
|
||||
): Promise<void> {
|
||||
const { email } = decodeToken(token)
|
||||
const account = await getAccount(db, email)
|
||||
|
||||
if (account == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
|
||||
}
|
||||
|
||||
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: account._id }, { $set: { first, last } })
|
||||
ctx.info('change-username success', { email })
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -2436,6 +2638,9 @@ export function getMethods (
|
||||
return {
|
||||
login: wrap(login),
|
||||
join: wrap(join),
|
||||
sendOtp: wrap(sendOtp),
|
||||
validateOtp: wrap(validateOtp),
|
||||
signUpOtp: wrap(signUpOtp),
|
||||
checkJoin: wrap(checkJoin),
|
||||
signUpJoin: wrap(signUpJoin),
|
||||
selectWorkspace: wrap(selectWorkspace),
|
||||
@@ -2455,7 +2660,8 @@ export function getMethods (
|
||||
sendInvite: wrap(sendInvite),
|
||||
confirm: wrap(confirm),
|
||||
getAccountInfoByToken: wrap(getAccountInfoByToken),
|
||||
createMissingEmployee: wrap(createMissingEmployee)
|
||||
createMissingEmployee: wrap(createMissingEmployee),
|
||||
changeUsername: wrap(changeUsername)
|
||||
// updateAccount: wrap(updateAccount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ export const accountPlugin = plugin(accountId, {
|
||||
FrontURL: '' as Metadata<string>,
|
||||
SES_URL: '' as Metadata<string>,
|
||||
ProductName: '' as Metadata<string>,
|
||||
Transactors: '' as Metadata<string>
|
||||
Transactors: '' as Metadata<string>,
|
||||
OtpTimeToLiveSec: '' as Metadata<number>,
|
||||
OtpRetryDelaySec: '' as Metadata<number>
|
||||
},
|
||||
string: {
|
||||
ConfirmationText: '' as IntlString,
|
||||
@@ -24,6 +26,9 @@ export const accountPlugin = plugin(accountId, {
|
||||
RecoverySubject: '' as IntlString,
|
||||
InviteText: '' as IntlString,
|
||||
InviteHTML: '' as IntlString,
|
||||
InviteSubject: '' as IntlString
|
||||
InviteSubject: '' as IntlString,
|
||||
OtpText: '' as IntlString,
|
||||
OtpHTML: '' as IntlString,
|
||||
OtpSubject: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user