mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 12:35:02 +02:00
Readonly allow signup (#9517)
Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
@@ -48,7 +48,8 @@ import core, {
|
||||
type Blob,
|
||||
type Timestamp,
|
||||
type Tx,
|
||||
type TxCUD
|
||||
type TxCUD,
|
||||
AccountRole
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
ArrOf,
|
||||
@@ -277,6 +278,10 @@ export function createModel (builder: Builder): void {
|
||||
TUserMentionInfo
|
||||
)
|
||||
|
||||
builder.mixin(activity.class.Reaction, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
removeAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: activity.component.DocUpdateMessagePresenter
|
||||
})
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
TObjectChatPanel,
|
||||
TThreadMessage
|
||||
} from './types'
|
||||
import { AccountRole } from '@hcengineering/core'
|
||||
|
||||
export { chunterId } from '@hcengineering/chunter'
|
||||
export { chunterOperation } from './migration'
|
||||
@@ -88,6 +89,10 @@ export function createModel (builder: Builder): void {
|
||||
component: chunter.component.WorkbenchTabExtension
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
createAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
|
||||
|
||||
spaceClasses.forEach((spaceClass) => {
|
||||
|
||||
@@ -309,6 +309,18 @@ export function createModel (builder: Builder): void {
|
||||
TUserRole
|
||||
)
|
||||
|
||||
builder.mixin(contact.class.PersonSpace, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
createAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
createAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.SocialIdentity, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
createAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, activity.mixin.ActivityDoc, {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DOMAIN_MODEL,
|
||||
DOMAIN_SPACE,
|
||||
IndexKind,
|
||||
type AccountRole,
|
||||
type Class,
|
||||
type CollectionSize,
|
||||
type Permission,
|
||||
@@ -27,7 +28,8 @@ import {
|
||||
type SpaceType,
|
||||
type SpaceTypeDescriptor,
|
||||
type TypedSpace,
|
||||
type AccountUuid
|
||||
type AccountUuid,
|
||||
type TxAccessLevel
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
ArrOf,
|
||||
@@ -45,8 +47,7 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
|
||||
import core from './component'
|
||||
import { TAttachedDoc, TDoc } from './core'
|
||||
|
||||
import { TAttachedDoc, TDoc, TClass } from './core'
|
||||
// S P A C E
|
||||
|
||||
@Model(core.class.Space, core.class.Doc, DOMAIN_SPACE)
|
||||
@@ -163,3 +164,10 @@ export class TPermission extends TDoc implements Permission {
|
||||
export class TSpacesTypeData extends TSpace implements RolesAssignment {
|
||||
[key: Ref<Role>]: AccountUuid[]
|
||||
}
|
||||
|
||||
@Mixin(core.mixin.TxAccessLevel, core.class.Class)
|
||||
export class TTxAccessLevel extends TClass implements TxAccessLevel {
|
||||
createAccessLevel?: AccountRole
|
||||
removeAccessLevel?: AccountRole
|
||||
updateAccessLevel?: AccountRole
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ArrOf, Prop, TypeString, type Builder } from '@hcengineering/model'
|
||||
import { type Asset } from '@hcengineering/platform'
|
||||
import { getRoleAttributeLabel } from '@hcengineering/core'
|
||||
|
||||
import { TSpacesTypeData } from './security'
|
||||
import { TSpacesTypeData, TTxAccessLevel } from './security'
|
||||
import core from './component'
|
||||
|
||||
const roles = [
|
||||
@@ -37,6 +37,7 @@ export function defineSpaceType (builder: Builder): void {
|
||||
}
|
||||
|
||||
builder.createModel(TSpacesTypeData)
|
||||
builder.createModel(TTxAccessLevel)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.SpaceTypeDescriptor,
|
||||
|
||||
@@ -513,6 +513,14 @@ export function createModel (builder: Builder): void {
|
||||
components: { input: { component: chunter.component.ChatMessageInput, props: { collection: 'messages' } } }
|
||||
})
|
||||
|
||||
builder.mixin(love.class.JoinRequest, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
removeAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(love.class.ParticipantInfo, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
updateAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(love.class.MeetingMinutes, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.mixin(love.class.Room, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
@@ -555,6 +555,10 @@ export function createModel (builder: Builder): void {
|
||||
presenter: notification.component.MentionInboxNotificationPresenter
|
||||
})
|
||||
|
||||
builder.mixin(notification.class.BrowserNotification, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
removeAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole, type AccountUuid } from '@hcengineering/core'
|
||||
import { AccountRole, type AccountUuid, type Class, DOMAIN_MODEL, type Ref, type Space } from '@hcengineering/core'
|
||||
import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import { createAction } from '@hcengineering/model-view'
|
||||
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import view, { type KeyBinding } from '@hcengineering/view'
|
||||
import type {
|
||||
Application,
|
||||
@@ -115,6 +115,12 @@ export function createModel (builder: Builder): void {
|
||||
TWorkbenchTab
|
||||
)
|
||||
|
||||
builder.mixin(workbench.class.WorkbenchTab, core.class.Class, core.mixin.TxAccessLevel, {
|
||||
createAccessLevel: AccountRole.Guest,
|
||||
removeAccessLevel: AccountRole.Guest,
|
||||
updateAccessLevel: AccountRole.Guest
|
||||
})
|
||||
|
||||
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: workbench.component.ApplicationPresenter
|
||||
})
|
||||
|
||||
@@ -89,9 +89,10 @@ export interface AccountClient {
|
||||
password: string,
|
||||
first: string,
|
||||
last: string,
|
||||
inviteId: string
|
||||
inviteId: string,
|
||||
workspaceUrl: string
|
||||
) => Promise<WorkspaceLoginInfo>
|
||||
join: (email: string, password: string, inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
join: (email: string, password: string, inviteId: string, workspaceUrl: 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>
|
||||
@@ -442,20 +443,21 @@ class AccountClientImpl implements AccountClient {
|
||||
password: string,
|
||||
first: string,
|
||||
last: string,
|
||||
inviteId: string
|
||||
inviteId: string,
|
||||
workspaceUrl: string
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const request = {
|
||||
method: 'signUpJoin' as const,
|
||||
params: { email, password, first, last, inviteId }
|
||||
params: { email, password, first, last, inviteId, workspaceUrl }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async join (email: string, password: string, inviteId: string): Promise<WorkspaceLoginInfo> {
|
||||
async join (email: string, password: string, inviteId: string, workspaceUrl: string): Promise<WorkspaceLoginInfo> {
|
||||
const request = {
|
||||
method: 'join' as const,
|
||||
params: { email, password, inviteId }
|
||||
params: { email, password, inviteId, workspaceUrl }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
|
||||
@@ -550,6 +550,12 @@ export const roleOrder: Record<AccountRole, number> = {
|
||||
[AccountRole.Admin]: 100
|
||||
}
|
||||
|
||||
export interface TxAccessLevel extends Class<Doc> {
|
||||
createAccessLevel?: AccountRole
|
||||
removeAccessLevel?: AccountRole
|
||||
updateAccessLevel?: AccountRole
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { BenchmarkDoc } from './benchmark'
|
||||
import { AccountRole } from './classes'
|
||||
import { AccountRole, TxAccessLevel } from './classes'
|
||||
import type {
|
||||
Account,
|
||||
AnyAttribute,
|
||||
@@ -99,6 +99,8 @@ export const systemAccount: Account = {
|
||||
|
||||
export const configUserAccountUuid = '0d94731c-0787-4bcd-aefe-304efc3706b1' as AccountUuid
|
||||
|
||||
export const readOnlyGuestAccountUuid = '83bbed9a-0867-4851-be32-31d49d1d42ce' as AccountUuid
|
||||
|
||||
export default plugin(coreId, {
|
||||
class: {
|
||||
Obj: '' as Ref<Class<Obj>>,
|
||||
@@ -189,7 +191,8 @@ export default plugin(coreId, {
|
||||
ConfigurationElement: '' as Ref<Mixin<ConfigurationElement>>,
|
||||
IndexConfiguration: '' as Ref<Mixin<IndexingConfiguration<Doc>>>,
|
||||
SpacesTypeData: '' as Ref<Mixin<Space>>,
|
||||
TransientConfiguration: '' as Ref<Mixin<TransientConfiguration>>
|
||||
TransientConfiguration: '' as Ref<Mixin<TransientConfiguration>>,
|
||||
TxAccessLevel: '' as Ref<Mixin<TxAccessLevel>>
|
||||
},
|
||||
space: {
|
||||
Tx: '' as Ref<Space>,
|
||||
|
||||
@@ -17,7 +17,14 @@ import core from './component'
|
||||
export * from './classes'
|
||||
export * from './client'
|
||||
export * from './collaboration'
|
||||
export { coreId, systemAccountUuid, systemAccountEmail, systemAccount, configUserAccountUuid } from './component'
|
||||
export {
|
||||
coreId,
|
||||
systemAccountUuid,
|
||||
readOnlyGuestAccountUuid,
|
||||
systemAccountEmail,
|
||||
systemAccount,
|
||||
configUserAccountUuid
|
||||
} from './component'
|
||||
export * from './hierarchy'
|
||||
export * from './measurements'
|
||||
export * from './memdb'
|
||||
|
||||
@@ -172,6 +172,7 @@ export default plugin(platformId, {
|
||||
MailboxError: '' as StatusCode<{ reason: string }>,
|
||||
SocialIdAlreadyExists: '' as StatusCode,
|
||||
ReadOnlyAccount: '' as StatusCode,
|
||||
RegularAccount: '' as StatusCode,
|
||||
SystemAccount: '' as StatusCode
|
||||
},
|
||||
metadata: {
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
maintenanceTime = _status.params.time
|
||||
maintenanceMessage = _status.params.message
|
||||
} else {
|
||||
if (_status.code === platform.status.RegularAccount) {
|
||||
readonlyAccount = false
|
||||
}
|
||||
if (readonlyAccount) return
|
||||
if (_status.code === platform.status.ReadOnlyAccount) {
|
||||
readonlyAccount = true
|
||||
|
||||
@@ -26,11 +26,13 @@ import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage
|
||||
import contact, { type Employee, getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
|
||||
import { employeeByAccountStore, employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources'
|
||||
import core, {
|
||||
AccountRole,
|
||||
type AccountUuid,
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
getCurrentAccount,
|
||||
hasAccountRole,
|
||||
notEmpty,
|
||||
type Ref,
|
||||
type Space,
|
||||
@@ -120,6 +122,10 @@ export async function canDeleteMessage (doc?: ChatMessage): Promise<boolean> {
|
||||
|
||||
const me = getCurrentAccount()
|
||||
|
||||
if (hasAccountRole(me, AccountRole.Maintainer)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return doc.createdBy !== undefined && me.socialIds.includes(doc.createdBy)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,13 +66,19 @@
|
||||
|
||||
const [loginStatus, result] =
|
||||
page === 'login'
|
||||
? await join(object.username, object.password, location.query?.inviteId ?? '')
|
||||
? await join(
|
||||
object.username,
|
||||
object.password,
|
||||
location.query?.inviteId ?? '',
|
||||
location.query?.workspace ?? ''
|
||||
)
|
||||
: await signUpJoin(
|
||||
object.username,
|
||||
object.password,
|
||||
object.first,
|
||||
object.last,
|
||||
location.query?.inviteId ?? ''
|
||||
location.query?.inviteId ?? '',
|
||||
location.query?.workspace ?? ''
|
||||
)
|
||||
status = loginStatus
|
||||
|
||||
|
||||
@@ -615,10 +615,11 @@ export async function getInviteLinkId (
|
||||
export async function join (
|
||||
email: string,
|
||||
password: string,
|
||||
inviteId: string
|
||||
inviteId: string,
|
||||
workspace: string
|
||||
): Promise<[Status, WorkspaceLoginInfo | null]> {
|
||||
try {
|
||||
const workspaceLoginInfo = await getAccountClient().join(email, password, inviteId)
|
||||
const workspaceLoginInfo = await getAccountClient().join(email, password, inviteId, workspace)
|
||||
|
||||
Analytics.handleEvent('Join', { email, ok: true })
|
||||
|
||||
@@ -643,10 +644,11 @@ export async function signUpJoin (
|
||||
password: string,
|
||||
first: string,
|
||||
last: string,
|
||||
inviteId: string
|
||||
inviteId: string,
|
||||
workspace: string
|
||||
): Promise<[Status, WorkspaceLoginInfo | null]> {
|
||||
try {
|
||||
const workspaceLoginInfo = await getAccountClient().signUpJoin(email, password, first, last, inviteId)
|
||||
const workspaceLoginInfo = await getAccountClient().signUpJoin(email, password, first, last, inviteId, workspace)
|
||||
|
||||
Analytics.handleEvent('Signup Join', { email, ok: true })
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
AccountUuid,
|
||||
Configuration,
|
||||
getCurrentAccount,
|
||||
pickPrimarySocialId
|
||||
pickPrimarySocialId,
|
||||
readOnlyGuestAccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -49,7 +50,7 @@
|
||||
import { translateCB } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, MessageBox, uiContext } from '@hcengineering/presentation'
|
||||
import { WorkspaceSetting } from '@hcengineering/setting'
|
||||
import { AvatarType, ensureEmployeeForPerson } from '@hcengineering/contact'
|
||||
import contact, { AvatarType, ensureEmployeeForPerson } from '@hcengineering/contact'
|
||||
import settingsRes from '../plugin'
|
||||
|
||||
let loading = true
|
||||
@@ -172,6 +173,11 @@
|
||||
guestUserInfo.guestSocialIds,
|
||||
guestUserInfo.guestPerson
|
||||
)
|
||||
} else {
|
||||
const readonlyEmployee = await client.findOne(contact.mixin.Employee, { personUuid: readOnlyGuestAccountUuid })
|
||||
if (readonlyEmployee !== undefined) {
|
||||
await client.update(readonlyEmployee, { active: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,8 +136,11 @@
|
||||
"Loading": "Načítání...",
|
||||
"NoRelations": "Žádné vztahy",
|
||||
"ReadOnlyWarningTitle": "✨ Ukázka pro hosty",
|
||||
"ReadOnlyWarningMessage": "Jste v režimu prohlížení. Chcete spravovat jakékoli projekty v Huly? Úkoly, termíny, tým — zaregistrujte se pro plný přístup!",
|
||||
"ReadOnlySignUp": "Zaregistrovat se \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Jste v režimu prohlížení. Chcete spravovat jakékoli projekty v Huly? Úkoly, termíny, tým — připojte se k tomuto prostoru pro spolupráci nebo si vytvořte vlastní!",
|
||||
"ReadOnlyJoinWorkspace": "Připojit se k prostoru 👥",
|
||||
"ReadOnlySignUp": "Vytvořit účet 🚀",
|
||||
"PermissionWarningTitle": "✨ Zvyšte svůj přístup!",
|
||||
"PermissionWarningMessage": "Vaše současná role má omezená oprávnění. Chcete-li odemknout tuto a další pokročilé funkce, požádejte správce svého pracovního prostoru o zvýšení úrovně přístupu!",
|
||||
"Icon": "Ikona",
|
||||
"Color": "Barva",
|
||||
"AutomationOnly": "Pouze automatizace"
|
||||
|
||||
@@ -136,8 +136,11 @@
|
||||
"Loading": "Laden...",
|
||||
"NoRelations": "Keine Beziehungen",
|
||||
"ReadOnlyWarningTitle": "✨ Gast-Demo",
|
||||
"ReadOnlyWarningMessage": "Sie sind im Ansichtsmodus. Möchten Sie beliebige Projekte in Huly verwalten? Aufgaben, Fristen, Team — registrieren Sie sich für vollen Zugriff!",
|
||||
"ReadOnlySignUp": "Registrieren \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Sie sind im Ansichtsmodus. Möchten Sie beliebige Projekte in Huly verwalten? Aufgaben, Fristen, Team — treten Sie diesem Arbeitsbereich bei oder erstellen Sie Ihren eigenen!",
|
||||
"ReadOnlyJoinWorkspace": "Arbeitsbereich beitreten 👥",
|
||||
"ReadOnlySignUp": "Konto erstellen 🚀",
|
||||
"PermissionWarningTitle": "✨ Erweitern Sie Ihren Zugriff!",
|
||||
"PermissionWarningMessage": "Ihre aktuelle Rolle hat eingeschränkte Berechtigungen. Um diese und andere erweiterte Funktionen freizuschalten, bitten Sie Ihren Arbeitsbereich-Administrator, Ihr Zugriffslevel zu erhöhen!",
|
||||
"Icon": "Symbol",
|
||||
"Color": "Farbe",
|
||||
"AutomationOnly": "Nur Automatisierung"
|
||||
|
||||
@@ -136,8 +136,11 @@
|
||||
"Loading": "Loading...",
|
||||
"NoRelations": "No relations",
|
||||
"ReadOnlyWarningTitle": "✨ Guest Demo",
|
||||
"ReadOnlyWarningMessage": "You're in view-only mode. Want to manage any project in Huly? Tasks, deadlines, team — sign up for full access!",
|
||||
"ReadOnlySignUp": "Sign up \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "You're in view-only mode. Want to manage any project in Huly? Tasks, deadlines, team — join this workspace to collaborate or create your own space!",
|
||||
"ReadOnlyJoinWorkspace": "Join workspace 👥",
|
||||
"ReadOnlySignUp": "Create account 🚀",
|
||||
"PermissionWarningTitle": "✨ Level up your access!",
|
||||
"PermissionWarningMessage": "Your current role has limited permissions. To unlock this and other advanced features, ask your workspace admin to upgrade your access level!",
|
||||
"Icon": "Icon",
|
||||
"Color": "Color",
|
||||
"AutomationOnly": "Automation only"
|
||||
|
||||
@@ -131,8 +131,11 @@
|
||||
"Loading": "Cargando...",
|
||||
"NoRelations": "No hay relaciones",
|
||||
"ReadOnlyWarningTitle": "✨ Demo de Invitado",
|
||||
"ReadOnlyWarningMessage": "Estás en modo de solo lectura. ¿Quieres gestionar cualquier proyecto en Huly? Tareas, plazos, equipo — ¡regístrate para acceso completo!",
|
||||
"ReadOnlySignUp": "Registrarse \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Estás en modo de solo lectura. ¿Quieres gestionar cualquier proyecto en Huly? Tareas, plazos, equipo — ¡únete a este espacio para colaborar o crea el tuyo!",
|
||||
"ReadOnlyJoinWorkspace": "Unirse al espacio 👥",
|
||||
"ReadOnlySignUp": "Crear cuenta 🚀",
|
||||
"PermissionWarningTitle": "✨ ¡Mejora tu acceso!",
|
||||
"PermissionWarningMessage": "Tu rol actual tiene permisos limitados. ¡Para desbloquear esta y otras funciones avanzadas, pídele al administrador de tu espacio de trabajo que actualice tu nivel de acceso!",
|
||||
"Icon": "Icono",
|
||||
"Color": "Color",
|
||||
"AutomationOnly": "Solo automatización"
|
||||
|
||||
@@ -131,8 +131,11 @@
|
||||
"Loading": "Chargement...",
|
||||
"NoRelations": "Aucune relation",
|
||||
"ReadOnlyWarningTitle": "✨ Démo Invité",
|
||||
"ReadOnlyWarningMessage": "Vous êtes en mode consultation. Voulez-vous gérer tous types de projets dans Huly ? Tâches, délais, équipe — inscrivez-vous pour un accès complet !",
|
||||
"ReadOnlySignUp": "Inscription \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Vous êtes en mode consultation. Voulez-vous gérer tous types de projets dans Huly ? Tâches, délais, équipe — rejoignez cet espace pour collaborer ou créez le vôtre !",
|
||||
"ReadOnlyJoinWorkspace": "Rejoindre l'espace 👥",
|
||||
"ReadOnlySignUp": "Créer un compte 🚀",
|
||||
"PermissionWarningTitle": "✨ Améliorez votre accès !",
|
||||
"PermissionWarningMessage": "Votre rôle actuel a des autorisations limitées. Pour débloquer cette fonctionnalité et d'autres fonctionnalités avancées, demandez à l'administrateur de votre espace de travail de mettre à jour votre niveau d'accès !",
|
||||
"Icon": "Icône",
|
||||
"Color": "Couleur",
|
||||
"AutomationOnly": "Automatisation uniquement"
|
||||
|
||||
@@ -131,8 +131,11 @@
|
||||
"Loading": "Caricamento...",
|
||||
"NoRelations": "Nessuna relazione",
|
||||
"ReadOnlyWarningTitle": "✨ Demo Ospite",
|
||||
"ReadOnlyWarningMessage": "Sei in modalità di sola lettura. Vuoi gestire qualsiasi progetto su Huly? Task, scadenze, team — registrati per l'accesso completo!",
|
||||
"ReadOnlySignUp": "Registrati \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Sei in modalità di sola lettura. Vuoi gestire qualsiasi progetto su Huly? Task, scadenze, team — unisciti a questo spazio per collaborare o creane uno tuo!",
|
||||
"ReadOnlyJoinWorkspace": "Unisciti allo spazio 👥",
|
||||
"ReadOnlySignUp": "Crea account 🚀",
|
||||
"PermissionWarningTitle": "✨ Migliora il tuo accesso!",
|
||||
"PermissionWarningMessage": "Il tuo ruolo attuale ha autorizzazioni limitate. Per sbloccare questa e altre funzionalità avanzate, chiedi all'amministratore del tuo spazio di lavoro di aggiornare il tuo livello di accesso!",
|
||||
"Icon": "Icona",
|
||||
"Color": "Colore",
|
||||
"AutomationOnly": "Solo automazione"
|
||||
|
||||
@@ -131,8 +131,11 @@
|
||||
"Loading": "読み込み中...",
|
||||
"NoRelations": "関係なし",
|
||||
"ReadOnlyWarningTitle": "✨ ゲストデモ",
|
||||
"ReadOnlyWarningMessage": "閲覧モードです。Hulyでどんなプロジェクトも管理しませんか? タスク、期限、チーム — 完全アクセスには登録してください!",
|
||||
"ReadOnlySignUp": "サインアップ \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "閲覧モードです。Hulyでどんなプロジェクトも管理しませんか? タスク、期限、チーム — このワークスペースに参加して共同作業するか、自分自身のスペースを作成しましょう!",
|
||||
"ReadOnlyJoinWorkspace": "ワークスペースに参加 👥",
|
||||
"ReadOnlySignUp": "アカウント作成 🚀",
|
||||
"PermissionWarningTitle": "✨ アクセスレベルを上げましょう!",
|
||||
"PermissionWarningMessage": "現在のロールの権限は制限されています。この機能や他の高度な機能を利用するには、ワークスペース管理者にアクセスレベルのアップグレードを依頼してください!",
|
||||
"Icon": "アイコン",
|
||||
"Color": "色",
|
||||
"AutomationOnly": "自動化のみ"
|
||||
|
||||
@@ -131,8 +131,11 @@
|
||||
"Loading": "Carregando...",
|
||||
"NoRelations": "Sem relações",
|
||||
"ReadOnlyWarningTitle": "✨ Demonstração para Convidados",
|
||||
"ReadOnlyWarningMessage": "Você está no modo de visualização. Quer gerenciar qualquer projeto no Huly? Tarefas, prazos, equipe — cadastre-se para acesso total!",
|
||||
"ReadOnlySignUp": "Registar \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Você está no modo de visualização. Quer gerenciar qualquer projeto no Huly? Tarefas, prazos, equipe — junte-se a este espaço para colaborar ou crie o seu!",
|
||||
"ReadOnlyJoinWorkspace": "Entrar no espaço 👥",
|
||||
"ReadOnlySignUp": "Criar conta 🚀",
|
||||
"PermissionWarningTitle": "✨ Melhore seu acesso!",
|
||||
"PermissionWarningMessage": "Seu cargo atual tem permissões limitadas. Para desbloquear este e outros recursos avançados, peça ao administrador do seu espaço de trabalho para atualizar seu nível de acesso!",
|
||||
"Icon": "Ícone",
|
||||
"Color": "Cor",
|
||||
"AutomationOnly": "Apenas automação"
|
||||
|
||||
@@ -133,8 +133,11 @@
|
||||
"Loading": "Загрузка...",
|
||||
"NoRelations": "Нет связей",
|
||||
"ReadOnlyWarningTitle": "✨ Гостевое демо",
|
||||
"ReadOnlyWarningMessage": "Вы в режиме просмотра. Хотите управлять любыми проектами в Huly? Задачи, сроки, команда — регистрируйтесь для полного доступа!",
|
||||
"ReadOnlySignUp": "Регистрация \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "Вы в режиме просмотра. Хотите управлять любыми проектами в Huly? Задачи, сроки, команда — присоединитесь к этому пространству для сотрудничества или создайте свое!",
|
||||
"ReadOnlyJoinWorkspace": "Присоединиться 👥",
|
||||
"ReadOnlySignUp": "Создать аккаунт 🚀",
|
||||
"PermissionWarningTitle": "✨ Повысьте свой уровень доступа!",
|
||||
"PermissionWarningMessage": "Ваша текущая роль имеет ограниченные права. Чтобы разблокировать эту и другие продвинутые функции, попросите администратора вашего пространства повысить ваш уровень доступа!",
|
||||
"Icon": "Иконка",
|
||||
"Color": "Цвет",
|
||||
"AutomationOnly": "Только автоматизация"
|
||||
|
||||
@@ -136,8 +136,11 @@
|
||||
"Loading": "加载中...",
|
||||
"NoRelations": "无关系",
|
||||
"ReadOnlyWarningTitle": "✨ 访客演示",
|
||||
"ReadOnlyWarningMessage": "您处于只读模式。想在Huly管理任何项目吗?任务、截止日期、团队 — 立即注册获取完整权限!",
|
||||
"ReadOnlySignUp": "注册 \uD83D\uDE80",
|
||||
"ReadOnlyWarningMessage": "您处于只读模式。想在Huly管理任何项目吗?任务、截止日期、团队 — 加入此工作区协作或创建您自己的空间!",
|
||||
"ReadOnlyJoinWorkspace": "加入工作区 👥",
|
||||
"ReadOnlySignUp": "创建账户 🚀",
|
||||
"PermissionWarningTitle": "✨ 提升您的访问权限!",
|
||||
"PermissionWarningMessage": "您当前角色的权限有限。要解锁此功能和其他高级功能,请让您的工作区管理员升级您的访问权限!",
|
||||
"Icon": "图标",
|
||||
"Color": "颜色",
|
||||
"AutomationOnly": "仅限自动化"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Notification, NotificationToast } from '@hcengineering/ui'
|
||||
|
||||
export let onRemove: () => void
|
||||
export let notification: Notification
|
||||
</script>
|
||||
|
||||
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
|
||||
<svelte:fragment slot="content">
|
||||
{notification.subTitle}
|
||||
</svelte:fragment>
|
||||
</NotificationToast>
|
||||
@@ -1,9 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Button, Notification, NotificationToast } from '@hcengineering/ui'
|
||||
import { Button, navigate, Notification, NotificationToast } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { getCurrentWorkspaceUrl } from '@hcengineering/presentation'
|
||||
|
||||
export let onRemove: () => void
|
||||
export let notification: Notification
|
||||
|
||||
function joinWorkspace (e: MouseEvent): void {
|
||||
navigate({ path: ['login', 'join'], query: { workspace: getCurrentWorkspaceUrl() } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
|
||||
@@ -11,7 +16,7 @@
|
||||
{notification.subTitle}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<div style="width: auto" />
|
||||
<Button label={view.string.ReadOnlyJoinWorkspace} stopPropagation={false} on:click={joinWorkspace} />
|
||||
<a href="https://huly.io/signup" target="_blank">
|
||||
<Button label={view.string.ReadOnlySignUp} stopPropagation={false} />
|
||||
</a>
|
||||
|
||||
@@ -20,12 +20,13 @@ import core, {
|
||||
TxProcessor,
|
||||
type TxResult
|
||||
} from '@hcengineering/core'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import platform, { getResource, PlatformError, translate } from '@hcengineering/platform'
|
||||
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
|
||||
import view, { type IAggregationManager } from '@hcengineering/view'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { addNotification, NotificationSeverity } from '@hcengineering/ui'
|
||||
import ReadOnlyNotification from './components/ReadOnlyNotification.svelte'
|
||||
import ForbiddenNotification from './components/ForbiddenNotification.svelte'
|
||||
import { getCurrentLanguage } from '@hcengineering/theme'
|
||||
|
||||
/**
|
||||
@@ -361,6 +362,23 @@ export class ReadOnlyAccessMiddleware extends BasePresentationMiddleware impleme
|
||||
)
|
||||
return {}
|
||||
}
|
||||
return await this.provideTx(tx)
|
||||
try {
|
||||
return await this.provideTx(tx)
|
||||
} catch (err: any) {
|
||||
if (err instanceof PlatformError && err.status.code === platform.status.Forbidden) {
|
||||
addNotification(
|
||||
await translate(view.string.PermissionWarningTitle, {}, getCurrentLanguage()),
|
||||
await translate(view.string.PermissionWarningMessage, {}, getCurrentLanguage()),
|
||||
ForbiddenNotification,
|
||||
{
|
||||
onClose: () => {}
|
||||
},
|
||||
NotificationSeverity.Info
|
||||
)
|
||||
return {}
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,9 @@ const view = plugin(viewId, {
|
||||
ReadOnlyWarningTitle: '' as IntlString,
|
||||
ReadOnlyWarningMessage: '' as IntlString,
|
||||
ReadOnlySignUp: '' as IntlString,
|
||||
ReadOnlyJoinWorkspace: '' as IntlString,
|
||||
PermissionWarningTitle: '' as IntlString,
|
||||
PermissionWarningMessage: '' as IntlString,
|
||||
Icon: '' as IntlString,
|
||||
Color: '' as IntlString,
|
||||
AutomationOnly: '' as IntlString
|
||||
|
||||
@@ -407,6 +407,8 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
|
||||
if (me.role === AccountRole.ReadOnlyGuest) {
|
||||
await broadcastEvent(PlatformEvent, new Status(Severity.INFO, platform.status.ReadOnlyAccount, {}))
|
||||
} else {
|
||||
await broadcastEvent(PlatformEvent, new Status(Severity.INFO, platform.status.RegularAccount, {}))
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -42,13 +42,15 @@ import core, {
|
||||
type Space,
|
||||
SpaceType,
|
||||
systemAccountUuid,
|
||||
readOnlyGuestAccountUuid,
|
||||
Tx,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxMixin,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc,
|
||||
TypedSpace
|
||||
TypedSpace,
|
||||
TxFactory
|
||||
} from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
@@ -92,6 +94,10 @@ export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl):
|
||||
|
||||
export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const systemTxFactory = new TxFactory(core.account.System, false)
|
||||
const systemTxes: Tx[] = []
|
||||
|
||||
for (const tx of _txes) {
|
||||
const mixinTx = tx as TxMixin<Person, Employee>
|
||||
if (mixinTx.attributes.active !== true) continue
|
||||
@@ -104,21 +110,59 @@ export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): P
|
||||
result.push(...txes)
|
||||
|
||||
const emp = control.hierarchy.as(person, contact.mixin.Employee)
|
||||
if (emp.role === 'GUEST') continue
|
||||
if (emp.role === 'GUEST') {
|
||||
const readonlyEmployees = await control.findAll(control.ctx, contact.mixin.Employee, {
|
||||
personUuid: readOnlyGuestAccountUuid
|
||||
})
|
||||
if (readonlyEmployees.length === 0) continue
|
||||
|
||||
const readonlyEmployee = readonlyEmployees[0]
|
||||
if (!readonlyEmployee.active) continue
|
||||
|
||||
const spaces = await control.findAll(control.ctx, core.class.Space, { members: readOnlyGuestAccountUuid })
|
||||
for (const space of spaces) {
|
||||
if (space._class === contact.class.PersonSpace || space.members.includes(account)) continue
|
||||
|
||||
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
||||
$push: {
|
||||
members: account
|
||||
}
|
||||
})
|
||||
systemTxes.push(pushTx)
|
||||
}
|
||||
|
||||
const collabs = await control.findAll(control.ctx, core.class.Collaborator, {
|
||||
collaborator: readOnlyGuestAccountUuid
|
||||
})
|
||||
|
||||
for (const collab of collabs) {
|
||||
const pushTx = systemTxFactory.createTxCreateDoc(core.class.Collaborator, collab.space, {
|
||||
attachedTo: collab.attachedTo,
|
||||
collaborator: account,
|
||||
attachedToClass: collab.attachedToClass,
|
||||
collection: 'collaborators'
|
||||
})
|
||||
systemTxes.push(pushTx)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
|
||||
for (const space of spaces) {
|
||||
if (space.members.includes(account)) continue
|
||||
|
||||
const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
||||
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
||||
$push: {
|
||||
members: account
|
||||
}
|
||||
})
|
||||
result.push(pushTx)
|
||||
systemTxes.push(pushTx)
|
||||
}
|
||||
}
|
||||
|
||||
await control.apply(control.ctx, systemTxes)
|
||||
|
||||
const account = control.ctx.contextData.account
|
||||
if (account.role !== AccountRole.Owner) return result
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import core, {
|
||||
MixinUpdate,
|
||||
notEmpty,
|
||||
PersonId,
|
||||
readOnlyGuestAccountUuid,
|
||||
Ref,
|
||||
RefTo,
|
||||
SortingOrder,
|
||||
@@ -358,7 +359,7 @@ export async function pushInboxNotifications (
|
||||
|
||||
const notificationData = {
|
||||
user: receiver.account,
|
||||
isViewed: receiver.role === 'GUEST',
|
||||
isViewed: receiver.role === 'GUEST' && receiver.account === readOnlyGuestAccountUuid,
|
||||
docNotifyContext: docNotifyContextId,
|
||||
archived: false,
|
||||
objectId,
|
||||
@@ -538,7 +539,7 @@ async function createNotifyContext (
|
||||
}
|
||||
|
||||
const lastViewedTimestamp =
|
||||
receiver.role === 'GUEST'
|
||||
receiver.role === 'GUEST' && receiver.account === readOnlyGuestAccountUuid
|
||||
? Number.MAX_VALUE
|
||||
: receiver.socialIds.some((it) => it === sender)
|
||||
? updateTimestamp
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import {
|
||||
readOnlyGuestAccountUuid,
|
||||
AccountRole,
|
||||
type PersonId,
|
||||
SocialIdType,
|
||||
@@ -965,7 +966,7 @@ describe('invite operations', () => {
|
||||
describe('loginAsGuest', () => {
|
||||
test('should successfully login as readonly guest', async () => {
|
||||
const mockGuestPerson = {
|
||||
uuid: utils.READONLY_GUEST_ACCOUNT
|
||||
uuid: readOnlyGuestAccountUuid
|
||||
}
|
||||
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue(mockGuestPerson)
|
||||
@@ -973,7 +974,7 @@ describe('invite operations', () => {
|
||||
const result = await loginAsGuest(mockCtx, mockDb, mockBranding, mockToken)
|
||||
|
||||
expect(result).toEqual({
|
||||
account: utils.READONLY_GUEST_ACCOUNT,
|
||||
account: readOnlyGuestAccountUuid,
|
||||
token: expect.any(String)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type PersonUuid,
|
||||
SocialIdType,
|
||||
systemAccountUuid,
|
||||
readOnlyGuestAccountUuid,
|
||||
type WorkspaceMemberInfo,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
@@ -100,11 +101,11 @@ import {
|
||||
verifyPassword,
|
||||
wrap,
|
||||
updateAllowReadOnlyGuests,
|
||||
READONLY_GUEST_ACCOUNT,
|
||||
getWorkspaceByDataId,
|
||||
assignableRoles,
|
||||
getWorkspacesInfoWithStatusByIds,
|
||||
doMergePersons
|
||||
doMergePersons,
|
||||
getWorkspaceJoinInfo
|
||||
} from './utils'
|
||||
|
||||
// Note: it is IMPORTANT to always destructure params passed here to avoid sending extra params
|
||||
@@ -127,7 +128,7 @@ export async function loginAsGuest (
|
||||
branding: Branding | null,
|
||||
token: string
|
||||
): Promise<LoginInfo> {
|
||||
const guestPerson = await db.person.findOne({ uuid: READONLY_GUEST_ACCOUNT as PersonUuid })
|
||||
const guestPerson = await db.person.findOne({ uuid: readOnlyGuestAccountUuid as PersonUuid })
|
||||
if (guestPerson == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
|
||||
}
|
||||
@@ -811,31 +812,24 @@ export async function join (
|
||||
email: string
|
||||
password: string
|
||||
inviteId: string
|
||||
workspaceUrl: string
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<WorkspaceLoginInfo | LoginInfo> {
|
||||
const { email, password, inviteId } = params
|
||||
const { email, password, inviteId, workspaceUrl } = params
|
||||
|
||||
if (email == null || email === '' || password == null || password === '' || inviteId == null || inviteId === '') {
|
||||
if (password == null || password === '') {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
if (invite == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
const workspaceJoinInfo = await getWorkspaceJoinInfo(ctx, db, email, inviteId, workspaceUrl)
|
||||
ctx.info('Joining a workspace using invite', {
|
||||
email,
|
||||
normalizedEmail: workspaceJoinInfo.email,
|
||||
...workspaceJoinInfo.invite
|
||||
})
|
||||
|
||||
const workspaceUuid = await checkInvite(ctx, invite, normalizedEmail)
|
||||
const workspace = await getWorkspaceById(db, workspaceUuid)
|
||||
|
||||
if (workspace == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
|
||||
}
|
||||
|
||||
ctx.info('Joining a workspace using invite', { email, normalizedEmail, ...invite })
|
||||
|
||||
const { token, account } = await login(ctx, db, branding, _token, { email: normalizedEmail, password })
|
||||
const { token, account } = await login(ctx, db, branding, _token, { email: workspaceJoinInfo.email, password })
|
||||
|
||||
if (token == null) {
|
||||
return {
|
||||
@@ -843,7 +837,7 @@ export async function join (
|
||||
}
|
||||
}
|
||||
|
||||
return await doJoinByInvite(ctx, db, branding, token, account, workspace, invite)
|
||||
return await doJoinByInvite(ctx, db, branding, token, account, workspaceJoinInfo.workspace, workspaceJoinInfo.invite)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1022,43 +1016,37 @@ export async function signUpJoin (
|
||||
first: string
|
||||
last?: string
|
||||
inviteId: string
|
||||
workspaceUrl: string
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const { email, password, first, last, inviteId } = params
|
||||
const { email, password, first, last, inviteId, workspaceUrl } = params
|
||||
|
||||
if (
|
||||
email == null ||
|
||||
email === '' ||
|
||||
password == null ||
|
||||
password === '' ||
|
||||
first == null ||
|
||||
first === '' ||
|
||||
inviteId == null ||
|
||||
inviteId === ''
|
||||
) {
|
||||
if (password == null || password === '' || first == null || first === '') {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
ctx.info('Signing up and joining a workspace using invite', { email, normalizedEmail, first, last, inviteId })
|
||||
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
if (invite == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
const workspaceUuid = await checkInvite(ctx, invite, normalizedEmail)
|
||||
const workspace = await getWorkspaceById(db, workspaceUuid)
|
||||
|
||||
if (workspace == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
|
||||
}
|
||||
const workspaceJoinInfo = await getWorkspaceJoinInfo(ctx, db, email, inviteId, workspaceUrl)
|
||||
ctx.info('Signing up and joining a workspace using invite', {
|
||||
email,
|
||||
normalizedEmail: workspaceJoinInfo.email,
|
||||
first,
|
||||
last,
|
||||
inviteId
|
||||
})
|
||||
|
||||
const { account } = await signUpByEmail(ctx, db, branding, email, password, first, last ?? '', true)
|
||||
void setTimezoneIfNotDefined(ctx, db, account, null, meta)
|
||||
|
||||
return await doJoinByInvite(ctx, db, branding, generateToken(account, workspaceUuid), account, workspace, invite)
|
||||
return await doJoinByInvite(
|
||||
ctx,
|
||||
db,
|
||||
branding,
|
||||
generateToken(account, workspaceJoinInfo.workspace?.uuid),
|
||||
account,
|
||||
workspaceJoinInfo.workspace,
|
||||
workspaceJoinInfo.invite
|
||||
)
|
||||
}
|
||||
|
||||
export async function confirm (
|
||||
@@ -1743,7 +1731,7 @@ export async function isReadOnlyGuest (
|
||||
token: string
|
||||
): Promise<boolean> {
|
||||
const { account } = decodeTokenVerbose(ctx, token)
|
||||
return account === READONLY_GUEST_ACCOUNT
|
||||
return account === readOnlyGuestAccountUuid
|
||||
}
|
||||
|
||||
export async function getPerson (
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
type WorkspaceUuid,
|
||||
type AccountUuid
|
||||
type AccountUuid,
|
||||
readOnlyGuestAccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, unknownError } from '@hcengineering/platform'
|
||||
import { decodeTokenVerbose } from '@hcengineering/server-token'
|
||||
@@ -67,7 +68,6 @@ import {
|
||||
getPersonName,
|
||||
doMergeAccounts,
|
||||
doMergePersons,
|
||||
READONLY_GUEST_ACCOUNT,
|
||||
assignableRoles
|
||||
} from './utils'
|
||||
|
||||
@@ -636,7 +636,7 @@ export async function createIntegration (
|
||||
const { socialId, kind, workspaceUuid, data } = params
|
||||
const social = await db.socialId.findOne({ _id: socialId })
|
||||
|
||||
if (social?.personUuid === READONLY_GUEST_ACCOUNT) {
|
||||
if (social?.personUuid === readOnlyGuestAccountUuid) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,12 @@ export interface WorkspaceInvite {
|
||||
autoJoin?: boolean
|
||||
}
|
||||
|
||||
export interface WorkspaceJoinInfo {
|
||||
email: string
|
||||
workspace: Workspace
|
||||
invite?: WorkspaceInvite | null
|
||||
}
|
||||
|
||||
export interface Mailbox {
|
||||
accountUuid: PersonUuid
|
||||
mailbox: string
|
||||
|
||||
+75
-23
@@ -24,14 +24,15 @@ import {
|
||||
type Person,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
readOnlyGuestAccountUuid,
|
||||
roleOrder,
|
||||
SocialIdType,
|
||||
type SocialKey,
|
||||
systemAccountUuid,
|
||||
type WorkspaceDataId,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
|
||||
type WorkspaceMode,
|
||||
type WorkspaceUuid,
|
||||
type WorkspaceDataId
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
@@ -59,13 +60,13 @@ import {
|
||||
type Workspace,
|
||||
type WorkspaceInfoWithStatus,
|
||||
type WorkspaceInvite,
|
||||
type WorkspaceJoinInfo,
|
||||
type WorkspaceLoginInfo,
|
||||
type WorkspaceStatus
|
||||
} from './types'
|
||||
import { isAdminEmail } from './admin'
|
||||
|
||||
export const GUEST_ACCOUNT = 'b6996120-416f-49cd-841e-e4a5d2e49c9b'
|
||||
export const READONLY_GUEST_ACCOUNT = '83bbed9a-0867-4851-be32-31d49d1d42ce'
|
||||
|
||||
export async function getAccountDB (
|
||||
uri: string,
|
||||
@@ -574,7 +575,7 @@ export async function selectWorkspace (
|
||||
extra = decodedToken.extra
|
||||
} catch (e) {
|
||||
if (workspace?.allowReadOnlyGuest === true) {
|
||||
accountUuid = READONLY_GUEST_ACCOUNT as AccountUuid
|
||||
accountUuid = readOnlyGuestAccountUuid
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
@@ -635,7 +636,7 @@ export async function selectWorkspace (
|
||||
let account = await db.account.findOne({ uuid: accountUuid })
|
||||
|
||||
if ((role == null || account == null) && workspace.allowReadOnlyGuest) {
|
||||
accountUuid = READONLY_GUEST_ACCOUNT as AccountUuid
|
||||
accountUuid = readOnlyGuestAccountUuid
|
||||
role = await db.getWorkspaceRole(accountUuid, workspace.uuid)
|
||||
account = await db.account.findOne({ uuid: accountUuid })
|
||||
}
|
||||
@@ -709,27 +710,31 @@ export async function updateAllowReadOnlyGuests (
|
||||
|
||||
await db.updateAllowReadOnlyGuests(workspace, readOnlyGuestsAllowed)
|
||||
if (!readOnlyGuestsAllowed) {
|
||||
await db.unassignWorkspace(READONLY_GUEST_ACCOUNT as AccountUuid, workspace)
|
||||
await db.unassignWorkspace(readOnlyGuestAccountUuid, workspace)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let guestPerson = await db.person.findOne({ uuid: READONLY_GUEST_ACCOUNT as PersonUuid })
|
||||
let guestPerson = await db.person.findOne({ uuid: readOnlyGuestAccountUuid as PersonUuid })
|
||||
if (guestPerson == null) {
|
||||
await db.person.insertOne({ uuid: READONLY_GUEST_ACCOUNT as PersonUuid, firstName: 'Anonymous', lastName: 'Guest' })
|
||||
await createAccount(db, READONLY_GUEST_ACCOUNT as PersonUuid, true)
|
||||
guestPerson = await db.person.findOne({ uuid: READONLY_GUEST_ACCOUNT as PersonUuid })
|
||||
await db.person.insertOne({
|
||||
uuid: readOnlyGuestAccountUuid as PersonUuid,
|
||||
firstName: 'Anonymous',
|
||||
lastName: 'Guest'
|
||||
})
|
||||
await createAccount(db, readOnlyGuestAccountUuid as PersonUuid, true)
|
||||
guestPerson = await db.person.findOne({ uuid: readOnlyGuestAccountUuid as PersonUuid })
|
||||
}
|
||||
const roleInWorkspace = await db.getWorkspaceRole(READONLY_GUEST_ACCOUNT as AccountUuid, workspace)
|
||||
const roleInWorkspace = await db.getWorkspaceRole(readOnlyGuestAccountUuid, workspace)
|
||||
if (roleInWorkspace == null) {
|
||||
await db.assignWorkspace(READONLY_GUEST_ACCOUNT as AccountUuid, workspace, AccountRole.ReadOnlyGuest)
|
||||
await db.assignWorkspace(readOnlyGuestAccountUuid, workspace, AccountRole.ReadOnlyGuest)
|
||||
}
|
||||
|
||||
const guestAccount = await db.account.findOne({ uuid: READONLY_GUEST_ACCOUNT as AccountUuid })
|
||||
const guestAccount = await db.account.findOne({ uuid: readOnlyGuestAccountUuid })
|
||||
if (guestPerson === null || guestAccount == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {}))
|
||||
}
|
||||
const guestSocialIds = await db.socialId.find({
|
||||
personUuid: READONLY_GUEST_ACCOUNT as PersonUuid,
|
||||
personUuid: readOnlyGuestAccountUuid as PersonUuid,
|
||||
verifiedOn: { $gt: 0 }
|
||||
})
|
||||
|
||||
@@ -748,7 +753,7 @@ export async function updateWorkspaceRole (
|
||||
): Promise<void> {
|
||||
const { targetAccount, targetRole } = params
|
||||
|
||||
if (targetAccount === READONLY_GUEST_ACCOUNT) {
|
||||
if (targetAccount === readOnlyGuestAccountUuid) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
@@ -1157,6 +1162,46 @@ export async function updateArchiveInfo (
|
||||
)
|
||||
}
|
||||
|
||||
export async function getWorkspaceJoinInfo (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
email: string,
|
||||
inviteId: string,
|
||||
workspaceUrl: string
|
||||
): Promise<WorkspaceJoinInfo> {
|
||||
if (email === undefined || email === '' || email === null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
if (inviteId !== undefined && inviteId !== '' && inviteId !== null) {
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
if (invite == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
const workspaceUuid = await checkInvite(ctx, invite, normalizedEmail)
|
||||
const workspace = await getWorkspaceById(db, workspaceUuid)
|
||||
if (workspace == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
|
||||
}
|
||||
return {
|
||||
email,
|
||||
invite,
|
||||
workspace
|
||||
}
|
||||
}
|
||||
if (workspaceUrl !== undefined && workspaceUrl !== '' && workspaceUrl !== null) {
|
||||
const workspace = await getWorkspaceByUrl(db, workspaceUrl)
|
||||
if (workspace == null || !workspace.allowReadOnlyGuest) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
return {
|
||||
email,
|
||||
workspace
|
||||
}
|
||||
}
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
export async function doJoinByInvite (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
@@ -1164,21 +1209,28 @@ export async function doJoinByInvite (
|
||||
token: string,
|
||||
account: AccountUuid,
|
||||
workspace: Workspace,
|
||||
invite: WorkspaceInvite
|
||||
invite: WorkspaceInvite | null | undefined
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const role = await db.getWorkspaceRole(account, workspace.uuid)
|
||||
|
||||
// TODO: should we re-join kicked users? How are they marked as inactive?
|
||||
if (role == null) {
|
||||
await db.assignWorkspace(account, workspace.uuid, invite.role)
|
||||
} else if (getRolePower(role) < getRolePower(invite.role)) {
|
||||
await db.updateWorkspaceRole(account, workspace.uuid, invite.role)
|
||||
if (invite !== undefined && invite != null) {
|
||||
// TODO: should we re-join kicked users? How are they marked as inactive?
|
||||
if (role == null) {
|
||||
await db.assignWorkspace(account, workspace.uuid, invite.role)
|
||||
} else if (getRolePower(role) < getRolePower(invite.role)) {
|
||||
await db.updateWorkspaceRole(account, workspace.uuid, invite.role)
|
||||
}
|
||||
await useInvite(db, invite.id)
|
||||
} else if (workspace.allowReadOnlyGuest) {
|
||||
if (role == null) {
|
||||
await db.assignWorkspace(account, workspace.uuid, AccountRole.Guest)
|
||||
}
|
||||
} else {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
const result = await selectWorkspace(ctx, db, branding, token, { workspaceUrl: workspace.url, kind: 'external' })
|
||||
|
||||
await useInvite(db, invite.id)
|
||||
|
||||
ctx.info('Successfully joined a workspace using invite', {
|
||||
account,
|
||||
workspaceUuid: workspace.uuid,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
BaseMiddleware,
|
||||
type Middleware,
|
||||
type PipelineContext,
|
||||
type TxMiddlewareResult
|
||||
} from '@hcengineering/server-core'
|
||||
import core, {
|
||||
AccountRole,
|
||||
type Doc,
|
||||
hasAccountRole,
|
||||
type MeasureContext,
|
||||
type SessionData,
|
||||
type Space,
|
||||
type Tx,
|
||||
type TxApplyIf,
|
||||
type TxCUD,
|
||||
TxProcessor,
|
||||
type TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
|
||||
export class GuestPermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
static async create (
|
||||
ctx: MeasureContext,
|
||||
context: PipelineContext,
|
||||
next: Middleware | undefined
|
||||
): Promise<GuestPermissionsMiddleware> {
|
||||
return new GuestPermissionsMiddleware(context, next)
|
||||
}
|
||||
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
const account = ctx.contextData.account
|
||||
if (hasAccountRole(account, AccountRole.User)) {
|
||||
return await this.provideTx(ctx, txes)
|
||||
}
|
||||
|
||||
if (account.role === AccountRole.DocGuest || account.role === AccountRole.ReadOnlyGuest) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
for (const tx of txes) {
|
||||
this.processTx(ctx, tx)
|
||||
}
|
||||
|
||||
return await this.provideTx(ctx, txes)
|
||||
}
|
||||
|
||||
private processTx (ctx: MeasureContext<SessionData>, tx: Tx): void {
|
||||
const h = this.context.hierarchy
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
for (const t of applyTx.txes) {
|
||||
this.processTx(ctx, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (TxProcessor.isExtendsCUD(tx._class)) {
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
||||
if (isSpace) {
|
||||
if (this.isForbiddenSpaceTx(cudTx as TxCUD<Space>)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
} else if (cudTx.space !== core.space.DerivedTx && this.isForbiddenTx(cudTx)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isForbiddenTx (tx: TxCUD<Doc>): boolean {
|
||||
if (tx._class === core.class.TxCreateDoc) return false
|
||||
if (tx._class === core.class.TxMixin) return false
|
||||
return !this.hasMixinAccessLevel(tx)
|
||||
}
|
||||
|
||||
private isForbiddenSpaceTx (tx: TxCUD<Space>): boolean {
|
||||
if (tx._class === core.class.TxRemoveDoc) return true
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
return !this.hasMixinAccessLevel(tx)
|
||||
}
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = tx as TxUpdateDoc<Space>
|
||||
const ops = updateTx.operations
|
||||
const keys = ['members', 'private', 'archived', 'owners', 'autoJoin']
|
||||
if (keys.some((key) => (ops as any)[key] !== undefined)) {
|
||||
return true
|
||||
}
|
||||
if (ops.$push !== undefined || ops.$pull !== undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private hasMixinAccessLevel (tx: TxCUD<Doc>): boolean {
|
||||
const h = this.context.hierarchy
|
||||
const accessLevelMixin = h.classHierarchyMixin(tx.objectClass, core.mixin.TxAccessLevel)
|
||||
if (accessLevelMixin === undefined) return false
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
return accessLevelMixin.createAccessLevel === AccountRole.Guest
|
||||
}
|
||||
if (tx._class === core.class.TxRemoveDoc) {
|
||||
return accessLevelMixin.removeAccessLevel === AccountRole.Guest
|
||||
}
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
return accessLevelMixin.updateAccessLevel === AccountRole.Guest
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export * from './model'
|
||||
export * from './modified'
|
||||
export * from './private'
|
||||
export * from './queryJoin'
|
||||
export * from './guestPermissions'
|
||||
export * from './spacePermissions'
|
||||
export * from './spaceSecurity'
|
||||
export * from './triggers'
|
||||
|
||||
@@ -32,8 +32,7 @@ import core, {
|
||||
type TypedSpace,
|
||||
type MeasureContext,
|
||||
type SessionData,
|
||||
type AccountUuid,
|
||||
AccountRole
|
||||
type AccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { type Middleware, type TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
|
||||
@@ -338,11 +337,6 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
}
|
||||
|
||||
protected checkPermissions (ctx: MeasureContext, tx: Tx): void {
|
||||
const account = ctx.contextData.account
|
||||
if (account.role === AccountRole.ReadOnlyGuest) {
|
||||
this.throwForbidden()
|
||||
}
|
||||
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
|
||||
|
||||
@@ -18,11 +18,14 @@ import core, {
|
||||
type AccountUuid,
|
||||
type AttachedDoc,
|
||||
type Class,
|
||||
DOMAIN_MODEL,
|
||||
clone,
|
||||
type Collaborator,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Domain,
|
||||
DOMAIN_MODEL,
|
||||
type FindResult,
|
||||
generateId,
|
||||
type LookupData,
|
||||
type MeasureContext,
|
||||
type ObjQueryType,
|
||||
@@ -32,32 +35,29 @@ import core, {
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
type SessionData,
|
||||
shouldShowArchived,
|
||||
type Space,
|
||||
systemAccountUuid,
|
||||
toFindResult,
|
||||
type Tx,
|
||||
type TxCUD,
|
||||
type TxCreateDoc,
|
||||
type TxCUD,
|
||||
TxProcessor,
|
||||
type TxRemoveDoc,
|
||||
type TxUpdateDoc,
|
||||
type TxWorkspaceEvent,
|
||||
WorkspaceEvent,
|
||||
clone,
|
||||
generateId,
|
||||
shouldShowArchived,
|
||||
systemAccountUuid,
|
||||
toFindResult,
|
||||
type SessionData,
|
||||
type Collaborator
|
||||
WorkspaceEvent
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import {
|
||||
BaseMiddleware,
|
||||
type Middleware,
|
||||
type PipelineContext,
|
||||
type ServerFindOptions,
|
||||
type TxMiddlewareResult,
|
||||
type PipelineContext
|
||||
type TxMiddlewareResult
|
||||
} from '@hcengineering/server-core'
|
||||
import { isOwner, isSystem } from './utils'
|
||||
|
||||
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class' | 'archived'>
|
||||
|
||||
/**
|
||||
@@ -404,35 +404,12 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
}
|
||||
}
|
||||
|
||||
private isForbiddenGuestTx (tx: TxCUD<Space>): boolean {
|
||||
if (tx._class === core.class.TxRemoveDoc) return true
|
||||
if (tx._class === core.class.TxCreateDoc) return false
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = tx as TxUpdateDoc<Space>
|
||||
const ops = updateTx.operations
|
||||
const keys = ['members', 'private', 'archived', 'owners', 'autoJoin']
|
||||
if (keys.some((key) => (ops as any)[key] !== undefined)) {
|
||||
return true
|
||||
}
|
||||
if (ops.$push !== undefined || ops.$pull !== undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async processTx (ctx: MeasureContext<SessionData>, tx: Tx): Promise<void> {
|
||||
const h = this.context.hierarchy
|
||||
if (TxProcessor.isExtendsCUD(tx._class)) {
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
||||
if (isSpace) {
|
||||
const account = ctx.contextData.account
|
||||
if (account.role === AccountRole.Guest) {
|
||||
if (this.isForbiddenGuestTx(cudTx as TxCUD<Space>)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
}
|
||||
await this.handleTx(ctx, cudTx as TxCUD<Space>)
|
||||
} else {
|
||||
await this.handeCollaborator(ctx, cudTx as TxCUD<Collaborator>)
|
||||
@@ -448,10 +425,6 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
||||
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
await this.init(ctx)
|
||||
const account = ctx.contextData.account
|
||||
if (account.role === AccountRole.DocGuest) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
const processed = new Set<Ref<Tx>>()
|
||||
ctx.contextData.contextCache.set('processed', processed)
|
||||
for (const tx of txes) {
|
||||
|
||||
@@ -39,7 +39,8 @@ import {
|
||||
SpaceSecurityMiddleware,
|
||||
TriggersMiddleware,
|
||||
TxMiddleware,
|
||||
UserStatusMiddleware
|
||||
UserStatusMiddleware,
|
||||
GuestPermissionsMiddleware
|
||||
} from '@hcengineering/middleware'
|
||||
import {
|
||||
createBenchmarkAdapter,
|
||||
@@ -125,6 +126,7 @@ export function createServerPipeline (
|
||||
(ctx: MeasureContext, context: PipelineContext, next?: Middleware) =>
|
||||
SpaceSecurityMiddleware.create(opt.adapterSecurity ?? false, ctx, context, next),
|
||||
SpacePermissionsMiddleware.create,
|
||||
GuestPermissionsMiddleware.create,
|
||||
ConfigurationMiddleware.create,
|
||||
ContextNameMiddleware.create,
|
||||
MarkDerivedEntryMiddleware.create,
|
||||
|
||||
Reference in New Issue
Block a user