mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 05:07:43 +02:00
Add login and signup by code (#6097)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "A message has been sent to your email containing a link to confirm your address.",
|
||||
"ConfirmationSent2": "Please follow the link to complete your sign up.",
|
||||
"Slogan": "A unique place to manage all of your work\nWelcome to the Platform",
|
||||
"ContinueWith": "Continue with {provider}"
|
||||
"ContinueWith": "Continue with {provider}",
|
||||
"HaventReceivedCode": "Haven't received the code?",
|
||||
"ResendCode": "Resend code",
|
||||
"WrongEmail": "Wrong email?",
|
||||
"ChangeEmail": "Change email",
|
||||
"SentTo": "We've sent a code to ",
|
||||
"CanFindCode": "Can't find your code? Check your spam folder.",
|
||||
"LoginWithPassword": "Login with password",
|
||||
"LoginWithCode": "Login with code",
|
||||
"FillInProfile": "Fill in your profile",
|
||||
"SetUpPassword": "Set up your password",
|
||||
"Next": "Next",
|
||||
"Skip": "Skip",
|
||||
"SignUpCompleted": "Sign up completed",
|
||||
"StartUsingHuly": "Start using Huly"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "Se ha enviado un mensaje a su correo electrónico con un enlace para confirmar su dirección.",
|
||||
"ConfirmationSent2": "Por favor, siga el enlace para completar su registro.",
|
||||
"Slogan": "Un lugar único para gestionar todo tu trabajo\nBienvenido/a a la Plataforma",
|
||||
"ContinueWith": "Continuar com {provider}"
|
||||
"ContinueWith": "Continuar com {provider}",
|
||||
"HaventReceivedCode": "¿No has recibido el código?",
|
||||
"ResendCode": "Reenviar código",
|
||||
"WrongEmail": "¿Correo electrónico incorrecto?",
|
||||
"ChangeEmail": "Cambiar correo electrónico",
|
||||
"SentTo": "Hemos enviado un código a ",
|
||||
"CanFindCode": "¿No encuentras tu código? Revisa tu",
|
||||
"LoginWithPassword": "Iniciar sesión con contraseña",
|
||||
"LoginWithCode": "Iniciar sesión con código",
|
||||
"FillInProfile": "Rellena tu perfil",
|
||||
"SetUpPassword": "Establecer tu contraseña",
|
||||
"Next": "Siguiente",
|
||||
"Skip": "Saltar",
|
||||
"SignUpCompleted": "Registro completado",
|
||||
"StartUsingHuly": "Comienza a usar Huly"
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "Un message a été envoyé à votre adresse e-mail contenant un lien pour confirmer votre adresse.",
|
||||
"ConfirmationSent2": "Veuillez suivre le lien pour compléter votre inscription.",
|
||||
"Slogan": "Un lieu unique pour gérer tout votre travail\nBienvenue sur la plateforme",
|
||||
"ContinueWith": "Continuer avec {provider}"
|
||||
"ContinueWith": "Continuer avec {provider}",
|
||||
"HaventReceivedCode": "Vous n'avez pas reçu le code ?",
|
||||
"ResendCode": "Renvoyer le code",
|
||||
"WrongEmail": "Mauvaise adresse e-mail ?",
|
||||
"ChangeEmail": "Changer l'adresse e-mail",
|
||||
"SentTo": "Nous avons envoyé un code à ",
|
||||
"CanFindCode": "Vous ne trouvez pas votre code ? Vérifiez votre",
|
||||
"LoginWithPassword": "Connexion avec mot de passe",
|
||||
"LoginWithCode": "Connexion avec code",
|
||||
"FillInProfile": "Remplissez votre profil",
|
||||
"SetUpPassword": "Définir votre mot de passe",
|
||||
"Next": "Suivant",
|
||||
"Skip": "Passer",
|
||||
"SignUpCompleted": "Inscription terminée",
|
||||
"StartUsingHuly": "Commencez à utiliser Huly"
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "Foi enviada uma mensagem para o seu email contendo um link para confirmar o seu endereço.",
|
||||
"ConfirmationSent2": "Por favor, siga o link para concluir o seu registo.",
|
||||
"Slogan": "Um local único para gerir todo o seu trabalho\nBem-vindo à Plataforma",
|
||||
"ContinueWith": "Continuar com {provider}"
|
||||
"ContinueWith": "Continuar com {provider}",
|
||||
"HaventReceivedCode": "Não recebeu o código?",
|
||||
"ResendCode": "Reenviar código",
|
||||
"WrongEmail": "Email errado?",
|
||||
"ChangeEmail": "Alterar email",
|
||||
"SentTo": "Enviamos um código para ",
|
||||
"CanFindCode": "Não encontra o seu código? Verifique a sua pasta de spam.",
|
||||
"LoginWithPassword": "Iniciar sessão com palavra-passe",
|
||||
"LoginWithCode": "Iniciar sessão com código",
|
||||
"FillInProfile": "Preencha o seu perfil",
|
||||
"SetUpPassword": "Definir a sua palavra-passe",
|
||||
"Next": "Seguinte",
|
||||
"Skip": "Saltar",
|
||||
"SignUpCompleted": "Registo concluído",
|
||||
"StartUsingHuly": "Começar a usar Huly"
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "На Вашу почту отправлено сообщение, c ссылкой для подтверждения email.",
|
||||
"ConfirmationSent2": "Пожалуйста, перейдите по ссылке для завершения регистрации.",
|
||||
"Slogan": "Уникальное место для организации всей вашей работы\nДобро пожаловать в Платформу",
|
||||
"ContinueWith": "Войти через {provider}"
|
||||
"ContinueWith": "Войти через {provider}",
|
||||
"HaventReceivedCode": "Не получили код?",
|
||||
"ResendCode": "Отправить код повторно",
|
||||
"WrongEmail": "Неверный email?",
|
||||
"ChangeEmail": "Изменить email",
|
||||
"SentTo": "Мы отправили код на ",
|
||||
"CanFindCode": "Не нашли код? Проверьте папку со спамом.",
|
||||
"LoginWithPassword": "Войти с паролем",
|
||||
"LoginWithCode": "Войти с кодом",
|
||||
"FillInProfile": "Заполните профиль",
|
||||
"SetUpPassword": "Установите пароль",
|
||||
"Next": "Дальше",
|
||||
"Skip": "Пропустить",
|
||||
"SignUpCompleted": "Регистрация завершена",
|
||||
"StartUsingHuly": "Начать использовать Huly"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,20 @@
|
||||
"ConfirmationSent": "已发送确认邮件到您的邮箱,包含一个确认地址的链接。",
|
||||
"ConfirmationSent2": "请点击链接完成注册。",
|
||||
"Slogan": "一个独特的地方来管理你所有的工作\n欢迎来到平台",
|
||||
"ContinueWith": "继续使用 {provider}"
|
||||
"ContinueWith": "继续使用 {provider}",
|
||||
"HaventReceivedCode": "没有收到验证码?",
|
||||
"ResendCode": "重新发送验证码",
|
||||
"WrongEmail": "错误的电子邮件?",
|
||||
"ChangeEmail": "更改电子邮件",
|
||||
"SentTo": "我们已发送验证码到 ",
|
||||
"CanFindCode": "找不到验证码?请检查您的垃圾邮件文件夹。",
|
||||
"LoginWithPassword": "使用密码登录",
|
||||
"LoginWithCode": "使用代码登录",
|
||||
"FillInProfile": "填写您的个人资料",
|
||||
"SetUpPassword": "设置您的密码",
|
||||
"Next": "下一个",
|
||||
"Skip": "跳过",
|
||||
"SignUpCompleted": "注册完成",
|
||||
"StartUsingHuly": "开始使用 Huly"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Loading, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import { onMount } from 'svelte'
|
||||
import login from '../plugin'
|
||||
import { afterConfirm, getSessionLoginInfo, navigateToWorkspace } from '../utils'
|
||||
import { afterConfirm, getSessionLoginInfo, goTo, navigateToWorkspace } from '../utils'
|
||||
|
||||
onMount(async () => {
|
||||
const result = await getSessionLoginInfo()
|
||||
@@ -18,7 +18,9 @@
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
await afterConfirm()
|
||||
await afterConfirm('onboard')
|
||||
} else {
|
||||
goTo('login')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { NavLink } from '@hcengineering/presentation'
|
||||
|
||||
import { getHref } from '../utils'
|
||||
import { BottomAction } from '../index'
|
||||
|
||||
export let action: BottomAction
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if action.caption}
|
||||
<span><Label label={action.caption} /></span>
|
||||
{/if}
|
||||
{#if action.page}
|
||||
<NavLink href={getHref(action.page)}><Label label={action.i18n} /></NavLink>
|
||||
{:else}
|
||||
<a href="." on:click|preventDefault={action.func}><Label label={action.i18n} /></a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
color: var(--theme-darker-color);
|
||||
}
|
||||
a {
|
||||
font-weight: 400;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
</style>
|
||||
@@ -14,14 +14,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Status, Severity, OK, setMetadata } from '@hcengineering/platform'
|
||||
import { Status, Severity, OK } from '@hcengineering/platform'
|
||||
|
||||
import Form from './Form.svelte'
|
||||
import { createWorkspace, getAccount, goTo } from '../utils'
|
||||
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import { createWorkspace, getAccount, goTo, setLoginInfo } from '../utils'
|
||||
import { getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import login from '../plugin'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { onMount } from 'svelte'
|
||||
import { LoginInfo } from '@hcengineering/login'
|
||||
|
||||
@@ -61,13 +60,8 @@
|
||||
status = loginStatus
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
setLoginInfo(result)
|
||||
|
||||
navigate({ path: [workbenchId, result.workspace] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,23 +16,16 @@
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { OK, Severity, Status, translate } from '@hcengineering/platform'
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
StylishEdit,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
getCurrentLocation,
|
||||
navigate,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { Button, Label, StylishEdit, deviceOptionsStore as deviceInfo, themeStore } from '@hcengineering/ui'
|
||||
import StatusControl from './StatusControl.svelte'
|
||||
|
||||
import { NavLink } from '@hcengineering/presentation'
|
||||
import { onMount } from 'svelte'
|
||||
import { BottomAction, getHref } from '..'
|
||||
import { BottomAction } from '..'
|
||||
import login from '../plugin'
|
||||
import { makeSequential } from '../mutex'
|
||||
import Providers from './Providers.svelte'
|
||||
import Tabs from './Tabs.svelte'
|
||||
import BottomActionComponent from './BottomAction.svelte'
|
||||
|
||||
interface Field {
|
||||
id?: string
|
||||
@@ -122,12 +115,7 @@
|
||||
object[field] = (object[field] as string).trim()
|
||||
}
|
||||
|
||||
const goTab = (path: string) => {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = path
|
||||
loc.path.length = 2
|
||||
navigate(loc)
|
||||
}
|
||||
let loginState: 'login' | 'signup' | 'none' = 'none'
|
||||
$: loginState = caption === login.string.LogIn ? 'login' : caption === login.string.SignUp ? 'signup' : 'none'
|
||||
</script>
|
||||
|
||||
@@ -150,28 +138,7 @@
|
||||
}}
|
||||
>
|
||||
{#if loginState !== 'none'}
|
||||
<div class="flex-row-center caption">
|
||||
<a
|
||||
class="title"
|
||||
class:selected={loginState === 'signup'}
|
||||
href="."
|
||||
on:click|preventDefault={() => {
|
||||
if (loginState !== 'signup') goTab('signup')
|
||||
}}
|
||||
>
|
||||
<Label label={login.string.SignUp} />
|
||||
</a>
|
||||
<a
|
||||
class="title"
|
||||
class:selected={loginState === 'login'}
|
||||
href="."
|
||||
on:click|preventDefault={() => {
|
||||
if (loginState !== 'login') goTab('login')
|
||||
}}
|
||||
>
|
||||
<Label label={login.string.LogIn} />
|
||||
</a>
|
||||
</div>
|
||||
<Tabs {loginState} />
|
||||
{:else}
|
||||
{#if subtitle !== undefined}
|
||||
<div class="fs-title">
|
||||
@@ -234,14 +201,7 @@
|
||||
{#if bottomActions.length}
|
||||
<div class="footer">
|
||||
{#each bottomActions as action}
|
||||
<div>
|
||||
<span><Label label={action.caption} /></span>
|
||||
{#if action.page}
|
||||
<NavLink href={getHref(action.page)}><Label label={action.i18n} /></NavLink>
|
||||
{:else}
|
||||
<a href="." on:click|preventDefault={action.func}><Label label={action.i18n} /></a>
|
||||
{/if}
|
||||
</div>
|
||||
<BottomActionComponent {action} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -258,28 +218,6 @@
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.caption a {
|
||||
padding-bottom: 0.375rem;
|
||||
border-bottom: 2px solid var(--theme-caption-color);
|
||||
|
||||
&:not(.selected) {
|
||||
color: var(--theme-dark-color);
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
cursor: default;
|
||||
}
|
||||
&:first-child {
|
||||
margin-right: 1.75rem;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.status {
|
||||
padding-top: 1rem;
|
||||
grid-column-start: 1;
|
||||
|
||||
@@ -13,20 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||
import {
|
||||
fetchMetadataLocalStorage,
|
||||
getCurrentLocation,
|
||||
Location,
|
||||
navigate,
|
||||
setMetadataLocalStorage
|
||||
} from '@hcengineering/ui'
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { Location, getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
|
||||
import { checkJoined, join, signUpJoin } from '../utils'
|
||||
import { checkJoined, join, setLoginInfo, signUpJoin } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { onMount } from 'svelte'
|
||||
import { BottomAction } from '..'
|
||||
@@ -84,13 +77,7 @@
|
||||
status = loginStatus
|
||||
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
setLoginInfo(result)
|
||||
|
||||
if (location.query?.navigateUrl != null) {
|
||||
try {
|
||||
@@ -137,13 +124,7 @@
|
||||
const [, result] = await checkJoined(location.query.inviteId)
|
||||
status = OK
|
||||
if (result !== undefined) {
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
setLoginInfo(result)
|
||||
|
||||
if (location.query?.navigateUrl != null) {
|
||||
try {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
import loginBackWebp from '../../img/login_back.webp'
|
||||
import loginBack2xWebp from '../../img/login_back_2x.webp'
|
||||
import login from '../plugin'
|
||||
import OnboardForm from './OnboardForm.svelte'
|
||||
|
||||
export let page: Pages = 'signup'
|
||||
|
||||
@@ -80,6 +81,10 @@
|
||||
}
|
||||
|
||||
async function chooseToken (): Promise<void> {
|
||||
if (page === 'auth') {
|
||||
// token handled by auth page
|
||||
return
|
||||
}
|
||||
if (getMetadata(presentation.metadata.Token) == null) {
|
||||
const lastToken = fetchMetadataLocalStorage(login.metadata.LastToken)
|
||||
if (lastToken != null) {
|
||||
@@ -133,6 +138,8 @@
|
||||
<LoginForm {navigateUrl} />
|
||||
{:else if page === 'signup'}
|
||||
<SignupForm />
|
||||
{:else if page === 'onboard'}
|
||||
<OnboardForm />
|
||||
{:else if page === 'createWorkspace'}
|
||||
<CreateWorkspaceForm />
|
||||
{:else if page === 'password'}
|
||||
|
||||
@@ -14,95 +14,49 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, setMetadata, Severity, Status } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, Location, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
|
||||
import { doLogin, getWorkspaces, navigateToWorkspace, selectWorkspace } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
|
||||
import { LoginInfo } from '@hcengineering/login'
|
||||
import { recoveryAction } from '../actions'
|
||||
import { type BottomAction, LoginMethods } from '../index'
|
||||
import LoginPasswordForm from './LoginPasswordForm.svelte'
|
||||
import LoginOtpForm from './LoginOtpForm.svelte'
|
||||
import BottomActionComponent from './BottomAction.svelte'
|
||||
import login from '../plugin'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
|
||||
const fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email },
|
||||
{
|
||||
id: 'current-password',
|
||||
name: 'password',
|
||||
i18n: login.string.Password,
|
||||
password: true
|
||||
}
|
||||
]
|
||||
let method: LoginMethods = LoginMethods.Otp
|
||||
|
||||
const object = {
|
||||
username: '',
|
||||
password: ''
|
||||
function changeMethod (event: CustomEvent<LoginMethods>): void {
|
||||
method = event.detail
|
||||
}
|
||||
|
||||
async function doLoginNavigate (
|
||||
result: LoginInfo | undefined,
|
||||
updateStatus: (status: Status<any>) => void
|
||||
): Promise<void> {
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
|
||||
if (navigateUrl !== undefined) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(navigateUrl)) as Location
|
||||
const workspace = loc.path[1]
|
||||
if (workspace !== undefined) {
|
||||
const workspaces = await getWorkspaces()
|
||||
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
||||
updateStatus(new Status(Severity.INFO, login.status.ConnectingToServer, {}))
|
||||
|
||||
const [loginStatus, result] = await selectWorkspace(workspace)
|
||||
updateStatus(loginStatus)
|
||||
navigateToWorkspace(workspace, result, navigateUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = result.confirmed ? 'selectWorkspace' : 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
if (navigateUrl !== undefined) {
|
||||
loc.query = { ...loc.query, navigateUrl }
|
||||
}
|
||||
navigate(loc)
|
||||
const loginWithPasswordAction: BottomAction = {
|
||||
i18n: login.string.LoginWithPassword,
|
||||
func: () => {
|
||||
method = LoginMethods.Password
|
||||
}
|
||||
}
|
||||
|
||||
let status = OK
|
||||
|
||||
const action = {
|
||||
i18n: login.string.LogIn,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
const [loginStatus, result] = await doLogin(object.username, object.password)
|
||||
status = loginStatus
|
||||
await doLoginNavigate(result, (st) => {
|
||||
status = st
|
||||
})
|
||||
const loginWithCodeAction: BottomAction = {
|
||||
i18n: login.string.LoginWithCode,
|
||||
func: () => {
|
||||
method = LoginMethods.Otp
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.LogIn}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
bottomActions={[recoveryAction]}
|
||||
ignoreInitialValidation
|
||||
withProviders
|
||||
/>
|
||||
{#if method === LoginMethods.Otp}
|
||||
<LoginOtpForm {navigateUrl} on:change={changeMethod} />
|
||||
<div class="action">
|
||||
<BottomActionComponent action={loginWithPasswordAction} />
|
||||
</div>
|
||||
{:else}
|
||||
<LoginPasswordForm {navigateUrl} on:change={changeMethod} />
|
||||
<div class="action">
|
||||
<BottomActionComponent action={loginWithCodeAction} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.action {
|
||||
margin-left: 5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 OtpForm from './OtpForm.svelte'
|
||||
import login from '../plugin'
|
||||
import Form from './Form.svelte'
|
||||
import { OtpLoginSteps, sendOtp } from '../index'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
|
||||
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
|
||||
const formData = {
|
||||
username: '' as string
|
||||
}
|
||||
|
||||
let status = OK
|
||||
let step = OtpLoginSteps.Email
|
||||
let otpRetryOn = 0
|
||||
|
||||
const action = {
|
||||
i18n: login.string.LogIn,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
const [otpStatus, result] = await sendOtp(formData.username)
|
||||
status = otpStatus
|
||||
|
||||
if (result !== undefined && result.sent && otpStatus === OK) {
|
||||
step = OtpLoginSteps.Otp
|
||||
otpRetryOn = result.retryOn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleStep (event: CustomEvent<OtpLoginSteps>): void {
|
||||
step = event.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if step === OtpLoginSteps.Email}
|
||||
<Form
|
||||
caption={login.string.LogIn}
|
||||
{status}
|
||||
{fields}
|
||||
object={formData}
|
||||
{action}
|
||||
ignoreInitialValidation
|
||||
withProviders
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if step === OtpLoginSteps.Otp && formData.username !== ''}
|
||||
<OtpForm email={formData.username} {navigateUrl} retryOn={otpRetryOn} on:step={handleStep} />
|
||||
{/if}
|
||||
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { doLogin, doLoginNavigate } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
import { recoveryAction } from '../actions'
|
||||
import login from '../plugin'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
|
||||
const fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email },
|
||||
{
|
||||
id: 'current-password',
|
||||
name: 'password',
|
||||
i18n: login.string.Password,
|
||||
password: true
|
||||
}
|
||||
]
|
||||
|
||||
const object = {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
|
||||
let status = OK
|
||||
|
||||
const action = {
|
||||
i18n: login.string.LogIn,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
const [loginStatus, result] = await doLogin(object.username, object.password)
|
||||
status = loginStatus
|
||||
await doLoginNavigate(
|
||||
result,
|
||||
(st) => {
|
||||
status = st
|
||||
},
|
||||
navigateUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.LogIn}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
bottomActions={[recoveryAction]}
|
||||
ignoreInitialValidation
|
||||
withProviders
|
||||
/>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { LoginInfo, WorkspaceLoginInfo } from '@hcengineering/login'
|
||||
import { OK } from '@hcengineering/platform'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import { OnboardSteps } from '../index'
|
||||
import login from '../plugin'
|
||||
import { ensureConfirmed, getAccount, getWorkspaces, goTo, navigateToWorkspace } from '../utils'
|
||||
|
||||
import Form from './Form.svelte'
|
||||
import OnboardUserForm from './OnboardUserForm.svelte'
|
||||
import OnboardWorkspaceForm from './OnboardWorkspaceForm.svelte'
|
||||
|
||||
const steps = Object.values(OnboardSteps)
|
||||
|
||||
let account: LoginInfo | undefined = undefined
|
||||
|
||||
let step: OnboardSteps = OnboardSteps.Workspace
|
||||
|
||||
function handleStepEvent (event: CustomEvent<LoginInfo | WorkspaceLoginInfo | undefined>): void {
|
||||
if (event.detail != null) {
|
||||
account = event.detail
|
||||
}
|
||||
handleNext(step)
|
||||
}
|
||||
|
||||
function handleNext (current: OnboardSteps): void {
|
||||
const next = steps.indexOf(current) + 1
|
||||
if (next < steps.length) {
|
||||
step = steps[next]
|
||||
}
|
||||
}
|
||||
|
||||
function isWorkspaceLoginInfo (info: WorkspaceLoginInfo | LoginInfo): info is WorkspaceLoginInfo {
|
||||
return (info as WorkspaceLoginInfo).workspace !== undefined
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
account = await getAccount()
|
||||
if (account != null) {
|
||||
await ensureConfirmed(account)
|
||||
} else {
|
||||
goTo('login')
|
||||
}
|
||||
|
||||
const workspaces = await getWorkspaces()
|
||||
if (workspaces.length > 0) {
|
||||
goTo('selectWorkspace')
|
||||
}
|
||||
})
|
||||
|
||||
const action = {
|
||||
i18n: login.string.StartUsingHuly,
|
||||
func: async () => {
|
||||
if (account !== undefined && isWorkspaceLoginInfo(account)) {
|
||||
navigateToWorkspace(account.workspace, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if account}
|
||||
{#if step === OnboardSteps.Workspace}
|
||||
<OnboardWorkspaceForm {account} on:step={handleStepEvent} />
|
||||
{:else if step === OnboardSteps.User}
|
||||
<OnboardUserForm {account} on:step={handleStepEvent} />
|
||||
{:else if step === OnboardSteps.Finish}
|
||||
<Form
|
||||
status={OK}
|
||||
caption={login.string.SignUpCompleted}
|
||||
subtitle={account.email}
|
||||
fields={[]}
|
||||
object={{}}
|
||||
{action}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { LoginInfo } from '@hcengineering/login'
|
||||
import { OK, Severity, Status, unknownError } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import login from '../plugin'
|
||||
import { changeUsername } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
|
||||
export let account: LoginInfo
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const fields = [
|
||||
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
|
||||
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true }
|
||||
]
|
||||
|
||||
const object = { first: '', last: '' }
|
||||
|
||||
let status: Status<any> = OK
|
||||
|
||||
const action = {
|
||||
i18n: login.string.Next,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
try {
|
||||
await changeUsername(object.first, object.last)
|
||||
status = OK
|
||||
dispatch('step')
|
||||
} catch (err) {
|
||||
status = unknownError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.FillInProfile}
|
||||
subtitle={account.email}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
bottomActions={[
|
||||
{
|
||||
i18n: login.string.Skip,
|
||||
func: () => {
|
||||
dispatch('step')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { LoginInfo } from '@hcengineering/login'
|
||||
import { Status, Severity, OK } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import Form from './Form.svelte'
|
||||
import { createWorkspace, setLoginInfo } from '../utils'
|
||||
import login from '../plugin'
|
||||
|
||||
export let account: LoginInfo
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const fields = [{ id: 'workspace', name: 'workspace', i18n: login.string.Workspace }]
|
||||
|
||||
const object = {
|
||||
workspace: ''
|
||||
}
|
||||
|
||||
let status: Status<any> = OK
|
||||
|
||||
const action = {
|
||||
i18n: login.string.CreateWorkspace,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
const [loginStatus, result] = await createWorkspace(object.workspace)
|
||||
status = loginStatus
|
||||
|
||||
if (result !== undefined) {
|
||||
setLoginInfo(result)
|
||||
dispatch('step', result)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form caption={login.string.CreateWorkspace} subtitle={account.email} {status} {fields} {object} {action} />
|
||||
@@ -0,0 +1,326 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { deviceOptionsStore as deviceInfo, Label, TimeLeft, CodeInput } from '@hcengineering/ui'
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
|
||||
import Tabs from './Tabs.svelte'
|
||||
import { BottomAction, doLoginNavigate, loginWithOtp, OtpLoginSteps, sendOtp } from '../index'
|
||||
import login from '../plugin'
|
||||
import BottomActionComponent from './BottomAction.svelte'
|
||||
import StatusControl from './StatusControl.svelte'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
export let email: string
|
||||
export let retryOn: Timestamp
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const fields = [
|
||||
{ id: 'otp1', name: 'otp1', optional: false },
|
||||
{ id: 'otp2', name: 'otp2', optional: false },
|
||||
{ id: 'otp3', name: 'otp3', optional: false },
|
||||
{ id: 'otp4', name: 'otp4', optional: false },
|
||||
{ id: 'otp5', name: 'otp5', optional: false },
|
||||
{ id: 'otp6', name: 'otp6', optional: false }
|
||||
]
|
||||
|
||||
let status = OK
|
||||
|
||||
const otpData: Record<string, string> = {
|
||||
otp1: '',
|
||||
otp2: '',
|
||||
otp3: '',
|
||||
otp4: '',
|
||||
otp5: '',
|
||||
otp6: ''
|
||||
}
|
||||
|
||||
let formElement: HTMLFormElement | undefined
|
||||
|
||||
function trim (field: string): void {
|
||||
otpData[field] = otpData[field].trim()
|
||||
}
|
||||
|
||||
async function validateOtp (): Promise<void> {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
const otp = otpData.otp1 + otpData.otp2 + otpData.otp3 + otpData.otp4 + otpData.otp5 + otpData.otp6
|
||||
const [loginStatus, result] = await loginWithOtp(email, otp)
|
||||
status = loginStatus
|
||||
|
||||
await doLoginNavigate(
|
||||
result,
|
||||
(st) => {
|
||||
status = st
|
||||
},
|
||||
navigateUrl
|
||||
)
|
||||
}
|
||||
|
||||
function onInput (e: Event): void {
|
||||
if (e.target == null) return
|
||||
|
||||
const target = e.target as HTMLInputElement
|
||||
const { value } = target
|
||||
|
||||
if (value == null || value === '') return
|
||||
|
||||
const index = fields.findIndex(({ id }) => id === target.id)
|
||||
if (index === -1) return
|
||||
|
||||
if (Object.values(otpData).every((v) => v !== '')) {
|
||||
void validateOtp()
|
||||
return
|
||||
}
|
||||
|
||||
const nextField = fields[index + 1]
|
||||
if (nextField === undefined) return
|
||||
|
||||
const nextInput = formElement?.querySelector(`input[name="${nextField.name}"]`) as HTMLInputElement
|
||||
if (nextInput != null) {
|
||||
nextInput.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function clearOtpData (): void {
|
||||
Object.keys(otpData).forEach((key) => {
|
||||
otpData[key] = ''
|
||||
})
|
||||
|
||||
const input = formElement?.querySelector(`input[name="${fields[0].name}"]`) as HTMLInputElement
|
||||
if (input != null) {
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown (e: KeyboardEvent): void {
|
||||
const key = e.key.toLowerCase()
|
||||
const target = e.target as HTMLInputElement
|
||||
|
||||
if (key !== 'backspace' && key !== 'delete') return
|
||||
|
||||
const index = fields.findIndex(({ id }) => id === target.id)
|
||||
if (index === -1) return
|
||||
const { value } = target
|
||||
|
||||
status = OK
|
||||
target.value = ''
|
||||
otpData[fields[index].name] = ''
|
||||
|
||||
if (value === '') {
|
||||
const prevField = fields[index - 1]
|
||||
if (prevField === undefined) return
|
||||
const prevInput = formElement?.querySelector(`input[name="${prevField.name}"]`) as HTMLInputElement
|
||||
if (prevInput != null) {
|
||||
prevInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPaste (e: ClipboardEvent): void {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.clipboardData == null) return
|
||||
|
||||
const text = e.clipboardData.getData('text')
|
||||
const digits = text.split('').filter((it) => it !== ' ')
|
||||
|
||||
if (digits.length !== fields.length) return
|
||||
|
||||
let focusName: string | undefined = undefined
|
||||
|
||||
for (const field of fields) {
|
||||
const digit = digits.shift()
|
||||
if (digit === undefined) break
|
||||
otpData[field.name] = digit
|
||||
focusName = field.name
|
||||
}
|
||||
|
||||
if (focusName !== undefined && focusName !== '' && formElement) {
|
||||
const input = formElement.querySelector(`input[name="${focusName}"]`) as HTMLInputElement | undefined
|
||||
input?.focus()
|
||||
}
|
||||
|
||||
if (Object.values(otpData).every((v) => v !== '')) {
|
||||
void validateOtp()
|
||||
}
|
||||
}
|
||||
|
||||
$: if (formElement != null) {
|
||||
formElement.addEventListener('input', onInput)
|
||||
formElement.addEventListener('keydown', onKeydown)
|
||||
formElement.addEventListener('paste', onPaste)
|
||||
const firstInput = formElement.querySelector(`input[name="${fields[0].name}"]`) as HTMLInputElement | undefined
|
||||
firstInput?.focus()
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (formElement !== undefined) {
|
||||
formElement.removeEventListener('input', onInput)
|
||||
formElement.removeEventListener('keydown', onKeydown)
|
||||
formElement.removeEventListener('paste', onPaste)
|
||||
}
|
||||
})
|
||||
|
||||
async function resendCode (): Promise<void> {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
const [otpStatus, result] = await sendOtp(email)
|
||||
status = otpStatus
|
||||
|
||||
if (result !== undefined && result.sent && otpStatus === OK) {
|
||||
retryOn = result.retryOn
|
||||
clearOtpData()
|
||||
if (timer !== undefined) timer.restart(retryOn)
|
||||
canResend = false
|
||||
}
|
||||
}
|
||||
let canResend = false
|
||||
|
||||
const changeEmailAction: BottomAction = {
|
||||
caption: login.string.WrongEmail,
|
||||
i18n: login.string.ChangeEmail,
|
||||
func: () => {
|
||||
dispatch('step', OtpLoginSteps.Email)
|
||||
}
|
||||
}
|
||||
|
||||
const resendCodeAction: BottomAction = {
|
||||
caption: login.string.HaventReceivedCode,
|
||||
i18n: login.string.ResendCode,
|
||||
func: () => {
|
||||
void resendCode()
|
||||
}
|
||||
}
|
||||
|
||||
let timer: TimeLeft | undefined
|
||||
</script>
|
||||
|
||||
<form
|
||||
bind:this={formElement}
|
||||
class="container"
|
||||
style:padding={$deviceInfo.docWidth <= 480 ? '.25rem 1.25rem' : '4rem 5rem'}
|
||||
style:min-height={$deviceInfo.docHeight > 720 ? '42rem' : '0'}
|
||||
>
|
||||
<div class="header">
|
||||
<Tabs loginState="login" />
|
||||
<div class="description">
|
||||
<Label label={login.string.SentTo} />
|
||||
<span class="email ml-1">
|
||||
{email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form">
|
||||
{#each fields as field, index (field.name)}
|
||||
{#if index === 3}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
<div class={'form-row'}>
|
||||
<CodeInput
|
||||
id={field.id}
|
||||
name={field.name}
|
||||
size="medium"
|
||||
bind:value={otpData[field.name]}
|
||||
on:blur={() => {
|
||||
trim(field.name)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="footer mt-6">
|
||||
<span class="flex-row-top">
|
||||
<Label label={login.string.CanFindCode} />
|
||||
<span class="time ml-2">
|
||||
<TimeLeft
|
||||
bind:this={timer}
|
||||
time={retryOn}
|
||||
on:timeout={() => {
|
||||
canResend = true
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<BottomActionComponent action={changeEmailAction} />
|
||||
{#if canResend}
|
||||
<BottomActionComponent action={resendCodeAction} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="status mt-2">
|
||||
<StatusControl {status} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.separator {
|
||||
display: block;
|
||||
width: 0.75rem;
|
||||
height: 1px;
|
||||
background-color: var(--theme-button-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
color: var(--theme-darker-color);
|
||||
}
|
||||
|
||||
.email {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 2.375rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-height: 4.625rem;
|
||||
color: var(--theme-darker-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { getCurrentLocation, Label, navigate } from '@hcengineering/ui'
|
||||
|
||||
import login from '../plugin'
|
||||
|
||||
export let loginState: 'login' | 'signup' | 'none' = 'none'
|
||||
|
||||
const goTab = (path: string) => {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = path
|
||||
loc.path.length = 2
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center caption">
|
||||
<a
|
||||
class="title"
|
||||
class:selected={loginState === 'signup'}
|
||||
href="."
|
||||
on:click|preventDefault={() => {
|
||||
if (loginState !== 'signup') goTab('signup')
|
||||
}}
|
||||
>
|
||||
<Label label={login.string.SignUp} />
|
||||
</a>
|
||||
<a
|
||||
class="title"
|
||||
class:selected={loginState === 'login'}
|
||||
href="."
|
||||
on:click|preventDefault={() => {
|
||||
if (loginState !== 'login') goTab('login')
|
||||
}}
|
||||
>
|
||||
<Label label={login.string.LogIn} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.caption a {
|
||||
padding-bottom: 0.375rem;
|
||||
border-bottom: 2px solid var(--theme-caption-color);
|
||||
|
||||
&:not(.selected) {
|
||||
color: var(--theme-dark-color);
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
cursor: default;
|
||||
}
|
||||
&:first-child {
|
||||
margin-right: 1.75rem;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -61,16 +61,33 @@ export const pages = [
|
||||
'join',
|
||||
'confirm',
|
||||
'confirmationSend',
|
||||
'auth'
|
||||
'auth',
|
||||
'login-password',
|
||||
'onboard'
|
||||
] as const
|
||||
|
||||
export enum OnboardSteps {
|
||||
Workspace = 'workspace',
|
||||
User = 'user',
|
||||
Finish = 'finish'
|
||||
}
|
||||
|
||||
export enum OtpLoginSteps {
|
||||
Email = 'email',
|
||||
Otp = 'otp'
|
||||
}
|
||||
|
||||
export enum LoginMethods {
|
||||
Password = 'password',
|
||||
Otp = 'otp'
|
||||
}
|
||||
export type Pages = (typeof pages)[number]
|
||||
|
||||
export interface BottomAction {
|
||||
i18n: IntlString
|
||||
page?: Pages
|
||||
func: () => void
|
||||
caption: IntlString
|
||||
caption?: IntlString
|
||||
}
|
||||
|
||||
export * from './utils'
|
||||
|
||||
@@ -59,6 +59,20 @@ export default mergeIds(loginId, login, {
|
||||
ConfirmationSent: '' as IntlString,
|
||||
ConfirmationSent2: '' as IntlString,
|
||||
Slogan: '' as IntlString,
|
||||
ContinueWith: '' as IntlString
|
||||
ContinueWith: '' as IntlString,
|
||||
HaventReceivedCode: '' as IntlString,
|
||||
ResendCode: '' as IntlString,
|
||||
WrongEmail: '' as IntlString,
|
||||
ChangeEmail: '' as IntlString,
|
||||
SentTo: '' as IntlString,
|
||||
CanFindCode: '' as IntlString,
|
||||
LoginWithCode: '' as IntlString,
|
||||
LoginWithPassword: '' as IntlString,
|
||||
FillInProfile: '' as IntlString,
|
||||
SetUpPassword: '' as IntlString,
|
||||
Next: '' as IntlString,
|
||||
Skip: '' as IntlString,
|
||||
SignUpCompleted: '' as IntlString,
|
||||
StartUsingHuly: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
//
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { AccountRole, concatLink, type Doc, type Ref } from '@hcengineering/core'
|
||||
import login, { loginId, type LoginInfo, type Workspace, type WorkspaceLoginInfo } from '@hcengineering/login'
|
||||
import { AccountRole, type Doc, type Ref, concatLink } from '@hcengineering/core'
|
||||
import { loginId, type LoginInfo, type OtpInfo, type Workspace, type WorkspaceLoginInfo } from '@hcengineering/login'
|
||||
import {
|
||||
OK,
|
||||
PlatformError,
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
translate,
|
||||
unknownError,
|
||||
unknownStatus,
|
||||
type Status
|
||||
Status,
|
||||
Severity
|
||||
} from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import {
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
type Location
|
||||
} from '@hcengineering/ui'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
import login from './plugin'
|
||||
import { type Pages } from './index'
|
||||
|
||||
/**
|
||||
@@ -117,6 +120,40 @@ export async function signUp (
|
||||
}
|
||||
}
|
||||
|
||||
export async function signUpOtp (email: string): Promise<[Status, OtpInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
}
|
||||
|
||||
const request = {
|
||||
method: 'signUpOtp',
|
||||
params: [email]
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(accountsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
const result = await response.json()
|
||||
if (result.error == null) {
|
||||
Analytics.handleEvent('signUpOtp')
|
||||
Analytics.setUser(email)
|
||||
} else {
|
||||
await handleStatusError('Sign up error', result.error)
|
||||
}
|
||||
return [result.error ?? OK, result.result]
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
return [unknownError(err), undefined]
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWorkspace (
|
||||
workspaceName: string
|
||||
): Promise<[Status, (LoginInfo & { workspace: string }) | undefined]> {
|
||||
@@ -376,6 +413,9 @@ export function setLoginInfo (loginInfo: WorkspaceLoginInfo): void {
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[loginInfo.workspace] = loginInfo.token
|
||||
|
||||
setMetadata(presentation.metadata.Token, loginInfo.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, loginInfo.token)
|
||||
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, loginInfo.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, loginInfo.email)
|
||||
@@ -627,6 +667,36 @@ export async function changePassword (oldPassword: string, password: string): Pr
|
||||
}
|
||||
}
|
||||
|
||||
export async function changeUsername (first: string, last: string): Promise<void> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
}
|
||||
|
||||
const token = getMetadata(presentation.metadata.Token) as string
|
||||
|
||||
const request = {
|
||||
method: 'changeUsername',
|
||||
params: [first, last]
|
||||
}
|
||||
|
||||
const response = await fetch(accountsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
const resp = await response.json()
|
||||
if (resp.error !== undefined) {
|
||||
const err = new PlatformError(resp.error)
|
||||
Analytics.handleError(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function leaveWorkspace (email: string): Promise<void> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
@@ -802,20 +872,16 @@ export function getHref (path: Pages): string {
|
||||
return host + url
|
||||
}
|
||||
|
||||
export async function afterConfirm (): Promise<void> {
|
||||
export async function afterConfirm (wspage: 'onboard' | 'createWorkspace' = 'createWorkspace'): Promise<void> {
|
||||
const joinedWS = await getWorkspaces()
|
||||
if (joinedWS.length === 0) {
|
||||
goTo('createWorkspace')
|
||||
goTo(wspage)
|
||||
} else if (joinedWS.length === 1) {
|
||||
const result = (await selectWorkspace(joinedWS[0].workspace, null))[1]
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[result.workspace] = result.token
|
||||
setMetadataLocalStorage(login.metadata.LoginTokens, tokens)
|
||||
setLoginInfo(result)
|
||||
|
||||
navigateToWorkspace(joinedWS[0].workspace, result)
|
||||
}
|
||||
@@ -866,3 +932,124 @@ export async function getProviders (): Promise<string[]> {
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function sendOtp (email: string): Promise<[Status, OtpInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
}
|
||||
|
||||
const request = {
|
||||
method: 'sendOtp',
|
||||
params: [email]
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(accountsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
const result = await response.json()
|
||||
|
||||
if (result.error == null) {
|
||||
Analytics.handleEvent('sendOtp')
|
||||
Analytics.setUser(email)
|
||||
} else {
|
||||
await handleStatusError('Send otp error', result.error)
|
||||
}
|
||||
return [result.error ?? OK, result.result]
|
||||
} catch (err: any) {
|
||||
console.error('Send otp error', err)
|
||||
Analytics.handleError(err)
|
||||
return [unknownError(err), undefined]
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginWithOtp (email: string, otp: string): Promise<[Status, LoginInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
}
|
||||
|
||||
const request = {
|
||||
method: 'validateOtp',
|
||||
params: [email, otp]
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(accountsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
const result = await response.json()
|
||||
|
||||
if (result.error == null) {
|
||||
Analytics.handleEvent('loginWithOtp')
|
||||
Analytics.setUser(email)
|
||||
} else {
|
||||
await handleStatusError('Login with otp error', result.error)
|
||||
}
|
||||
return [result.error ?? OK, result.result]
|
||||
} catch (err: any) {
|
||||
console.error('Login with otp error', err)
|
||||
Analytics.handleError(err)
|
||||
return [unknownError(err), undefined]
|
||||
}
|
||||
}
|
||||
|
||||
export async function doLoginNavigate (
|
||||
result: LoginInfo | undefined,
|
||||
updateStatus: (status: Status) => void,
|
||||
navigateUrl?: string
|
||||
): Promise<void> {
|
||||
if (result !== undefined) {
|
||||
setMetadata(presentation.metadata.Token, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
|
||||
if (navigateUrl !== undefined) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(navigateUrl)) as Location
|
||||
const workspace = loc.path[1]
|
||||
if (workspace !== undefined) {
|
||||
const workspaces = await getWorkspaces()
|
||||
if (workspaces.find((p) => p.workspace === workspace) !== undefined) {
|
||||
updateStatus(new Status(Severity.INFO, login.status.ConnectingToServer, {}))
|
||||
|
||||
const [loginStatus, result] = await selectWorkspace(workspace, undefined)
|
||||
updateStatus(loginStatus)
|
||||
navigateToWorkspace(workspace, result, navigateUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = result.confirmed ? 'selectWorkspace' : 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
if (navigateUrl !== undefined) {
|
||||
loc.query = { ...loc.query, navigateUrl }
|
||||
}
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureConfirmed (account: LoginInfo): Promise<void> {
|
||||
if (!account.confirmed) {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = 'confirmationSend'
|
||||
loc.path.length = 2
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { AccountRole, Doc, Ref } from '@hcengineering/core'
|
||||
import { AccountRole, Doc, Ref, Timestamp } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, Resource, Status } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
@@ -56,6 +56,11 @@ export interface LoginInfo {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface OtpInfo {
|
||||
sent: boolean
|
||||
retryOn: Timestamp
|
||||
}
|
||||
|
||||
export default plugin(loginId, {
|
||||
metadata: {
|
||||
AccountsUrl: '' as Asset,
|
||||
|
||||
Reference in New Issue
Block a user