mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-12 02:11:57 +00:00
UBERF-9636: Meeting links - more cases (#8369)
Some checks are pending
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Some checks are pending
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
0bbf5773e5
commit
c6326b3c73
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -182,8 +182,8 @@
|
||||
"args": ["src/__start.ts"],
|
||||
"env": {
|
||||
"MONGO_URL": "mongodb://localhost:27017",
|
||||
"DB_URL": "mongodb://localhost:27017",
|
||||
// "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
|
||||
// "DB_URL": "mongodb://localhost:27017",
|
||||
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
|
||||
"SERVER_SECRET": "secret",
|
||||
"REGION_INFO": "|Mongo;cockroach|CockroachDB",
|
||||
"TRANSACTOR_URL": "ws://huly.local:3333,ws://huly.local:3332;;cockroach",
|
||||
|
@ -74,7 +74,7 @@ export interface AccountClient {
|
||||
join: (email: string, password: string, inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
createInvite: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise<string>
|
||||
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
checkAutoJoin: (inviteId: string, firstName: string, lastName?: string) => Promise<WorkspaceLoginInfo>
|
||||
checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise<WorkspaceLoginInfo>
|
||||
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
|
||||
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
|
||||
getRegionInfo: () => Promise<RegionInfo[]>
|
||||
@ -377,7 +377,7 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async checkAutoJoin (inviteId: string, firstName: string, lastName?: string): Promise<WorkspaceLoginInfo> {
|
||||
async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise<WorkspaceLoginInfo> {
|
||||
const request = {
|
||||
method: 'checkAutoJoin' as const,
|
||||
params: { inviteId, firstName, lastName }
|
||||
|
@ -84,6 +84,7 @@
|
||||
background-color: var(--theme-button-focused);
|
||||
border-color: var(--theme-list-divider-color);
|
||||
}
|
||||
|
||||
input {
|
||||
height: 3.25rem;
|
||||
margin: 0;
|
||||
@ -91,7 +92,12 @@
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--theme-button-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
.nolabel {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
@ -66,6 +66,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # velké písmeno} other {Heslo musí obsahovat alespoň # velkých písmen}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # malé písmeno} other {Heslo musí obsahovat alespoň # malých písmen}}",
|
||||
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti.",
|
||||
"RestoreArchivedWorkspace": "Obnovit pracovní prostor."
|
||||
"RestoreArchivedWorkspace": "Obnovit pracovní prostor.",
|
||||
"Hello": "Ahoj {name},",
|
||||
"ProcessingInvite": "Zpracovávám pozvánku, čekejte prosím...",
|
||||
"SignToProceed": "Přihlaste se, abyste mohli pokračovat",
|
||||
"Proceed": "Pokračovat"
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {Das Passwort muss mindestens # Großbuchstaben enthalten} other {Das Passwort muss mindestens # Großbuchstaben enthalten}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {Das Passwort muss mindestens # Kleinbuchstaben enthalten} other {Das Passwort muss mindestens # Kleinbuchstaben enthalten}}",
|
||||
"WorkspaceArchivedDesc": "Workspace wurde wegen Inaktivität archiviert.",
|
||||
"RestoreArchivedWorkspace": "Workspace wiederherstellen."
|
||||
"RestoreArchivedWorkspace": "Workspace wiederherstellen.",
|
||||
"Hello": "Hallo {name},",
|
||||
"ProcessingInvite": "Einladung wird bearbeitet, bitte warten...",
|
||||
"SignToProceed": "Bitte melden Sie sich an, um fortzufahren",
|
||||
"Proceed": "Fortfahren"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {Password must contain at least # uppercase letter} other {Password must contain at least # uppercase letters}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {Password must contain at least # lowercase letter} other {Password must contain at least # lowercase letters}}",
|
||||
"WorkspaceArchivedDesc": "Workspace is archived because of being unused.",
|
||||
"RestoreArchivedWorkspace": "Unarchive"
|
||||
"RestoreArchivedWorkspace": "Unarchive",
|
||||
"Hello": "Hello {name},",
|
||||
"ProcessingInvite": "Processing invite, please wait...",
|
||||
"SignToProceed": "Please sign in to proceed",
|
||||
"Proceed": "Proceed"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {La contraseña debe contener al menos # letra mayúscula} other {La contraseña debe contener al menos # letras mayúsculas}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {La contraseña debe contener al menos # letra minúscula} other {La contraseña debe contener al menos # letras minúsculas}}",
|
||||
"WorkspaceArchivedDesc": "El espacio de trabajo está archivado por no estar en uso.",
|
||||
"RestoreArchivedWorkspace": "Restaurar"
|
||||
"RestoreArchivedWorkspace": "Restaurar",
|
||||
"Hello": "Hola {name},",
|
||||
"ProcessingInvite": "Procesando invitación, por favor espere...",
|
||||
"SignToProceed": "Por favor inicie sesión para continuar",
|
||||
"Proceed": "Continuar"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre majuscule} other {Le mot de passe doit contenir au moins # lettres majuscules}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre minuscule} other {Le mot de passe doit contenir au moins # lettres minuscules}}",
|
||||
"WorkspaceArchivedDesc": "L'espace de travail est archivé en raison de son inactivité.",
|
||||
"RestoreArchivedWorkspace": "Restaurer"
|
||||
"RestoreArchivedWorkspace": "Restaurer",
|
||||
"Hello": "Bonjour {name},",
|
||||
"ProcessingInvite": "Invitation en cours, veuillez patienter...",
|
||||
"SignToProceed": "Veuillez vous connecter pour continuer",
|
||||
"Proceed": "Continuer"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {La password deve contenere almeno # lettera maiuscola} other {La password deve contenere almeno # lettere maiuscole}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {La password deve contenere almeno # lettera minuscola} other {La password deve contenere almeno # lettere minuscole}}",
|
||||
"WorkspaceArchivedDesc": "Il workspace è stato archiviato perché inutilizzato.",
|
||||
"RestoreArchivedWorkspace": "Un archiviato workspace"
|
||||
"RestoreArchivedWorkspace": "Un archiviato workspace",
|
||||
"Hello": "Ciao {name},",
|
||||
"ProcessingInvite": "Elaborazione invito, attendere prego...",
|
||||
"SignToProceed": "Accedi per procedere",
|
||||
"Proceed": "Procedere"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {A senha deve conter pelo menos # letra maiúscula} other {A senha deve conter pelo menos # letras maiúsculas}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {A senha deve conter pelo menos # letra minúscula} other {A senha deve conter pelo menos # letras minúsculas}}",
|
||||
"WorkspaceArchivedDesc": "O espaço de trabalho está arquivado por estar inativo.",
|
||||
"RestoreArchivedWorkspace": "Restaurar"
|
||||
"RestoreArchivedWorkspace": "Restaurar",
|
||||
"Hello": "Olá {name},",
|
||||
"ProcessingInvite": "Processando convite, aguarde...",
|
||||
"SignToProceed": "Por favor, inicie sessão para continuar",
|
||||
"Proceed": "Continuar"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {Пароль должен содержать минимум # заглавную букву} other {Пароль должен содержать минимум # заглавных букв}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {Пароль должен содержать минимум # строчную букву} other {Пароль должен содержать минимум # строчных букв}}",
|
||||
"WorkspaceArchivedDesc": "Рабочее пространство архивировано из-за неиспользования.",
|
||||
"RestoreArchivedWorkspace": "Восстановить"
|
||||
"RestoreArchivedWorkspace": "Восстановить",
|
||||
"Hello": "Привет {name},",
|
||||
"ProcessingInvite": "Обработка приглашения, пожалуйста, подождите...",
|
||||
"SignToProceed": "Пожалуйста, войдите, чтобы продолжить",
|
||||
"Proceed": "Продолжить"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
"PasswordMinUpperChars": "{count, plural, =1 {密码至少需要 # 个大写字母} other {密码至少需要 # 个大写字母}}",
|
||||
"PasswordMinLowerChars": "{count, plural, =1 {密码至少需要 # 个小写字母} other {密码至少需要 # 个小写字母}}",
|
||||
"WorkspaceArchivedDesc": "工作区已被归档,因为未使用。",
|
||||
"RestoreArchivedWorkspace": "解包"
|
||||
"RestoreArchivedWorkspace": "解包",
|
||||
"Hello": "你好 {name},",
|
||||
"ProcessingInvite": "处理邀请,请稍候...",
|
||||
"SignToProceed": "请登录以继续",
|
||||
"Proceed": "继续"
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,22 @@
|
||||
import { logIn } from '@hcengineering/workbench'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import { afterConfirm, getLoginInfoFromQuery, goTo, isWorkspaceLoginInfo, navigateToWorkspace } from '../utils'
|
||||
import {
|
||||
afterConfirm,
|
||||
getLoginInfoFromQuery,
|
||||
getAutoJoinInfo,
|
||||
goTo,
|
||||
isWorkspaceLoginInfo,
|
||||
navigateToWorkspace
|
||||
} from '../utils'
|
||||
|
||||
onMount(async () => {
|
||||
const autoJoinInfo = getAutoJoinInfo()
|
||||
if (autoJoinInfo != null) {
|
||||
goTo('autoJoin')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await getLoginInfoFromQuery()
|
||||
|
||||
if (result != null) {
|
||||
|
@ -15,94 +15,104 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { Location, getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import { logIn, workbenchId } from '@hcengineering/workbench'
|
||||
import { setMetadata, translate } from '@hcengineering/platform'
|
||||
import { Location, Loading, Label, getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import { type LoginInfo } from '@hcengineering/account-client'
|
||||
import { loginId } from '@hcengineering/login'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import Form from './Form.svelte'
|
||||
import { setLoginInfo, checkAutoJoin, isWorkspaceLoginInfo } from '../utils'
|
||||
import { loginAction, recoveryAction } from '../actions'
|
||||
import { setLoginInfo, checkAutoJoin, isWorkspaceLoginInfo, navigateToWorkspace } from '../utils'
|
||||
import login from '../plugin'
|
||||
import LoginForm from './LoginForm.svelte'
|
||||
|
||||
const location = getCurrentLocation()
|
||||
Analytics.handleEvent('auto_join_invite_link_activated')
|
||||
|
||||
const fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email, disabled: true },
|
||||
{
|
||||
id: 'current-password',
|
||||
name: 'password',
|
||||
i18n: login.string.Password,
|
||||
password: true
|
||||
}
|
||||
]
|
||||
|
||||
$: object = {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
|
||||
let status = OK
|
||||
|
||||
$: action = {
|
||||
i18n: login.string.Join,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
await check()
|
||||
}
|
||||
}
|
||||
let loading = true
|
||||
let email: string | undefined = undefined
|
||||
let name = ''
|
||||
let subtitle = ''
|
||||
|
||||
onMount(() => {
|
||||
void check()
|
||||
})
|
||||
|
||||
async function check (): Promise<void> {
|
||||
if (location.query?.inviteId == null || location.query?.firstName == null) return
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
$: void updateSubtitle(name, $themeStore.language)
|
||||
|
||||
const [, result] = await checkAutoJoin(
|
||||
location.query.inviteId,
|
||||
location.query.firstName,
|
||||
location.query.lastName ?? ''
|
||||
)
|
||||
status = OK
|
||||
if (result != null) {
|
||||
if (isWorkspaceLoginInfo(result)) {
|
||||
setLoginInfo(result)
|
||||
|
||||
if (location.query?.navigateUrl != null) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(location.query.navigateUrl)) as Location
|
||||
if (loc.path[1] === result.workspaceUrl) {
|
||||
navigate(loc)
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
navigate({ path: [workbenchId, result.workspaceUrl] })
|
||||
} else {
|
||||
if (result.email == null) {
|
||||
console.error('No email in auto join info')
|
||||
navigate({ path: [loginId, 'login'] })
|
||||
return
|
||||
}
|
||||
|
||||
object.username = result.email
|
||||
}
|
||||
async function updateSubtitle (name: string, language: string): Promise<void> {
|
||||
if (name != null && name !== '') {
|
||||
subtitle = await translate(login.string.Hello, { name }, language)
|
||||
}
|
||||
}
|
||||
|
||||
async function check (): Promise<void> {
|
||||
if (location.query?.inviteId == null || (location.query?.firstName == null && location.query?.token == null)) return
|
||||
|
||||
if (location.query?.token != null) {
|
||||
setMetadata(presentation.metadata.Token, location.query.token)
|
||||
|
||||
delete location.query.token
|
||||
navigate(location, true)
|
||||
}
|
||||
|
||||
try {
|
||||
const [, result] = await checkAutoJoin(
|
||||
location.query.inviteId,
|
||||
location.query.firstName ?? '',
|
||||
location.query.lastName ?? ''
|
||||
)
|
||||
|
||||
if (result != null) {
|
||||
if (isWorkspaceLoginInfo(result)) {
|
||||
await logIn(result)
|
||||
navigateToWorkspace(result.workspaceUrl, result, location.query?.navigateUrl)
|
||||
return
|
||||
} else {
|
||||
if (result.email == null) {
|
||||
console.error('No email in auto join info')
|
||||
navigate({ path: [loginId, 'login'] })
|
||||
return
|
||||
}
|
||||
|
||||
email = result.email
|
||||
name = result.name
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to check auto join', err)
|
||||
navigate({ path: [loginId, 'login'] })
|
||||
return
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onLogin (loginInfo: LoginInfo | null): Promise<void> {
|
||||
if (loginInfo?.token == null) {
|
||||
return
|
||||
}
|
||||
|
||||
setMetadata(presentation.metadata.Token, loginInfo.token)
|
||||
await check()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.Join}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
bottomActions={[loginAction, recoveryAction]}
|
||||
withProviders
|
||||
/>
|
||||
{#if loading}
|
||||
<div>
|
||||
<div class="title"><Label label={login.string.ProcessingInvite} /></div>
|
||||
<Loading />
|
||||
</div>
|
||||
{:else}
|
||||
<LoginForm signUpDisabled {email} caption={login.string.SignToProceed} {subtitle} {onLogin} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
color: var(--theme-caption-color);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
@ -89,6 +89,9 @@
|
||||
if (page === 'auth') {
|
||||
// token handled by auth page
|
||||
return
|
||||
} else if (page === 'autoJoin') {
|
||||
// there's a separate workflow for auto join
|
||||
return
|
||||
}
|
||||
|
||||
if (getMetadata(presentation.metadata.Token) == null) {
|
||||
|
@ -14,14 +14,21 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type IntlString, type Status } from '@hcengineering/platform'
|
||||
|
||||
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'
|
||||
import { LoginInfo } from '@hcengineering/account-client'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
export let signUpDisabled = false
|
||||
export let email: string | undefined = undefined
|
||||
export let caption: IntlString | undefined = undefined
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
|
||||
|
||||
let method: LoginMethods = LoginMethods.Otp
|
||||
|
||||
@ -45,12 +52,12 @@
|
||||
</script>
|
||||
|
||||
{#if method === LoginMethods.Otp}
|
||||
<LoginOtpForm {navigateUrl} {signUpDisabled} on:change={changeMethod} />
|
||||
<LoginOtpForm {navigateUrl} {signUpDisabled} {email} {caption} {subtitle} {onLogin} on:change={changeMethod} />
|
||||
<div class="action">
|
||||
<BottomActionComponent action={loginWithPasswordAction} />
|
||||
</div>
|
||||
{:else}
|
||||
<LoginPasswordForm {navigateUrl} {signUpDisabled} on:change={changeMethod} />
|
||||
<LoginPasswordForm {navigateUrl} {signUpDisabled} {email} {caption} {subtitle} {onLogin} on:change={changeMethod} />
|
||||
<div class="action">
|
||||
<BottomActionComponent action={loginWithCodeAction} />
|
||||
</div>
|
||||
|
@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { type IntlString, OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { LoginInfo } from '@hcengineering/account-client'
|
||||
|
||||
import OtpForm from './OtpForm.svelte'
|
||||
import login from '../plugin'
|
||||
@ -22,12 +23,22 @@
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
export let signUpDisabled = false
|
||||
export let email: string | undefined = undefined
|
||||
export let caption: IntlString = login.string.LogIn
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
|
||||
|
||||
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
|
||||
$: fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email, disabled: email !== undefined && email !== '' }
|
||||
]
|
||||
const formData = {
|
||||
username: '' as string
|
||||
}
|
||||
|
||||
$: if (email !== undefined && email !== '' && formData.username === '') {
|
||||
formData.username = email
|
||||
}
|
||||
|
||||
let status = OK
|
||||
let step = OtpLoginSteps.Email
|
||||
let otpRetryOn = 0
|
||||
@ -53,7 +64,8 @@
|
||||
|
||||
{#if step === OtpLoginSteps.Email}
|
||||
<Form
|
||||
caption={login.string.LogIn}
|
||||
{caption}
|
||||
{subtitle}
|
||||
{status}
|
||||
{fields}
|
||||
object={formData}
|
||||
@ -65,5 +77,13 @@
|
||||
{/if}
|
||||
|
||||
{#if step === OtpLoginSteps.Otp && formData.username !== ''}
|
||||
<OtpForm email={formData.username} {signUpDisabled} {navigateUrl} retryOn={otpRetryOn} on:step={handleStep} />
|
||||
<OtpForm
|
||||
email={formData.username}
|
||||
{signUpDisabled}
|
||||
{navigateUrl}
|
||||
retryOn={otpRetryOn}
|
||||
{onLogin}
|
||||
canChangeEmail={email === undefined || email === ''}
|
||||
on:step={handleStep}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { type IntlString, OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { type LoginInfo } from '@hcengineering/account-client'
|
||||
|
||||
import { doLogin, doLoginNavigate } from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
@ -22,9 +23,13 @@
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
export let signUpDisabled = false
|
||||
export let email: string | undefined = undefined
|
||||
export let caption: IntlString = login.string.LogIn
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
|
||||
|
||||
const fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email },
|
||||
$: fields = [
|
||||
{ id: 'email', name: 'username', i18n: login.string.Email, disabled: email !== undefined && email !== '' },
|
||||
{
|
||||
id: 'current-password',
|
||||
name: 'password',
|
||||
@ -38,6 +43,10 @@
|
||||
password: ''
|
||||
}
|
||||
|
||||
$: if (email !== undefined && email !== '' && object.username === '') {
|
||||
object.username = email
|
||||
}
|
||||
|
||||
let status = OK
|
||||
|
||||
const action = {
|
||||
@ -46,19 +55,25 @@
|
||||
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
|
||||
)
|
||||
|
||||
if (onLogin !== undefined) {
|
||||
void onLogin(result, status)
|
||||
} else {
|
||||
await doLoginNavigate(
|
||||
result,
|
||||
(st) => {
|
||||
status = st
|
||||
},
|
||||
navigateUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.LogIn}
|
||||
{caption}
|
||||
{subtitle}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
|
@ -18,6 +18,7 @@
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
import { LoginInfo } from '@hcengineering/account-client'
|
||||
|
||||
import Tabs from './Tabs.svelte'
|
||||
import { BottomAction, doLoginNavigate, validateOtpLogin, OtpLoginSteps, loginOtp } from '../index'
|
||||
@ -30,6 +31,8 @@
|
||||
export let retryOn: Timestamp
|
||||
export let signUpDisabled = false
|
||||
export let loginState: 'login' | 'signup' | 'none' = 'none'
|
||||
export let canChangeEmail = true
|
||||
export let onLogin: ((loginInfo: LoginInfo | null, status: Status) => void | Promise<void>) | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -66,13 +69,17 @@
|
||||
const [loginStatus, result] = await validateOtpLogin(email, otp)
|
||||
status = loginStatus
|
||||
|
||||
await doLoginNavigate(
|
||||
result,
|
||||
(st) => {
|
||||
status = st
|
||||
},
|
||||
navigateUrl
|
||||
)
|
||||
if (onLogin !== undefined) {
|
||||
void onLogin(result, status)
|
||||
} else {
|
||||
await doLoginNavigate(
|
||||
result,
|
||||
(st) => {
|
||||
status = st
|
||||
},
|
||||
navigateUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onInput (e: Event): void {
|
||||
@ -276,7 +283,9 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<BottomActionComponent action={changeEmailAction} />
|
||||
{#if canChangeEmail}
|
||||
<BottomActionComponent action={changeEmailAction} />
|
||||
{/if}
|
||||
{#if canResend}
|
||||
<BottomActionComponent action={resendCodeAction} />
|
||||
{/if}
|
||||
|
@ -45,11 +45,20 @@
|
||||
|
||||
function getLink (provider: Provider): string {
|
||||
const inviteId = location.query?.inviteId
|
||||
const autoJoin = location.query?.autoJoin !== undefined
|
||||
const navigateUrl = location.query?.navigateUrl
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl) ?? ''
|
||||
let path = `/auth/${provider.name}`
|
||||
if (inviteId != null) {
|
||||
path += `?inviteId=${inviteId}`
|
||||
if (autoJoin) {
|
||||
path += '&autoJoin'
|
||||
}
|
||||
if (navigateUrl != null) {
|
||||
path += `&navigateUrl=${navigateUrl}`
|
||||
}
|
||||
}
|
||||
|
||||
return concatLink(accountsUrl, path)
|
||||
}
|
||||
</script>
|
||||
|
@ -135,7 +135,6 @@
|
||||
let search: string = ''
|
||||
</script>
|
||||
|
||||
<!-- TODO: show some social login instead of account.account -->
|
||||
<form class="container" style:padding={$deviceInfo.docWidth <= 480 ? '1.25rem' : '5rem'}>
|
||||
<div class="grow-separator" />
|
||||
<div class="fs-title">
|
||||
|
@ -69,6 +69,10 @@ export default mergeIds(loginId, login, {
|
||||
LoginWithCode: '' as IntlString,
|
||||
LoginWithPassword: '' as IntlString,
|
||||
SignUpWithCode: '' as IntlString,
|
||||
SignUpWithPassword: '' as IntlString
|
||||
SignUpWithPassword: '' as IntlString,
|
||||
Hello: '' as IntlString,
|
||||
ProcessingInvite: '' as IntlString,
|
||||
SignToProceed: '' as IntlString,
|
||||
Proceed: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -451,7 +451,7 @@ export function setLoginInfo (loginInfo: WorkspaceLoginInfo): void {
|
||||
export function navigateToWorkspace (
|
||||
workspaceUrl: string,
|
||||
loginInfo: WorkspaceLoginInfo | null,
|
||||
navigateUrl?: string,
|
||||
navigateUrl?: string | null,
|
||||
replace = false
|
||||
): void {
|
||||
if (loginInfo == null) {
|
||||
@ -460,7 +460,7 @@ export function navigateToWorkspace (
|
||||
|
||||
setLoginInfo(loginInfo)
|
||||
|
||||
if (navigateUrl !== undefined) {
|
||||
if (navigateUrl != null) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(navigateUrl)) as Location
|
||||
if (loc.path[1] === workspaceUrl) {
|
||||
@ -512,7 +512,7 @@ export async function checkJoined (inviteId: string): Promise<[Status, Workspace
|
||||
|
||||
export async function checkAutoJoin (
|
||||
inviteId: string,
|
||||
firstName: string,
|
||||
firstName?: string,
|
||||
lastName?: string
|
||||
): Promise<[Status, WorkspaceInviteInfo | WorkspaceLoginInfo | null]> {
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
@ -789,7 +789,7 @@ export async function afterConfirm (clearQuery = false): Promise<void> {
|
||||
|
||||
setLoginInfo(result)
|
||||
|
||||
navigateToWorkspace(joinedWS[0].uuid, result, undefined, clearQuery)
|
||||
navigateToWorkspace(joinedWS[0].url, result, undefined, clearQuery)
|
||||
}
|
||||
} else {
|
||||
goTo('selectWorkspace', clearQuery)
|
||||
@ -812,6 +812,22 @@ export async function getLoginInfo (): Promise<LoginInfo | WorkspaceLoginInfo |
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoJoinInfo (): any {
|
||||
const query = getCurrentLocation().query
|
||||
|
||||
if (query == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { token, autoJoin, inviteId, navigateUrl } = query
|
||||
|
||||
if (token == null || autoJoin === undefined || inviteId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { token, autoJoin, inviteId, navigateUrl }
|
||||
}
|
||||
|
||||
export async function getLoginInfoFromQuery (): Promise<LoginInfo | WorkspaceLoginInfo | null> {
|
||||
const token = getCurrentLocation().query?.token
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { type AccountDB, LoginInfo, joinWithProvider, loginOrSignUpWithProvider } from '@hcengineering/account'
|
||||
import { type AccountDB } from '@hcengineering/account'
|
||||
import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } from '@hcengineering/core'
|
||||
import Router from 'koa-router'
|
||||
import { Strategy as GitHubStrategy } from 'passport-github2'
|
||||
import qs from 'querystringify'
|
||||
import { Passport } from '.'
|
||||
import { getHost, safeParseAuthState } from './utils'
|
||||
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
|
||||
|
||||
export function registerGithub (
|
||||
measureCtx: MeasureContext,
|
||||
@ -37,14 +36,7 @@ export function registerGithub (
|
||||
|
||||
router.get('/auth/github', async (ctx, next) => {
|
||||
measureCtx.info('try auth via', { provider: 'github' })
|
||||
const host = getHost(ctx.request.headers)
|
||||
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
|
||||
const state = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding
|
||||
})
|
||||
)
|
||||
const state = encodeState(ctx, brandings)
|
||||
|
||||
passport.authenticate('github', { scope: ['user:email'], session: true, state })(ctx, next)
|
||||
})
|
||||
@ -61,60 +53,29 @@ export function registerGithub (
|
||||
})(ctx, next)
|
||||
},
|
||||
async (ctx, next) => {
|
||||
try {
|
||||
const email = ctx.state.user.emails?.[0]?.value
|
||||
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [ctx.state.user.username, '']
|
||||
const email = ctx.state.user.emails?.[0]?.value
|
||||
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [ctx.state.user.username, '']
|
||||
const db = await dbPromise
|
||||
|
||||
measureCtx.info('Provider auth handler', { email, type: 'github' })
|
||||
let loginInfo: LoginInfo | null
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
const db = await dbPromise
|
||||
const socialKey = { type: SocialIdType.GITHUB, value: ctx.state.user.username }
|
||||
const redirectUrl = await handleProviderAuth(
|
||||
measureCtx,
|
||||
db,
|
||||
brandings,
|
||||
frontUrl,
|
||||
'github',
|
||||
ctx.query?.state,
|
||||
ctx.state?.user,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
{ type: SocialIdType.GITHUB, value: ctx.state.user.username },
|
||||
signUpDisabled
|
||||
)
|
||||
|
||||
if (state.inviteId != null && state.inviteId !== '') {
|
||||
loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
state.inviteId as any,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
} else {
|
||||
loginInfo = await loginOrSignUpWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
}
|
||||
|
||||
if (loginInfo === null) {
|
||||
measureCtx.info('Failed to auth: no associated account found', {
|
||||
email,
|
||||
type: 'github',
|
||||
user: ctx.state?.user
|
||||
})
|
||||
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
|
||||
} else {
|
||||
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
|
||||
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
|
||||
|
||||
measureCtx.info('Success auth, redirect', { email, type: 'github', target: origin })
|
||||
// Successful authentication, redirect to your application
|
||||
ctx.redirect(`${origin}?${query}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: 'github', user: ctx.state?.user })
|
||||
if (redirectUrl !== '') {
|
||||
ctx.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
||||
)
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { type AccountDB, LoginInfo, joinWithProvider, loginOrSignUpWithProvider } from '@hcengineering/account'
|
||||
import { type AccountDB } from '@hcengineering/account'
|
||||
import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } from '@hcengineering/core'
|
||||
import Router from 'koa-router'
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
|
||||
import qs from 'querystringify'
|
||||
import { Passport } from '.'
|
||||
import { getHost, safeParseAuthState } from './utils'
|
||||
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
|
||||
|
||||
export function registerGoogle (
|
||||
measureCtx: MeasureContext,
|
||||
@ -37,14 +36,7 @@ export function registerGoogle (
|
||||
|
||||
router.get('/auth/google', async (ctx, next) => {
|
||||
measureCtx.info('try auth via', { provider: 'google' })
|
||||
const host = getHost(ctx.request.headers)
|
||||
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
|
||||
const state = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding
|
||||
})
|
||||
)
|
||||
const state = encodeState(ctx, brandings)
|
||||
|
||||
passport.authenticate('google', { scope: ['profile', 'email'], session: true, state })(ctx, next)
|
||||
})
|
||||
@ -68,57 +60,25 @@ export function registerGoogle (
|
||||
const email = ctx.state.user.emails?.[0]?.value
|
||||
const first = ctx.state.user.name.givenName
|
||||
const last = ctx.state.user.name.familyName
|
||||
measureCtx.info('Provider auth handler', { email, type: 'google' })
|
||||
const db = await dbPromise
|
||||
|
||||
try {
|
||||
let loginInfo: LoginInfo | null
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
const db = await dbPromise
|
||||
const socialKey = { type: SocialIdType.GOOGLE, value: email }
|
||||
const redirectUrl = await handleProviderAuth(
|
||||
measureCtx,
|
||||
db,
|
||||
brandings,
|
||||
frontUrl,
|
||||
'google',
|
||||
ctx.query?.state,
|
||||
ctx.state?.user,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
{ type: SocialIdType.GOOGLE, value: email },
|
||||
signUpDisabled
|
||||
)
|
||||
|
||||
if (state.inviteId != null && state.inviteId !== '') {
|
||||
loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
state.inviteId as any,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
} else {
|
||||
loginInfo = await loginOrSignUpWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
}
|
||||
|
||||
if (loginInfo === null) {
|
||||
measureCtx.info('Failed to auth: no associated account found', {
|
||||
email,
|
||||
type: 'google',
|
||||
user: ctx.state?.user
|
||||
})
|
||||
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
|
||||
} else {
|
||||
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
|
||||
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
|
||||
|
||||
// Successful authentication, redirect to your application
|
||||
measureCtx.info('Success auth, redirect', { email, type: 'google', target: origin })
|
||||
ctx.redirect(`${origin}?${query}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: 'google', user: ctx.state?.user })
|
||||
if (redirectUrl !== '') {
|
||||
ctx.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
await next()
|
||||
|
@ -12,14 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { type AccountDB, LoginInfo, joinWithProvider, loginOrSignUpWithProvider } from '@hcengineering/account'
|
||||
import { type AccountDB } from '@hcengineering/account'
|
||||
import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } from '@hcengineering/core'
|
||||
import Router from 'koa-router'
|
||||
import { Issuer, Strategy } from 'openid-client'
|
||||
import qs from 'querystringify'
|
||||
|
||||
import { Passport } from '.'
|
||||
import { getHost, safeParseAuthState } from './utils'
|
||||
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
|
||||
|
||||
export function registerOpenid (
|
||||
measureCtx: MeasureContext,
|
||||
@ -64,14 +63,7 @@ export function registerOpenid (
|
||||
|
||||
router.get('/auth/openid', async (ctx, next) => {
|
||||
measureCtx.info('try auth via', { provider: 'openid' })
|
||||
const host = getHost(ctx.request.headers)
|
||||
const brandingKey = host !== undefined ? brandings[host]?.key ?? undefined : undefined
|
||||
const state = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding: brandingKey
|
||||
})
|
||||
)
|
||||
const state = encodeState(ctx, brandings)
|
||||
|
||||
await passport.authenticate('oidc', {
|
||||
scope: 'openid profile email',
|
||||
@ -90,62 +82,30 @@ export function registerOpenid (
|
||||
})(ctx, next)
|
||||
},
|
||||
async (ctx, next) => {
|
||||
try {
|
||||
const email = ctx.state.user.email
|
||||
const verifiedEmail = (ctx.state.user.email_verified as boolean) ? email : ''
|
||||
const [first, last] = ctx.state.user.name?.split(' ') ?? [ctx.state.user.username, '']
|
||||
measureCtx.info('Provider auth handler', { email, verifiedEmail, type: 'openid' })
|
||||
const email = ctx.state.user.email
|
||||
const verifiedEmail = (ctx.state.user.email_verified as boolean) ? email : ''
|
||||
const [first, last] = ctx.state.user.name?.split(' ') ?? [ctx.state.user.username, '']
|
||||
|
||||
let loginInfo: LoginInfo | null
|
||||
const state = safeParseAuthState(ctx.query?.state)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
const db = await dbPromise
|
||||
const socialKey = { type: SocialIdType.OIDC, value: ctx.state.user.sub }
|
||||
const db = await dbPromise
|
||||
const redirectUrl = await handleProviderAuth(
|
||||
measureCtx,
|
||||
db,
|
||||
brandings,
|
||||
frontUrl,
|
||||
'openid',
|
||||
ctx.query?.state,
|
||||
ctx.state?.user,
|
||||
verifiedEmail,
|
||||
first,
|
||||
last,
|
||||
{ type: SocialIdType.OIDC, value: ctx.state.user.sub },
|
||||
signUpDisabled
|
||||
)
|
||||
|
||||
if (state.inviteId != null && state.inviteId !== '') {
|
||||
loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
verifiedEmail,
|
||||
first,
|
||||
last,
|
||||
state.inviteId as any,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
} else {
|
||||
loginInfo = await loginOrSignUpWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
verifiedEmail,
|
||||
first,
|
||||
last,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
}
|
||||
|
||||
if (loginInfo === null) {
|
||||
measureCtx.info('Failed to auth: no associated account found', {
|
||||
email,
|
||||
verifiedEmail,
|
||||
type: 'openid',
|
||||
user: ctx.state?.user
|
||||
})
|
||||
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
|
||||
} else {
|
||||
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
|
||||
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
|
||||
|
||||
measureCtx.info('Success auth, redirect', { email, type: 'openid', target: origin })
|
||||
// Successful authentication, redirect to your application
|
||||
ctx.redirect(`${origin}?${query}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: 'openid', user: ctx.state?.user })
|
||||
if (redirectUrl !== '') {
|
||||
ctx.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
||||
)
|
||||
|
@ -12,7 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { AccountDB, joinWithProvider, LoginInfo, loginOrSignUpWithProvider } from '@hcengineering/account'
|
||||
import { BrandingMap, concatLink, getBranding, MeasureContext, SocialKey } from '@hcengineering/core'
|
||||
import { IncomingHttpHeaders } from 'http'
|
||||
import qs from 'querystringify'
|
||||
|
||||
export function getHost (headers: IncomingHttpHeaders): string | undefined {
|
||||
let host: string | undefined
|
||||
@ -27,6 +30,8 @@ export function getHost (headers: IncomingHttpHeaders): string | undefined {
|
||||
export interface AuthState {
|
||||
inviteId?: string
|
||||
branding?: string
|
||||
autoJoin?: boolean
|
||||
navigateUrl?: string
|
||||
}
|
||||
|
||||
export function safeParseAuthState (rawState: string | undefined): AuthState {
|
||||
@ -40,3 +45,89 @@ export function safeParseAuthState (rawState: string | undefined): AuthState {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeState (ctx: any, brandings: BrandingMap): string {
|
||||
const host = getHost(ctx.request.headers)
|
||||
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
|
||||
const state: AuthState = {
|
||||
inviteId: ctx.query?.inviteId,
|
||||
branding,
|
||||
autoJoin: ctx.query?.autoJoin !== undefined,
|
||||
navigateUrl: ctx.query?.navigateUrl
|
||||
}
|
||||
|
||||
return encodeURIComponent(JSON.stringify(state))
|
||||
}
|
||||
|
||||
export async function handleProviderAuth (
|
||||
measureCtx: MeasureContext,
|
||||
db: AccountDB,
|
||||
brandings: BrandingMap,
|
||||
frontUrl: string,
|
||||
providerType: string,
|
||||
rawState: string | undefined,
|
||||
user: any,
|
||||
email: string,
|
||||
first: string,
|
||||
last: string,
|
||||
socialKey: SocialKey,
|
||||
signUpDisabled: boolean | undefined
|
||||
): Promise<string> {
|
||||
try {
|
||||
measureCtx.info('Provider auth handler', { email, type: providerType })
|
||||
let loginInfo: LoginInfo | null
|
||||
const state = safeParseAuthState(rawState)
|
||||
const branding = getBranding(brandings, state?.branding)
|
||||
|
||||
if (state.inviteId != null && state.inviteId !== '' && state.autoJoin == null) {
|
||||
loginInfo = await joinWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
state.inviteId as any,
|
||||
socialKey,
|
||||
signUpDisabled
|
||||
)
|
||||
} else {
|
||||
loginInfo = await loginOrSignUpWithProvider(
|
||||
measureCtx,
|
||||
db,
|
||||
null,
|
||||
email,
|
||||
first,
|
||||
last,
|
||||
socialKey,
|
||||
signUpDisabled === true || state.autoJoin != null
|
||||
)
|
||||
}
|
||||
|
||||
if (loginInfo === null) {
|
||||
measureCtx.info('Failed to auth: no associated account found', {
|
||||
email,
|
||||
type: providerType,
|
||||
user
|
||||
})
|
||||
return concatLink(branding?.front ?? frontUrl, '/login')
|
||||
} else {
|
||||
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
|
||||
const queryObj: any = { token: loginInfo.token }
|
||||
if (state.autoJoin != null) {
|
||||
queryObj.autoJoin = state.autoJoin
|
||||
queryObj.inviteId = state.inviteId
|
||||
queryObj.navigateUrl = state.navigateUrl
|
||||
}
|
||||
|
||||
const query = encodeURIComponent(qs.stringify(queryObj))
|
||||
|
||||
// Successful authentication, redirect to your application
|
||||
measureCtx.info('Success auth, redirect', { email, type: providerType, target: origin })
|
||||
return `${origin}?${query}`
|
||||
}
|
||||
} catch (err: any) {
|
||||
measureCtx.error('failed to auth', { err, type: providerType, user })
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +231,7 @@ async function migrateAccount (account: OldAccount, accountDB: AccountDB): Promi
|
||||
...verified
|
||||
})
|
||||
|
||||
await createAccount(accountDB, personUuid, account.confirmed, account.createdOn)
|
||||
await createAccount(accountDB, personUuid, account.confirmed, false, account.createdOn)
|
||||
if (account.hash != null && account.salt != null) {
|
||||
await accountDB.account.updateOne({ uuid: personUuid }, { hash: account.hash, salt: account.salt })
|
||||
}
|
||||
|
@ -1302,7 +1302,7 @@ describe('account utils', () => {
|
||||
personUuid,
|
||||
verifiedOn: undefined
|
||||
})
|
||||
expect(mockDb.account.insertOne).toHaveBeenCalledWith({ uuid: personUuid })
|
||||
expect(mockDb.account.insertOne).toHaveBeenCalledWith({ uuid: personUuid, automatic: false })
|
||||
expect(mockDb.setPassword).toHaveBeenCalledWith(personUuid, expect.any(Buffer), expect.any(Buffer))
|
||||
})
|
||||
|
||||
@ -1326,7 +1326,7 @@ describe('account utils', () => {
|
||||
|
||||
expect(result.account).toBe(personUuid)
|
||||
expect(mockDb.person.updateOne).toHaveBeenCalledWith({ uuid: personUuid }, { firstName, lastName })
|
||||
expect(mockDb.account.insertOne).toHaveBeenCalledWith({ uuid: personUuid })
|
||||
expect(mockDb.account.insertOne).toHaveBeenCalledWith({ uuid: personUuid, automatic: false })
|
||||
expect(mockDb.setPassword).toHaveBeenCalledWith(personUuid, expect.any(Buffer), expect.any(Buffer))
|
||||
})
|
||||
|
||||
|
@ -846,11 +846,14 @@ export class PostgresAccountDB implements AccountDB {
|
||||
|
||||
private getV3Migration (): [string, string] {
|
||||
return [
|
||||
'account_db_v3_add_invite_auto_join',
|
||||
'account_db_v3_add_invite_auto_join_final',
|
||||
`
|
||||
ALTER TABLE ${this.ns}.invite
|
||||
ADD COLUMN IF NOT EXISTS email STRING,
|
||||
ADD COLUMN IF NOT EXISTS auto_join BOOL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE ${this.ns}.account
|
||||
ADD COLUMN IF NOT EXISTS automatic BOOL;
|
||||
`
|
||||
]
|
||||
}
|
||||
|
@ -725,7 +725,7 @@ export async function checkAutoJoin (
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { inviteId: string, firstName: string, lastName?: string }
|
||||
params: { inviteId: string, firstName?: string, lastName?: string }
|
||||
): Promise<WorkspaceLoginInfo | WorkspaceInviteInfo> {
|
||||
const { inviteId, firstName, lastName } = params
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
@ -768,8 +768,11 @@ export async function checkAutoJoin (
|
||||
if (targetAccount != null) {
|
||||
if (token == null) {
|
||||
// Login required
|
||||
const person = await db.person.findOne({ uuid: targetAccount.uuid })
|
||||
|
||||
return {
|
||||
workspace: workspace.uuid,
|
||||
name: person == null ? '' : getPersonName(person),
|
||||
email: normalizedEmail
|
||||
}
|
||||
}
|
||||
@ -778,15 +781,20 @@ export async function checkAutoJoin (
|
||||
|
||||
if (callerAccount !== targetAccount.uuid) {
|
||||
// Login with target email required
|
||||
const person = await db.person.findOne({ uuid: targetAccount.uuid })
|
||||
|
||||
return {
|
||||
workspace: workspace.uuid,
|
||||
name: person == null ? '' : getPersonName(person),
|
||||
email: normalizedEmail
|
||||
}
|
||||
}
|
||||
|
||||
const targetRole = await getWorkspaceRole(db, targetAccount.uuid, workspace.uuid)
|
||||
|
||||
if (targetRole == null || getRolePower(targetRole) < getRolePower(invite.role)) {
|
||||
if (targetRole == null) {
|
||||
await db.assignWorkspace(targetAccount.uuid, workspace.uuid, invite.role)
|
||||
} else if (getRolePower(targetRole) < getRolePower(invite.role)) {
|
||||
await db.updateWorkspaceRole(targetAccount.uuid, workspace.uuid, invite.role)
|
||||
}
|
||||
|
||||
@ -800,7 +808,17 @@ export async function checkAutoJoin (
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const { account } = await signUpByEmail(ctx, db, branding, normalizedEmail, null, firstName, lastName ?? '', true)
|
||||
const { account } = await signUpByEmail(
|
||||
ctx,
|
||||
db,
|
||||
branding,
|
||||
normalizedEmail,
|
||||
null,
|
||||
firstName,
|
||||
lastName ?? '',
|
||||
true,
|
||||
true
|
||||
)
|
||||
|
||||
return await doJoinByInvite(ctx, db, branding, generateToken(account, workspaceUuid), account, workspace, invite)
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ export interface SocialId extends SocialIdBase {
|
||||
|
||||
export interface Account {
|
||||
uuid: PersonUuid
|
||||
automatic?: boolean
|
||||
timezone?: string
|
||||
locale?: string
|
||||
hash?: Buffer | null
|
||||
@ -270,6 +271,7 @@ export interface RegionInfo {
|
||||
export interface WorkspaceInviteInfo {
|
||||
workspace: WorkspaceUuid
|
||||
email?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface MailboxOptions {
|
||||
|
@ -435,6 +435,7 @@ export async function createAccount (
|
||||
db: AccountDB,
|
||||
personUuid: PersonUuid,
|
||||
confirmed = false,
|
||||
automatic = false,
|
||||
createdOn = Date.now()
|
||||
): Promise<void> {
|
||||
// Create Huly social id and account
|
||||
@ -446,7 +447,7 @@ export async function createAccount (
|
||||
personUuid,
|
||||
...(confirmed ? { verifiedOn: Date.now() } : {})
|
||||
})
|
||||
await db.account.insertOne({ uuid: personUuid })
|
||||
await db.account.insertOne({ uuid: personUuid, automatic })
|
||||
await db.accountEvent.insertOne({
|
||||
accountUuid: personUuid,
|
||||
eventType: AccountEventType.ACCOUNT_CREATED,
|
||||
@ -462,7 +463,8 @@ export async function signUpByEmail (
|
||||
password: string | null,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
confirmed = false
|
||||
confirmed = false,
|
||||
automatic = false
|
||||
): Promise<{ account: PersonUuid, socialId: PersonId }> {
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
|
||||
@ -493,7 +495,7 @@ export async function signUpByEmail (
|
||||
})
|
||||
}
|
||||
|
||||
await createAccount(db, account, confirmed)
|
||||
await createAccount(db, account, confirmed, automatic)
|
||||
if (password != null) {
|
||||
await setPassword(ctx, db, branding, account, password)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user