UBER-496: Fix few issues (#3439)

This commit is contained in:
Andrey Sobolev 2023-06-16 18:29:07 +07:00 committed by GitHub
parent 354e76e1f1
commit 05d273bb59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 277 additions and 61 deletions

5
.vscode/launch.json vendored
View File

@ -69,7 +69,10 @@
"TRANSACTOR_URL": "ws:/localhost:3333",
"ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080",
"SES_URL": "http://localhost:8091"
"SES_URL": "http://localhost:8091",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,

View File

@ -627,6 +627,9 @@ export function createModel (builder: Builder): void {
builder.mixin(core.class.Account, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.EmployeeAccountPresenter
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.EmployeeAccountRefPresenter
})
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.OrganizationPresenter

View File

@ -36,6 +36,7 @@ export default mergeIds(contactId, contact, {
Contacts: '' as AnyComponent,
ContactsTabs: '' as AnyComponent,
EmployeeAccountPresenter: '' as AnyComponent,
EmployeeAccountRefPresenter: '' as AnyComponent,
OrganizationEditor: '' as AnyComponent,
EmployeePresenter: '' as AnyComponent,
EmployeeRefPresenter: '' as AnyComponent,

View File

@ -651,7 +651,7 @@ export function createModel (builder: Builder): void {
const applicantViewOptions = (colors: boolean): ViewOptionsModel => {
const model: ViewOptionsModel = {
groupBy: ['state', 'assignee', 'space'],
groupBy: ['state', 'assignee', 'space', 'createdBy', 'modifiedBy'],
orderBy: [
['state', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],

View File

@ -70,7 +70,7 @@ import {
TimeSpendReport,
trackerId
} from '@hcengineering/tracker'
import { KeyBinding, ViewOptionsModel } from '@hcengineering/view'
import { KeyBinding, ViewAction, ViewOptionsModel } from '@hcengineering/view'
import tracker from './plugin'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
@ -385,7 +385,7 @@ export function createModel (builder: Builder): void {
)
const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
groupBy: ['status', 'assignee', 'priority', 'component', 'milestone'],
groupBy: ['status', 'assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['status', SortingOrder.Ascending],
['priority', SortingOrder.Descending],
@ -554,7 +554,7 @@ export function createModel (builder: Builder): void {
)
const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'milestone'],
groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['rank', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
@ -667,7 +667,7 @@ export function createModel (builder: Builder): void {
attachTo: tracker.class.IssueTemplate,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['assignee', 'priority', 'component', 'milestone'],
groupBy: ['assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
@ -1060,6 +1060,20 @@ export function createModel (builder: Builder): void {
['backlog', tracker.string.Backlog, {}]
]
}
},
{
id: 'all-projects',
component: workbench.component.SpecialView,
icon: view.icon.Archive,
label: tracker.string.AllProjects,
position: 'bottom',
visibleIf: workbench.function.IsOwner,
spaceClass: tracker.class.Project,
componentProps: {
_class: tracker.class.Project,
label: tracker.string.AllIssues,
icon: tracker.icon.Issues
}
}
],
spaces: [
@ -1068,6 +1082,7 @@ export function createModel (builder: Builder): void {
spaceClass: tracker.class.Project,
addSpaceLabel: tracker.string.CreateProject,
createComponent: tracker.component.CreateProject,
visibleIf: tracker.function.IsProjectJoined,
icon: tracker.icon.Home,
specials: [
{
@ -1162,9 +1177,7 @@ export function createModel (builder: Builder): void {
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
query: {
archived: false
},
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
@ -1182,9 +1195,7 @@ export function createModel (builder: Builder): void {
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Project,
query: {
archived: false
},
query: {},
context: {
mode: ['context', 'browser'],
group: 'edit'
@ -1213,6 +1224,28 @@ export function createModel (builder: Builder): void {
},
tracker.action.DeleteProject
)
createAction(builder, {
label: tracker.string.Unarchive,
icon: view.icon.Archive,
action: view.actionImpl.UpdateDocument as ViewAction,
actionProps: {
key: 'archived',
ask: true,
value: false,
label: tracker.string.Unarchive,
message: tracker.string.UnarchiveConfirm
},
input: 'any',
category: tracker.category.Tracker,
query: {
archived: true
},
context: {
mode: ['context', 'browser'],
group: 'tools'
},
target: tracker.class.Project
})
createAction(
builder,
@ -1391,6 +1424,10 @@ export function createModel (builder: Builder): void {
override: [view.action.Open]
})
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
filters: [
'status',
@ -1852,7 +1889,7 @@ export function createModel (builder: Builder): void {
)
const milestoneOptions: ViewOptionsModel = {
groupBy: ['status'],
groupBy: ['status', 'createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['targetDate', SortingOrder.Descending],
@ -1934,7 +1971,7 @@ export function createModel (builder: Builder): void {
)
const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead'],
groupBy: ['lead', 'createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
@ -1973,4 +2010,33 @@ export function createModel (builder: Builder): void {
},
tracker.viewlet.ComponentList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Project,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['createdBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [showColorsViewOption]
},
configOptions: {
strict: true,
hiddenKeys: ['label', 'description']
},
config: [
{
key: '',
props: { kind: 'list' }
},
{ key: '', displayProps: { grow: true } }
]
},
tracker.viewlet.ProjectList
)
}

View File

@ -40,7 +40,10 @@ export default mergeIds(trackerId, tracker, {
CreatedDate: '' as IntlString,
ChangeStatus: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString
ConfigDescription: '' as IntlString,
Unarchive: '' as IntlString,
UnarchiveConfirm: '' as IntlString,
AllProjects: '' as IntlString
},
activity: {
TxIssueCreated: '' as AnyComponent,
@ -62,7 +65,8 @@ export default mergeIds(trackerId, tracker, {
IssueTemplateList: '' as Ref<Viewlet>,
IssueKanban: '' as Ref<Viewlet>,
MilestoneList: '' as Ref<Viewlet>,
ComponentList: '' as Ref<Viewlet>
ComponentList: '' as Ref<Viewlet>,
ProjectList: '' as Ref<Viewlet>
},
ids: {
TxIssueCreated: '' as Ref<TxViewlet>,

View File

@ -13,8 +13,8 @@
// limitations under the License.
//
import { Space } from '@hcengineering/core'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import { Doc, Space } from '@hcengineering/core'
import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import workbench from '@hcengineering/workbench-resources/src/plugin'
@ -32,6 +32,7 @@ export default mergeIds(workbenchId, workbench, {
HiddenApplication: '' as IntlString
},
function: {
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>>
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
IsOwner: '' as Resource<(docs: Doc[]) => Promise<boolean>>
}
})

View File

@ -7,6 +7,18 @@
"heftEvent": "clean",
"actionId": "defaultClean",
"globsToDelete": ["dist", "lib", "temp"]
},
{
"actionKind": "copyFiles",
"heftEvent": "pre-compile",
"actionId": "copy-lang",
"copyOperations": [
{
"sourceFolder": "src",
"destinationFolders": ["lib"],
"includeGlobs": ["lang"]
}
]
}
],
"heftPlugins": [

View File

@ -7,6 +7,18 @@
"heftEvent": "clean",
"actionId": "defaultClean",
"globsToDelete": ["dist", "lib", "temp"]
},
{
"actionKind": "copyFiles",
"heftEvent": "pre-compile",
"actionId": "copy-lang",
"copyOperations": [
{
"sourceFolder": "src",
"destinationFolders": ["lib"],
"includeGlobs": ["lang"]
}
]
}
],
"heftPlugins": [

View File

@ -14,6 +14,7 @@
"WorkspaceNotFound": "Workspace not found",
"InvalidPassword": "Invalid password",
"AccountAlreadyExists": "Account already exists",
"WorkspaceRateLimit": "Server is busy, Please wait a bit and try again",
"AccountAlreadyConfirmed": "Account already confirmed",
"AccountWasMerged": "Account was merged",
"WorkspaceAlreadyExists": "Workspace already exists",

View File

@ -14,6 +14,7 @@
"WorkspaceNotFound": "Рабочее пространство не найдено",
"InvalidPassword": "Неверный пароль",
"AccountAlreadyExists": "Аккаунт уже существует",
"WorkspaceRateLimit": "Сервер перегружен, Пожалуйста подождите",
"AccountAlreadyConfirmed": "Аккаунт уже подтвержден",
"AccountWasMerged": "Аккаунт был объединен",
"WorkspaceAlreadyExists": "Рабочее пространство уже существует",

View File

@ -152,6 +152,7 @@ export default plugin(platformId, {
AccountAlreadyConfirmed: '' as StatusCode<{ account: string }>,
AccountWasMerged: '' as StatusCode<{ account: string }>,
WorkspaceAlreadyExists: '' as StatusCode<{ workspace: string }>,
WorkspaceRateLimit: '' as StatusCode<{ workspace: string }>,
ProductIdMismatch: '' as StatusCode<{ productId: string }>
},
metadata: {

View File

@ -36,7 +36,7 @@
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if employee}
<EmployeePresenter value={employee} {disabled} {inline} {accent} {avatarSize} />
<EmployeePresenter value={employee} {disabled} {inline} {accent} {avatarSize} on:accent-color />
{:else}
<div class="flex-row-center">
<Avatar size={avatarSize} />

View File

@ -30,5 +30,5 @@
</script>
{#if account}
<EmployeeAccountPresenter value={account} {disabled} {inline} {avatarSize} />
<EmployeeAccountPresenter value={account} {disabled} {inline} {avatarSize} on:accent-color />
{/if}

View File

@ -68,6 +68,7 @@ import EmployeePresenter from './components/EmployeePresenter.svelte'
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import MemberPresenter from './components/MemberPresenter.svelte'
import Members from './components/Members.svelte'
import MembersBox from './components/MembersBox.svelte'
import MembersPresenter from './components/MembersPresenter.svelte'
import MergeEmployee from './components/MergeEmployee.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte'
@ -141,7 +142,8 @@ export {
UserInfo,
IconMembers,
SelectAvatars,
UserBoxItems
UserBoxItems,
MembersBox
}
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@ -311,7 +313,8 @@ export default async (): Promise<Resources> => ({
EmployeeFilter,
EmployeeFilterValuePresenter,
EmployeeAccountFilterValuePresenter,
DeleteConfirmationPopup
DeleteConfirmationPopup,
EmployeeAccountRefPresenter
},
completion: {
EmployeeQuery: async (

View File

@ -18,7 +18,8 @@
import type { Ref } from '@hcengineering/core'
import core from '@hcengineering/core'
import { Panel } from '@hcengineering/panel'
import { createQuery, getClient, MembersBox } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import { MembersBox } from '@hcengineering/contact-resources'
import type { Funnel } from '@hcengineering/lead'
import { FullDescriptionBox } from '@hcengineering/text-editor'
import { EditBox, Grid } from '@hcengineering/ui'

View File

@ -49,7 +49,7 @@
$: queries = { assigned, created, subscribed }
$: mode = $resolvedLocationStore.query?.mode ?? undefined
let searchQuery: DocumentQuery<Lead> = { ...baseQuery }
let searchQuery: DocumentQuery<Lead> = { ...(baseQuery ?? {}) }
function updateSearchQuery (search: string): void {
searchQuery = search === '' ? { ...baseQuery } : { ...baseQuery, $search: search }
}
@ -72,11 +72,11 @@
)
}
$: if (mode === 'subscribed') getSubscribed()
$: if (mode === undefined || queries[mode] === undefined) {
$: if (mode === undefined || (queries as any)[mode] === undefined) {
;[[mode]] = config
}
$: if (mode !== undefined) {
baseQuery = { ...queries[mode] }
baseQuery = { ...((queries as any)[mode] ?? {}) }
modeSelectorProps = {
config,
mode,
@ -101,6 +101,9 @@
.findOne<Viewlet>(view.class.Viewlet, { attachTo: _class, descriptor: task.viewlet.StatusTable })
.then((res) => {
viewlet = res
if (res == null) {
return
}
preferenceQuery.query(
view.class.ViewletPreference,
{

View File

@ -27,7 +27,7 @@
"WantAnotherWorkspace": "Want to create another workspace?",
"ChangeAccount": "Change account",
"NotSeeingWorkspace": "Not seeing your workspace?",
"WorkspaceNameRule": "The workspace name can contain lowercase letters, numbers and symbol -",
"WorkspaceNameRule": "The workspace name can contain lowercase letters, numbers and symbol - (inside name)",
"ForgotPassword": "Forgot your password?",
"KnowPassword": "Know your password?",
"Recover": "Recover",

View File

@ -27,7 +27,7 @@
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
"ChangeAccount": "Сменить пользователя",
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?",
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символа -",
"WorkspaceNameRule": "Название рабочего пространства должно состояить из строчных латинских букв, цифр и символа - (внутри имени)",
"ForgotPassword": "Забыли пароль?",
"KnowPassword": "Знаете пароль?",
"Recover": "Восстановить",

View File

@ -28,7 +28,7 @@
{
name: 'workspace',
i18n: login.string.Workspace,
rule: /^[0-9a-z\-)(]{3,63}$/,
rule: /^[0-9a-z][0-9a-z-]{2,62}[0-9a-z]$/,
ruleDescr: login.string.WorkspaceNameRule
}
]

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { Comment } from '@hcengineering/chunter'
import { updateBacklinks } from '@hcengineering/chunter-resources/src/backlinks'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import { EmployeeAccount } from '@hcengineering/contact'
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'

View File

@ -15,6 +15,6 @@
"Field": "Поле",
"TemplateCategory": "Группа шаблонов",
"CreateTemplateCategory": "Создать группу",
"Copy": "Копировать"
"Copy": "Копировать"
}
}

View File

@ -273,7 +273,10 @@
"NoStatusFound": "No matching status found",
"CreateMissingStatus": "Create missing status",
"UnsetParent": "Parent issue will be unset"
"UnsetParent": "Parent issue will be unset",
"Unarchive": "Unarchive",
"UnarchiveConfirm": "Do you want to unarchive project?",
"AllProjects": "All projects"
},
"status": {}
}

View File

@ -272,7 +272,10 @@
"ProjectEmojiiCategory": "Эмодзи",
"NoStatusFound": "Статус не найдет",
"CreateMissingStatus": "Создать отсутствущий статус",
"UnsetParent": "Родительская задача будет убрана"
"UnsetParent": "Родительская задача будет убрана",
"Unarchive": "Разархивировать",
"UnarchiveConfirm": "Вы действительно хотите разархивировать?",
"AllProjects": "All projects"
},
"status": {}
}

View File

@ -13,7 +13,17 @@
// limitations under the License.
//
import { Class, Client, Doc, DocumentQuery, Ref, RelatedDocument, toIdMap, TxOperations } from '@hcengineering/core'
import {
Class,
Client,
Doc,
DocumentQuery,
getCurrentAccount,
Ref,
RelatedDocument,
toIdMap,
TxOperations
} from '@hcengineering/core'
import { Resources, translate } from '@hcengineering/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Milestone, Project } from '@hcengineering/tracker'
@ -419,7 +429,8 @@ export default async (): Promise<Resources> => ({
GetAllPriority: getAllPriority,
GetAllComponents: getAllComponents,
GetAllMilestones: getAllMilestones,
GetVisibleFilters: getVisibleFilters
GetVisibleFilters: getVisibleFilters,
IsProjectJoined: async (project: Project) => !project.private || project.members.includes(getCurrentAccount()._id)
},
actionImpl: {
Move: move,

View File

@ -21,11 +21,11 @@ import {
CreateAggregationManagerFunc,
GetAllValuesFunc,
GrouppingManagerResource,
KeyFilter,
SortFunc,
Viewlet,
ViewletDescriptor,
ViewQueryAction,
KeyFilter
Viewlet,
ViewletDescriptor
} from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib'
@ -382,7 +382,8 @@ export default mergeIds(trackerId, tracker, {
GetAllPriority: '' as GetAllValuesFunc,
GetAllComponents: '' as GetAllValuesFunc,
GetAllMilestones: '' as GetAllValuesFunc,
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>
},
aggregation: {
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import {
@ -50,6 +50,7 @@
export let createComponentProps: Record<string, any> = {}
export let isCreationDisabled = false
export let descriptors: Ref<ViewletDescriptor>[] | undefined = undefined
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
let search = ''
let viewlet: WithLookup<Viewlet> | undefined
@ -116,7 +117,7 @@
preferenceQuery.unsubscribe()
}
$: query = viewlet?.baseQuery ?? {}
$: query = { ...(baseQuery ?? {}), ...(viewlet?.baseQuery ?? {}) }
$: searchQuery = search === '' ? query : { ...query, $search: search }
$: resultQuery = searchQuery
@ -186,7 +187,7 @@
</div>
</div>
{#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}}
{#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}
<Loading />
{:else}
<FilterBar

View File

@ -117,6 +117,32 @@
return await getResource(value.presenter)
}
}
let visibleIf: ((space: Space) => Promise<boolean>) | undefined
$: if (model.visibleIf) {
getResource(model.visibleIf).then((r) => {
visibleIf = r
})
}
let filteredSpaces: Space[] = []
async function updateSpaces (spaces: Space[], visibleIf: (space: Space) => Promise<boolean>): Promise<void> {
const result: Space[] = []
for (const s of spaces) {
if (await visibleIf(s)) {
result.push(s)
}
}
filteredSpaces = result
}
$: if (visibleIf) {
updateSpaces(spaces, visibleIf)
} else {
filteredSpaces = spaces
}
</script>
<TreeNode
@ -125,7 +151,7 @@
actions={async () => getParentActions()}
shortDropbox={model.specials !== undefined}
>
{#each spaces as space, i (space._id)}
{#each filteredSpaces as space, i (space._id)}
{#await getPresenter(space._class) then presenter}
{#if separate && model.specials && i !== 0}<TreeSeparator line />{/if}
{#if model.specials && presenter}

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Space } from '@hcengineering/core'
import { AccountRole, Space, getCurrentAccount } from '@hcengineering/core'
import { Resources } from '@hcengineering/platform'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Archive from './components/Archive.svelte'
@ -42,7 +42,8 @@ export default async (): Promise<Resources> => ({
Workbench
},
function: {
HasArchiveSpaces: hasArchiveSpaces
HasArchiveSpaces: hasArchiveSpaces,
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner
},
actionImpl: {
Navigate: doNavigate

View File

@ -59,6 +59,8 @@ export interface SpacesNavModel {
// Child special items.
specials?: SpecialNavModel[]
visibleIf?: Resource<(space: Space) => Promise<boolean>>
}
/**

View File

@ -97,7 +97,6 @@ export function serveAccount (methods: Record<string, AccountMethod>, productId
}
const db = client.db(ACCOUNT_DB)
const result = await method(db, productId, request, token)
console.log(result)
ctx.body = result
})

View File

@ -104,6 +104,7 @@ export interface Account {
// Defined for server admins only
admin?: boolean
confirmed?: boolean
lastWorkspace?: number
}
/**
@ -115,6 +116,7 @@ export interface Workspace {
organisation: string
accounts: ObjectId[]
productId: string
disabled?: boolean
}
/**
@ -285,6 +287,9 @@ export async function selectWorkspace (
const workspaceInfo = await getWorkspace(db, productId, workspace)
if (workspaceInfo !== null) {
if (workspaceInfo.disabled === true) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
}
const workspaces = accountInfo.workspaces
for (const w of workspaces) {
@ -347,6 +352,7 @@ export async function join (
): Promise<WorkspaceLoginInfo> {
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
console.log(`join attempt:${email}, ${workspace.name}`)
await assignWorkspace(db, productId, email, workspace.name)
const token = (await login(db, productId, email, password)).token
@ -360,6 +366,7 @@ export async function join (
*/
export async function confirmEmail (db: Db, email: string): Promise<Account> {
const account = await getAccount(db, email)
console.log(`confirm email:${email}`)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountId }))
@ -446,6 +453,7 @@ export async function signUpJoin (
last: string,
inviteId: ObjectId
): Promise<WorkspaceLoginInfo> {
console.log(`signup join:${email} ${first} ${last}`)
const invite = await getInvite(db, inviteId)
const workspace = await checkInvite(invite, email)
await createAcc(db, productId, email, password, first, last, invite?.emailMask === email)
@ -529,6 +537,7 @@ export async function createAccount (
export async function listWorkspaces (db: Db, productId: string): Promise<WorkspaceInfoOnly[]> {
return (await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, {})).toArray())
.map((it) => ({ ...it, productId }))
.filter((it) => it.disabled !== true)
.map(trimWorkspace)
}
@ -612,20 +621,59 @@ export async function upgradeWorkspace (
export const createUserWorkspace =
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
if (!/^[0-9a-z][0-9a-z-]{2,62}[0-9a-z]$/.test(workspace)) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidId, { id: workspace }))
}
const { email, extra } = decodeToken(token)
if (extra?.confirmed === false) {
const nonConfirmed = extra?.confirmed === false
console.log(`Creating workspace ${workspace} for ${email} ${nonConfirmed ? 'non confirmed' : 'confirmed'}`)
if (nonConfirmed) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
}
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
const info = await getAccount(db, email)
if (info === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
}
if (info.lastWorkspace !== undefined) {
if (Date.now() - info.lastWorkspace < 60 * 1000) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceRateLimit, { workspace }))
}
}
try {
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
} catch (err: any) {
console.error(err)
// We need to drop workspace, to prevent wrong data usage.
const ws = await getWorkspace(db, productId, workspace)
if (ws === null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace }))
}
await db.collection(WORKSPACE_COLLECTION).updateOne(
{
_id: ws._id
},
{ $set: { disabled: true } }
)
throw err
}
info.lastWorkspace = Date.now()
// Update last workspace time.
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
await assignWorkspace(db, productId, email, workspace)
await setRole(email, workspace, productId, AccountRole.Owner)
const info = await getAccount(db, email)
const result = {
endpoint: getEndpoint(),
email,
token: generateToken(email, getWorkspaceId(workspace, productId), getExtra(info)),
productId
}
console.log(`Creating workspace ${workspace} Done`)
return result
}
@ -678,7 +726,9 @@ export async function getUserWorkspaces (db: Db, productId: string, token: strin
.collection<Workspace>(WORKSPACE_COLLECTION)
.find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } }))
.toArray()
).map(trimWorkspace)
)
.filter((it) => it.disabled !== true)
.map(trimWorkspace)
}
async function getWorkspaceAndAccount (

View File

@ -57,17 +57,24 @@ export class MinioService {
}
async list (workspaceId: WorkspaceId, prefix?: string): Promise<MinioWorkspaceItem[]> {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
const list = await this.client.listObjects(getBucketId(workspaceId), prefix, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
try {
const items = new Map<string, BucketItem & { metaData: ItemBucketMetadata }>()
const list = await this.client.listObjects(getBucketId(workspaceId), prefix, true)
await new Promise((resolve) => {
list.on('data', (data) => {
items.set(data.name, { metaData: {}, ...data })
})
list.on('end', () => {
resolve(null)
})
})
list.on('end', () => {
resolve(null)
})
})
return Array.from(items.values())
return Array.from(items.values())
} catch (err: any) {
if (((err?.message as string) ?? '').includes('Invalid bucket name')) {
return []
}
throw err
}
}
async stat (workspaceId: WorkspaceId, objectName: string): Promise<BucketItemStat> {