UBERF-10303: Always sign up with OTP (#8665)

This commit is contained in:
Alexey Zinoviev 2025-04-23 06:33:57 +04:00 committed by GitHub
parent 37c913f0fe
commit 26194a4dbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 116 additions and 73 deletions

View File

@ -251,6 +251,7 @@ export async function configurePlatform (): Promise<void> {
setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL)) setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL))
setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL)
setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL ?? '') setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL ?? '')
setMetadata(presentation.metadata.MailUrl, config.MAIL_URL)
setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL ?? '') setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL ?? '')
setMetadata(presentation.metadata.StatsUrl, config.STATS_URL) setMetadata(presentation.metadata.StatsUrl, config.STATS_URL)

View File

@ -43,6 +43,7 @@ export interface Config {
PUBLIC_SCHEDULE_URL?: string PUBLIC_SCHEDULE_URL?: string
CALDAV_SERVER_URL?: string CALDAV_SERVER_URL?: string
EXPORT_URL?: string EXPORT_URL?: string
MAIL_URL?: string
} }
export interface Branding { export interface Branding {

View File

@ -186,6 +186,7 @@ export interface Config {
PUBLIC_SCHEDULE_URL?: string PUBLIC_SCHEDULE_URL?: string
CALDAV_SERVER_URL?: string CALDAV_SERVER_URL?: string
EXPORT_URL?: string EXPORT_URL?: string
MAIL_URL?: string
} }
export interface Branding { export interface Branding {
@ -428,6 +429,7 @@ export async function configurePlatform() {
setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL)) setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL))
setMetadata(presentation.metadata.StatsUrl, config.STATS_URL) setMetadata(presentation.metadata.StatsUrl, config.STATS_URL)
setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL) setMetadata(presentation.metadata.LinkPreviewUrl, config.LINK_PREVIEW_URL)
setMetadata(presentation.metadata.MailUrl, config.MAIL_URL)
setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL) setMetadata(recorder.metadata.StreamUrl, config.STREAM_URL)
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR) setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR)

View File

@ -61,7 +61,7 @@ export interface AccountClient {
kind?: 'external' | 'internal' | 'byregion', kind?: 'external' | 'internal' | 'byregion',
externalRegions?: string[] externalRegions?: string[]
) => Promise<WorkspaceLoginInfo> ) => Promise<WorkspaceLoginInfo>
validateOtp: (email: string, code: string) => Promise<LoginInfo> validateOtp: (email: string, code: string, password?: string) => Promise<LoginInfo>
loginOtp: (email: string) => Promise<OtpInfo> loginOtp: (email: string) => Promise<OtpInfo>
getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo> getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo>
getLoginWithWorkspaceInfo: () => Promise<LoginInfoWithWorkspaces> getLoginWithWorkspaceInfo: () => Promise<LoginInfoWithWorkspaces>
@ -98,6 +98,9 @@ export interface AccountClient {
getRegionInfo: () => Promise<RegionInfo[]> getRegionInfo: () => Promise<RegionInfo[]>
createWorkspace: (name: string, region?: string) => Promise<WorkspaceLoginInfo> createWorkspace: (name: string, region?: string) => Promise<WorkspaceLoginInfo>
signUpOtp: (email: string, first: string, last: string) => Promise<OtpInfo> signUpOtp: (email: string, first: string, last: string) => Promise<OtpInfo>
/**
* Deprecated. Only to be used for dev setups without mail service.
*/
signUp: (email: string, password: string, first: string, last: string) => Promise<LoginInfo> signUp: (email: string, password: string, first: string, last: string) => Promise<LoginInfo>
login: (email: string, password: string) => Promise<LoginInfo> login: (email: string, password: string) => Promise<LoginInfo>
getPerson: () => Promise<Person> getPerson: () => Promise<Person>
@ -283,10 +286,10 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request) return await this.rpc(request)
} }
async validateOtp (email: string, code: string): Promise<LoginInfo> { async validateOtp (email: string, code: string, password?: string): Promise<LoginInfo> {
const request = { const request = {
method: 'validateOtp' as const, method: 'validateOtp' as const,
params: { email, code } params: { email, code, password }
} }
return await this.rpc(request) return await this.rpc(request)

View File

@ -153,7 +153,8 @@ export default plugin(presentationId, {
PreviewConfig: '' as Metadata<PreviewConfig | undefined>, PreviewConfig: '' as Metadata<PreviewConfig | undefined>,
ClientHook: '' as Metadata<ClientHook>, ClientHook: '' as Metadata<ClientHook>,
SessionId: '' as Metadata<string>, SessionId: '' as Metadata<string>,
StatsUrl: '' as Metadata<string> StatsUrl: '' as Metadata<string>,
MailUrl: '' as Metadata<string>
}, },
status: { status: {
FileTooLarge: '' as StatusCode FileTooLarge: '' as StatusCode

View File

@ -70,6 +70,8 @@
"Hello": "Ahoj {name},", "Hello": "Ahoj {name},",
"ProcessingInvite": "Zpracovávám pozvánku, čekejte prosím...", "ProcessingInvite": "Zpracovávám pozvánku, čekejte prosím...",
"SignToProceed": "Přihlaste se, abyste mohli pokračovat", "SignToProceed": "Přihlaste se, abyste mohli pokračovat",
"Proceed": "Pokračovat" "Proceed": "Pokračovat",
"SetPasswordLater": "Nastavím heslo později",
"SetPasswordNow": "Nastavím heslo nyní"
} }
} }

View File

@ -70,6 +70,8 @@
"Hello": "Hallo {name},", "Hello": "Hallo {name},",
"ProcessingInvite": "Einladung wird bearbeitet, bitte warten...", "ProcessingInvite": "Einladung wird bearbeitet, bitte warten...",
"SignToProceed": "Bitte melden Sie sich an, um fortzufahren", "SignToProceed": "Bitte melden Sie sich an, um fortzufahren",
"Proceed": "Fortfahren" "Proceed": "Fortfahren",
"SetPasswordLater": "Ich werde später ein Passwort festlegen",
"SetPasswordNow": "Ich werde jetzt ein Passwort festlegen"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Hello {name},", "Hello": "Hello {name},",
"ProcessingInvite": "Processing invite, please wait...", "ProcessingInvite": "Processing invite, please wait...",
"SignToProceed": "Please sign in to proceed", "SignToProceed": "Please sign in to proceed",
"Proceed": "Proceed" "Proceed": "Proceed",
"SetPasswordLater": "I'll set a password later",
"SetPasswordNow": "I'll set a password now"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Hola {name},", "Hello": "Hola {name},",
"ProcessingInvite": "Procesando invitación, por favor espere...", "ProcessingInvite": "Procesando invitación, por favor espere...",
"SignToProceed": "Por favor inicie sesión para continuar", "SignToProceed": "Por favor inicie sesión para continuar",
"Proceed": "Continuar" "Proceed": "Continuar",
"SetPasswordLater": "Estableceré una contraseña más tarde",
"SetPasswordNow": "Estableceré una contraseña ahora"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Bonjour {name},", "Hello": "Bonjour {name},",
"ProcessingInvite": "Invitation en cours, veuillez patienter...", "ProcessingInvite": "Invitation en cours, veuillez patienter...",
"SignToProceed": "Veuillez vous connecter pour continuer", "SignToProceed": "Veuillez vous connecter pour continuer",
"Proceed": "Continuer" "Proceed": "Continuer",
"SetPasswordLater": "Je définirai un mot de passe plus tard",
"SetPasswordNow": "Je définirai un mot de passe maintenant"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Ciao {name},", "Hello": "Ciao {name},",
"ProcessingInvite": "Elaborazione invito, attendere prego...", "ProcessingInvite": "Elaborazione invito, attendere prego...",
"SignToProceed": "Accedi per procedere", "SignToProceed": "Accedi per procedere",
"Proceed": "Procedere" "Proceed": "Procedere",
"SetPasswordLater": "Imposterò una password più tardi",
"SetPasswordNow": "Imposterò una password ora"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "{name} さん、こんにちは", "Hello": "{name} さん、こんにちは",
"ProcessingInvite": "招待を処理中です。しばらくお待ちください...", "ProcessingInvite": "招待を処理中です。しばらくお待ちください...",
"SignToProceed": "続行するにはサインインしてください", "SignToProceed": "続行するにはサインインしてください",
"Proceed": "続行" "Proceed": "続行",
"SetPasswordLater": "後でパスワードを設定します",
"SetPasswordNow": "今すぐパスワードを設定します"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Olá {name},", "Hello": "Olá {name},",
"ProcessingInvite": "Processando convite, aguarde...", "ProcessingInvite": "Processando convite, aguarde...",
"SignToProceed": "Por favor, inicie sessão para continuar", "SignToProceed": "Por favor, inicie sessão para continuar",
"Proceed": "Continuar" "Proceed": "Continuar",
"SetPasswordLater": "Vou definir uma senha mais tarde",
"SetPasswordNow": "Vou definir uma senha agora"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "Привет {name},", "Hello": "Привет {name},",
"ProcessingInvite": "Обработка приглашения, пожалуйста, подождите...", "ProcessingInvite": "Обработка приглашения, пожалуйста, подождите...",
"SignToProceed": "Пожалуйста, войдите, чтобы продолжить", "SignToProceed": "Пожалуйста, войдите, чтобы продолжить",
"Proceed": "Продолжить" "Proceed": "Продолжить",
"SetPasswordLater": "Я установлю пароль позже",
"SetPasswordNow": "Я установлю пароль сейчас"
} }
} }

View File

@ -69,6 +69,8 @@
"Hello": "你好 {name},", "Hello": "你好 {name},",
"ProcessingInvite": "处理邀请,请稍候...", "ProcessingInvite": "处理邀请,请稍候...",
"SignToProceed": "请登录以继续", "SignToProceed": "请登录以继续",
"Proceed": "继续" "Proceed": "继续",
"SetPasswordLater": "稍后设置密码",
"SetPasswordNow": "现在设置密码"
} }
} }

View File

@ -15,6 +15,7 @@
export const LoginEvents = { export const LoginEvents = {
SignUpEmail: 'signup.viaEmail', SignUpEmail: 'signup.viaEmail',
SignUpOtp: 'signup.viaOtp',
SignUpGoogle: 'signup.viaGoogle', SignUpGoogle: 'signup.viaGoogle',
SignUpGithub: 'signup.viaGitHub', SignUpGithub: 'signup.viaGitHub',

View File

@ -55,6 +55,7 @@
export let page: Pages = 'signup' export let page: Pages = 'signup'
const signUpDisabled = getMetadata(login.metadata.DisableSignUp) ?? false const signUpDisabled = getMetadata(login.metadata.DisableSignUp) ?? false
const useOTP = getMetadata(presentation.metadata.MailUrl) != null && getMetadata(presentation.metadata.MailUrl) !== ''
let navigateUrl: string | undefined let navigateUrl: string | undefined
onDestroy(location.subscribe(updatePageLoc)) onDestroy(location.subscribe(updatePageLoc))
@ -149,9 +150,9 @@
<Scroller padding={'1rem 0'}> <Scroller padding={'1rem 0'}>
<div class="form-content"> <div class="form-content">
{#if page === 'login'} {#if page === 'login'}
<LoginForm {navigateUrl} {signUpDisabled} /> <LoginForm {navigateUrl} {signUpDisabled} {useOTP} />
{:else if page === 'signup'} {:else if page === 'signup'}
<SignupForm {navigateUrl} {signUpDisabled} /> <SignupForm {navigateUrl} {signUpDisabled} {useOTP} />
{:else if page === 'createWorkspace'} {:else if page === 'createWorkspace'}
<CreateWorkspaceForm /> <CreateWorkspaceForm />
{:else if page === 'password'} {:else if page === 'password'}

View File

@ -25,12 +25,13 @@
export let navigateUrl: string | undefined = undefined export let navigateUrl: string | undefined = undefined
export let signUpDisabled = false export let signUpDisabled = false
export let useOTP = true
export let email: string | undefined = undefined export let email: string | undefined = undefined
export let caption: IntlString | undefined = undefined export let caption: IntlString | undefined = undefined
export let subtitle: string | undefined = undefined export let subtitle: string | undefined = undefined
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
let method: LoginMethods = LoginMethods.Otp let method: LoginMethods = useOTP ? LoginMethods.Otp : LoginMethods.Password
function changeMethod (event: CustomEvent<LoginMethods>): void { function changeMethod (event: CustomEvent<LoginMethods>): void {
method = event.detail method = event.detail

View File

@ -21,7 +21,7 @@
import { LoginInfo } from '@hcengineering/account-client' import { LoginInfo } from '@hcengineering/account-client'
import Tabs from './Tabs.svelte' import Tabs from './Tabs.svelte'
import { BottomAction, doLoginNavigate, validateOtpLogin, OtpLoginSteps, loginOtp } from '../index' import { BottomAction, doLoginNavigate, doValidateOtp, OtpLoginSteps, loginOtp } from '../index'
import login from '../plugin' import login from '../plugin'
import BottomActionComponent from './BottomAction.svelte' import BottomActionComponent from './BottomAction.svelte'
import StatusControl from './StatusControl.svelte' import StatusControl from './StatusControl.svelte'
@ -32,6 +32,7 @@
export let signUpDisabled = false export let signUpDisabled = false
export let loginState: 'login' | 'signup' | 'none' = 'none' export let loginState: 'login' | 'signup' | 'none' = 'none'
export let canChangeEmail = true export let canChangeEmail = true
export let password: string | undefined = undefined
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -66,7 +67,7 @@
status = new Status(Severity.INFO, login.status.ConnectingToServer, {}) status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const otp = otpData.otp1 + otpData.otp2 + otpData.otp3 + otpData.otp4 + otpData.otp5 + otpData.otp6 const otp = otpData.otp1 + otpData.otp2 + otpData.otp3 + otpData.otp4 + otpData.otp5 + otpData.otp6
const [loginStatus, result] = await validateOtpLogin(email, otp) const [loginStatus, result] = await doValidateOtp(loginState === 'signup', email, otp, password)
status = loginStatus status = loginStatus
if (onLogin !== undefined) { if (onLogin !== undefined) {

View File

@ -15,23 +15,24 @@
--> -->
<script lang="ts"> <script lang="ts">
import { OK, Severity, Status } from '@hcengineering/platform' import { OK, Severity, Status } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { logIn } from '@hcengineering/workbench' import { logIn } from '@hcengineering/workbench'
import BottomActionComponent from './BottomAction.svelte' import BottomActionComponent from './BottomAction.svelte'
import login from '../plugin' import login from '../plugin'
import { getPasswordValidationRules } from '../validations' import { getPasswordValidationRules } from '../validations'
import { goTo, signUp } from '../utils' import { goTo } from '../utils'
import Form from './Form.svelte' import Form from './Form.svelte'
import { BottomAction, LoginMethods, OtpLoginSteps, signUpOtp } from '../index' import { OtpLoginSteps, signUp, signUpOtp } from '../index'
import type { Field } from '../types' import type { Field } from '../types'
import OtpForm from './OtpForm.svelte' import OtpForm from './OtpForm.svelte'
export let signUpDisabled = false export let signUpDisabled = false
export let navigateUrl: string | undefined = undefined export let navigateUrl: string | undefined = undefined
export let useOTP = true // False only for dev/tests
let method: LoginMethods = LoginMethods.Otp
let fields: Array<Field> let fields: Array<Field>
let form: Form let form: Form
let withPassword = !useOTP
$: { $: {
fields = [ fields = [
@ -40,7 +41,7 @@
{ id: 'email', name: 'username', i18n: login.string.Email } { id: 'email', name: 'username', i18n: login.string.Email }
] ]
if (method === LoginMethods.Password) { if (withPassword) {
fields.push({ fields.push({
id: 'new-password', id: 'new-password',
name: 'password', name: 'password',
@ -71,9 +72,17 @@
const action = { const action = {
i18n: login.string.SignUp, i18n: login.string.SignUp,
func: async () => { func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {}) if (useOTP) {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
if (method === LoginMethods.Password) { const [otpStatus, result] = await signUpOtp(object.username, object.first, object.last)
status = otpStatus
if (result?.sent === true && otpStatus === OK) {
step = OtpLoginSteps.Otp
otpRetryOn = result.retryOn
}
} else {
const [loginStatus, result] = await signUp(object.username, object.password, object.first, object.last) const [loginStatus, result] = await signUp(object.username, object.password, object.first, object.last)
status = loginStatus status = loginStatus
@ -82,26 +91,17 @@
await logIn(result) await logIn(result)
goTo('confirmationSend') goTo('confirmationSend')
} }
} else {
const [otpStatus, result] = await signUpOtp(object.username, object.first, object.last)
status = otpStatus
if (result?.sent === true && otpStatus === OK) {
step = OtpLoginSteps.Otp
otpRetryOn = result.retryOn
}
} }
} }
} }
let changeMethodAction: BottomAction let withPasswordAction: BottomAction
$: changeMethodAction = { $: withPasswordAction = {
i18n: method === LoginMethods.Password ? login.string.SignUpWithCode : login.string.SignUpWithPassword, i18n: withPassword ? login.string.SetPasswordLater : login.string.SetPasswordNow,
func: () => { func: () => {
method = method === LoginMethods.Password ? LoginMethods.Otp : LoginMethods.Password withPassword = !withPassword
if (method === LoginMethods.Password) { step = OtpLoginSteps.Email
step = OtpLoginSteps.Email
}
setTimeout(() => { setTimeout(() => {
if (form != null) { if (form != null) {
form.invalidate() form.invalidate()
@ -125,17 +125,27 @@
{signUpDisabled} {signUpDisabled}
{navigateUrl} {navigateUrl}
loginState="signup" loginState="signup"
password={object.password}
retryOn={otpRetryOn} retryOn={otpRetryOn}
on:step={handleStep} on:step={handleStep}
/> />
{/if} {/if}
<div class="action"> {#if useOTP}
<BottomActionComponent action={changeMethodAction} /> <div class="action">
</div> <BottomActionComponent action={withPasswordAction} />
</div>
{:else}
<div class="placeholder" />
{/if}
<style lang="scss"> <style lang="scss">
.action { .action {
margin-left: 5rem; margin-left: 5rem;
} }
// TODO: Refactor me please
.placeholder {
height: 1.125rem;
}
</style> </style>

View File

@ -73,6 +73,8 @@ export default mergeIds(loginId, login, {
Hello: '' as IntlString, Hello: '' as IntlString,
ProcessingInvite: '' as IntlString, ProcessingInvite: '' as IntlString,
SignToProceed: '' as IntlString, SignToProceed: '' as IntlString,
Proceed: '' as IntlString Proceed: '' as IntlString,
SetPasswordLater: '' as IntlString,
SetPasswordNow: '' as IntlString
} }
}) })

View File

@ -885,23 +885,29 @@ export async function loginOtp (email: string): Promise<[Status, OtpInfo | null]
} }
} }
export async function validateOtpLogin (email: string, code: string): Promise<[Status, LoginInfo | null]> { export async function doValidateOtp (
isSignUp: boolean,
email: string,
code: string,
password?: string
): Promise<[Status, LoginInfo | null]> {
const telemetryEvent = isSignUp ? LoginEvents.SignUpOtp : LoginEvents.LoginOtp
try { try {
const loginInfo = await getAccountClient(null).validateOtp(email, code) const loginInfo = await getAccountClient(null).validateOtp(email, code, password)
Analytics.handleEvent(LoginEvents.LoginOtp, { email, ok: true }) Analytics.handleEvent(telemetryEvent, { email, ok: true })
Analytics.setUser(email) Analytics.setUser(email)
return [OK, loginInfo] return [OK, loginInfo]
} catch (err: any) { } catch (err: any) {
if (err instanceof PlatformError) { if (err instanceof PlatformError) {
Analytics.handleEvent(LoginEvents.LoginOtp, { email, ok: false }) Analytics.handleEvent(telemetryEvent, { email, ok: false })
await handleStatusError('Login with otp error', err.status) await handleStatusError('Login with otp error', err.status)
return [err.status, null] return [err.status, null]
} else { } else {
console.error('Login with otp error', err) console.error('Login with otp error', err)
Analytics.handleEvent(LoginEvents.LoginOtp, { email, ok: false }) Analytics.handleEvent(telemetryEvent, { email, ok: false })
Analytics.handleError(err) Analytics.handleError(err)
return [unknownError(err), null] return [unknownError(err), null]
} }

View File

@ -24,7 +24,6 @@ export class LoginPage {
} }
async login (email: string, password: string): Promise<void> { async login (email: string, password: string): Promise<void> {
await this.loginWithPassword.click()
await this.inputEmail.fill(email) await this.inputEmail.fill(email)
await this.inputPassword.fill(password) await this.inputPassword.fill(password)
expect(await this.buttonLogin.isEnabled()).toBe(true) expect(await this.buttonLogin.isEnabled()).toBe(true)

View File

@ -10,7 +10,6 @@ export class SignupPage {
readonly inputRepeatNewPassword: Locator readonly inputRepeatNewPassword: Locator
readonly buttonSignUp: Locator readonly buttonSignUp: Locator
readonly textError: Locator readonly textError: Locator
readonly signUpPasswordBtn: Locator
constructor (page: Page) { constructor (page: Page) {
this.page = page this.page = page
@ -21,15 +20,9 @@ export class SignupPage {
this.inputRepeatNewPassword = page.locator('input[name="new-password"]').nth(1) this.inputRepeatNewPassword = page.locator('input[name="new-password"]').nth(1)
this.buttonSignUp = page.locator('div.send button') this.buttonSignUp = page.locator('div.send button')
this.textError = page.locator('div.ERROR > span') this.textError = page.locator('div.ERROR > span')
this.signUpPasswordBtn = page.locator('a', { hasText: 'Sign up with password' })
} }
async signupPwd (userData: UserSignUp): Promise<void> { async signupPwd (userData: UserSignUp): Promise<void> {
const isOtp = await this.signUpPasswordBtn.isVisible()
if (isOtp) {
await this.signUpPasswordBtn.click()
}
await this.inputFirstName.fill(userData.firstName) await this.inputFirstName.fill(userData.firstName)
await this.inputLastName.fill(userData.lastName) await this.inputLastName.fill(userData.lastName)
await this.inputEmail.fill(userData.email) await this.inputEmail.fill(userData.email)

View File

@ -198,6 +198,8 @@ export async function loginOtp (
/** /**
* Given an email, password, first name, and last name, creates a new account and sends a confirmation email. * Given an email, password, first name, and last name, creates a new account and sends a confirmation email.
* The email confirmation is not required if the email service is not configured. * The email confirmation is not required if the email service is not configured.
*
* ---------DEPRECATED. Only to be used for dev setups without mail service. Use signUpOtp instead.
*/ */
export async function signUp ( export async function signUp (
ctx: MeasureContext, ctx: MeasureContext,
@ -286,9 +288,10 @@ export async function validateOtp (
params: { params: {
email: string email: string
code: string code: string
password?: string
} }
): Promise<LoginInfo> { ): Promise<LoginInfo> {
const { email, code } = params const { email, code, password } = params
// Note: can support OTP based on any other social logins later // Note: can support OTP based on any other social logins later
const normalizedEmail = cleanEmail(email) const normalizedEmail = cleanEmail(email)
@ -317,6 +320,9 @@ export async function validateOtp (
if (account == null) { if (account == null) {
// This is a signup // This is a signup
await createAccount(db, emailSocialId.personUuid, true) await createAccount(db, emailSocialId.personUuid, true)
if (password != null) {
await setPassword(ctx, db, branding, emailSocialId.personUuid as AccountUuid, password)
}
ctx.info('OTP signup success', emailSocialId) ctx.info('OTP signup success', emailSocialId)
} else { } else {

View File

@ -273,6 +273,7 @@ export function start (
pushPublicKey?: string pushPublicKey?: string
disableSignUp?: string disableSignUp?: string
streamUrl?: string streamUrl?: string
mailUrl?: string
}, },
port: number, port: number,
extraConfig?: Record<string, string | undefined> extraConfig?: Record<string, string | undefined>
@ -346,6 +347,7 @@ export function start (
UPLOAD_CONFIG: config.uploadConfig, UPLOAD_CONFIG: config.uploadConfig,
PUSH_PUBLIC_KEY: config.pushPublicKey, PUSH_PUBLIC_KEY: config.pushPublicKey,
DISABLE_SIGNUP: config.disableSignUp, DISABLE_SIGNUP: config.disableSignUp,
MAIL_URL: config.mailUrl,
...(extraConfig ?? {}) ...(extraConfig ?? {})
} }
res.status(200) res.status(200)

View File

@ -119,6 +119,8 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
const disableSignUp = process.env.DISABLE_SIGNUP const disableSignUp = process.env.DISABLE_SIGNUP
const mailUrl = process.env.MAIL_URL
const config = { const config = {
storageAdapter, storageAdapter,
accountsUrl, accountsUrl,
@ -139,7 +141,8 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
pushPublicKey, pushPublicKey,
disableSignUp, disableSignUp,
linkPreviewUrl, linkPreviewUrl,
streamUrl streamUrl,
mailUrl
} }
console.log('Starting Front service with', config) console.log('Starting Front service with', config)
const shutdown = start(ctx, config, SERVER_PORT, extraConfig) const shutdown = start(ctx, config, SERVER_PORT, extraConfig)

View File

@ -38,14 +38,12 @@ test.describe('login test', () => {
test('check if user is able to go to to recovery, then login and then signup', async ({ page }) => { test('check if user is able to go to to recovery, then login and then signup', async ({ page }) => {
await checkIfUrlContains(page, '/login') await checkIfUrlContains(page, '/login')
await loginPage.checkIfLoginButtonIsDisabled() await loginPage.checkIfLoginButtonIsDisabled()
await loginPage.loginWithPassword().click()
await loginPage.checkIfLoginButtonIsDisabled() await loginPage.checkIfLoginButtonIsDisabled()
await loginPage.clickOnRecover() await loginPage.clickOnRecover()
await checkIfUrlContains(page, '/password') await checkIfUrlContains(page, '/password')
await loginPage.checkIfPasswordRecoveryIsVisible() await loginPage.checkIfPasswordRecoveryIsVisible()
await loginPage.clickOnRecoveryLogin() await loginPage.clickOnRecoveryLogin()
await checkIfUrlContains(page, '/login') await checkIfUrlContains(page, '/login')
await loginPage.loginWithPassword().click()
await loginPage.checkIfLoginButtonIsDisabled() await loginPage.checkIfLoginButtonIsDisabled()
await loginPage.clickOnRecover() await loginPage.clickOnRecover()
await loginPage.clickOnRecoverySignUp() await loginPage.clickOnRecoverySignUp()

View File

@ -12,7 +12,6 @@ export class LoginPage {
inputPassword = (): Locator => this.page.locator('input[name=current-password]') inputPassword = (): Locator => this.page.locator('input[name=current-password]')
buttonLogin = (): Locator => this.page.locator('button', { hasText: 'Log In' }) buttonLogin = (): Locator => this.page.locator('button', { hasText: 'Log In' })
loginWithPassword = (): Locator => this.page.locator('a', { hasText: 'Login with password' }) loginWithPassword = (): Locator => this.page.locator('a', { hasText: 'Login with password' })
signUpWithPassword = (): Locator => this.page.locator('a', { hasText: 'Sign up with password' })
linkSignUp = (): Locator => this.page.locator('a.title', { hasText: 'Sign Up' }) linkSignUp = (): Locator => this.page.locator('a.title', { hasText: 'Sign Up' })
invalidCredentialsMessage = (): Locator => invalidCredentialsMessage = (): Locator =>
this.page.getByText('Account not found or the provided credentials are incorrect') this.page.getByText('Account not found or the provided credentials are incorrect')
@ -37,11 +36,8 @@ export class LoginPage {
await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished() await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished()
} }
async clickSignUp (usePassword: boolean = true): Promise<void> { async clickSignUp (): Promise<void> {
await this.linkSignUp().click() await this.linkSignUp().click()
if (usePassword) {
await this.signUpWithPassword().click()
}
} }
async clickOnRecover (): Promise<void> { async clickOnRecover (): Promise<void> {
@ -57,7 +53,6 @@ export class LoginPage {
} }
async login (email: string, password: string): Promise<void> { async login (email: string, password: string): Promise<void> {
await this.loginWithPassword().click()
await this.inputEmail().fill(email) await this.inputEmail().fill(email)
await this.inputPassword().fill(password) await this.inputPassword().fill(password)
expect(await this.buttonLogin().isEnabled()).toBe(true) expect(await this.buttonLogin().isEnabled()).toBe(true)

View File

@ -10,7 +10,6 @@ export class SignUpPage extends CommonPage {
this.page = page this.page = page
} }
signUpPasswordBtn = (): Locator => this.page.locator('a', { hasText: 'Sign up with password' })
inputFirstName = (): Locator => this.page.locator('input[name="given-name"]') inputFirstName = (): Locator => this.page.locator('input[name="given-name"]')
inputLastName = (): Locator => this.page.locator('input[name="family-name"]') inputLastName = (): Locator => this.page.locator('input[name="family-name"]')
inputEmail = (): Locator => this.page.locator('input[name="email"]') inputEmail = (): Locator => this.page.locator('input[name="email"]')

View File

@ -89,9 +89,8 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}` const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto() await loginPage.goto()
await loginPage.clickSignUp(false) await loginPage.clickSignUp()
await signUpPage.signUpPasswordBtn().click()
await signUpPage.checkInfo(page, 'Required field First name') await signUpPage.checkInfo(page, 'Required field First name')
await signUpPage.enterFirstName(newUser.firstName) await signUpPage.enterFirstName(newUser.firstName)
await signUpPage.checkInfo(page, 'Required field Last name') await signUpPage.checkInfo(page, 'Required field Last name')

View File

@ -94,9 +94,8 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}` const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto() await loginPage.goto()
await loginPage.clickSignUp(false) await loginPage.clickSignUp()
await signUpPage.signUpPasswordBtn().click()
await signUpPage.checkInfo(page, 'Required field First name') await signUpPage.checkInfo(page, 'Required field First name')
await signUpPage.enterFirstName(newUser.firstName) await signUpPage.enterFirstName(newUser.firstName)
await signUpPage.checkInfo(page, 'Required field Last name') await signUpPage.checkInfo(page, 'Required field Last name')