QFIX: Admin panel (#7953)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-02-06 22:51:31 +07:00 committed by GitHub
parent 529230ff68
commit 931ed82403
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 48 additions and 43 deletions

View File

@ -17,7 +17,12 @@
"dom"
],
"incremental": true,
"types": ["jest"],
"types": [
"jest"
],
"isolatedModules": true
}
},
"exclude": [
"node_modules/**"
]
}

View File

@ -745,9 +745,9 @@ export function decodeTokenPayload (token: string): any {
}
export function isAdminUser (): boolean {
// TODO: fixme
return false
// return decodeTokenPayload(getMetadata(plugin.metadata.Token) ?? '').admin === 'true'
const decodedToken = decodeTokenPayload(getMetadata(plugin.metadata.Token) ?? '')
console.log('decodedToken', decodedToken)
return decodedToken.extra?.admin === 'true'
}
export function isSpace (space: Doc): space is Space {

View File

@ -9,7 +9,7 @@
isUpgradingMode,
reduceCalls,
versionToString,
type BaseWorkspaceInfo
type WorkspaceInfoWithStatus
} from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { isAdminUser, MessageBox } from '@hcengineering/presentation'
@ -30,7 +30,8 @@
ticker
} from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation, type RegionInfo } from '../utils'
import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation } from '../utils'
import { RegionInfo } from '@hcengineering/account-client'
$: now = $ticker
@ -43,7 +44,7 @@
window.open(url, '_blank')
}
type WorkspaceInfo = BaseWorkspaceInfo & { attempts: number }
type WorkspaceInfo = WorkspaceInfoWithStatus & { attempts: number }
let workspaces: WorkspaceInfo[] = []
@ -73,9 +74,9 @@
$: sortedWorkspaces = workspaces
.filter(
(it) =>
((it.workspaceName?.includes(search) ?? false) ||
(it.workspaceUrl?.includes(search) ?? false) ||
it.workspace?.includes(search) ||
((it.name?.includes(search) ?? false) ||
(it.url?.includes(search) ?? false) ||
it.uuid?.includes(search) ||
it.createdBy?.includes(search)) &&
((showActive && isActiveMode(it.mode)) ||
(showArchived && isArchivingMode(it.mode)) ||
@ -92,7 +93,7 @@
case SortingRule.LastVisit:
return (b.lastVisit ?? 0) - (a.lastVisit ?? 0)
}
return (b.workspaceUrl ?? b.workspace).localeCompare(a.workspaceUrl ?? a.workspace)
return (b.url ?? b.uuid).localeCompare(a.url ?? a.uuid)
})
let backupIdx = new Map<string, number>()
@ -156,7 +157,7 @@
backupable = mixedBackupSorting
for (const [idx, it] of mixedBackupSorting.entries()) {
newBackupIdx.set(it.workspace, idx)
newBackupIdx.set(it.uuid, idx)
}
backupIdx = newBackupIdx
}
@ -184,7 +185,7 @@
let showOther: boolean = true
$: groupped = groupByArray(sortedWorkspaces, (it) => {
const lastUsageDays = Math.round((now - it.lastVisit) / (1000 * 3600 * 24))
const lastUsageDays = Math.round((now - (it.lastVisit ?? 0)) / (1000 * 3600 * 24))
return Object.entries(dayRanges).find(([_k, v]) => lastUsageDays <= v)?.[0] ?? 'Other'
})
@ -202,10 +203,10 @@
$: byVersion = groupByArray(
workspaces.filter((it) => {
const lastUsed = Math.round((now - it.lastVisit) / (1000 * 3600 * 24))
const lastUsed = Math.round((now - (it.lastVisit ?? 0)) / (1000 * 3600 * 24))
return isActiveMode(it.mode) && lastUsed < 1
}),
(it) => versionToString(it.version ?? { major: 0, minor: 0, patch: 0 })
(it) => versionToString({ major: it.versionMajor, minor: it.versionMinor, patch: it.versionPatch })
)
let superAdminMode = false
@ -334,7 +335,7 @@
message: getEmbeddedLabel(`Please confirm archive ${archivedV.length} workspaces`),
action: async () => {
void performWorkspaceOperation(
archivedV.map((it) => it.workspace),
archivedV.map((it) => it.uuid),
'archive'
)
}
@ -354,7 +355,7 @@
message: getEmbeddedLabel(`Please confirm migrate ${archivedV.length} workspaces`),
action: async () => {
await performWorkspaceOperation(
activeV.map((it) => it.workspace),
activeV.map((it) => it.uuid),
'migrate-to',
selectedRegionId
)
@ -365,9 +366,9 @@
{/if}
</svelte:fragment>
{#each v.slice(0, limit) as workspace}
{@const wsName = workspace.workspaceName ?? workspace.workspace}
{@const lastUsageDays = Math.round((now - workspace.lastVisit) / (1000 * 3600 * 24))}
{@const bIdx = backupIdx.get(workspace.workspace)}
{@const wsName = workspace.name}
{@const lastUsageDays = Math.round((now - (workspace.lastVisit ?? 0)) / (1000 * 3600 * 24))}
{@const bIdx = backupIdx.get(workspace.uuid)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex fs-title cursor-pointer focused-button bordered">
@ -375,11 +376,7 @@
<span class="label overflow-label flex-row-center" style:width={'12rem'}>
{wsName}
<div class="ml-1">
<Button
icon={IconOpen}
size={'small'}
on:click={() => select(workspace.workspaceUrl ?? workspace.workspace)}
/>
<Button icon={IconOpen} size={'small'} on:click={() => select(workspace.url)} />
</div>
</span>
<div class="ml-1" style:width={'12rem'}>
@ -400,12 +397,9 @@
{workspace.attempts}
</span>
<!-- <span class="flex flex-between select-text overflow-label" style:width={'25rem'}>
{workspace.workspace}
</span> -->
<span class="flex flex-between" style:width={'5rem'}>
{#if workspace.progress !== 100 && workspace.progress !== 0}
({workspace.progress}%)
{#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
({workspace.processingProgress}%)
{/if}
</span>
<span class="flex flex-between" style:width={'5rem'}>
@ -447,10 +441,10 @@
kind={'ghost'}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Archive ${workspace.workspaceUrl}`),
label: getEmbeddedLabel(`Archive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.workspace, 'archive')
await performWorkspaceOperation(workspace.uuid, 'archive')
}
})
}}
@ -465,10 +459,10 @@
label={getEmbeddedLabel('Unarchive')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Unarchive ${workspace.workspaceUrl}`),
label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.workspace, 'unarchive')
await performWorkspaceOperation(workspace.uuid, 'unarchive')
}
})
}}
@ -483,10 +477,10 @@
label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Migrate ${workspace.workspaceUrl}`),
label: getEmbeddedLabel(`Migrate ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.workspace, 'migrate-to', selectedRegionId)
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
}
})
}}
@ -501,10 +495,10 @@
label={getEmbeddedLabel('Delete')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Delete ${workspace.workspaceUrl}`),
label: getEmbeddedLabel(`Delete ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.workspace, 'delete')
await performWorkspaceOperation(workspace.uuid, 'delete')
}
})
}}

View File

@ -272,7 +272,10 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection<WorkspaceS
}
async find (query: Query<WorkspaceStatus>, sort?: Sort<WorkspaceStatus>, limit?: number): Promise<WorkspaceStatus[]> {
return (await this.wsCollection.find(this.toWsQuery(query), this.toWsSort(sort), limit)).map((ws) => ws.status)
return (await this.wsCollection.find(this.toWsQuery(query), this.toWsSort(sort), limit)).map((ws) => ({
...ws.status,
workspaceUuid: ws.uuid
}))
}
async findOne (query: Query<WorkspaceStatus>): Promise<WorkspaceStatus | null> {

View File

@ -91,6 +91,8 @@ import {
// Move to config?
const processingTimeoutMs = 30 * 1000
const ADMIN_EMAILS = new Set(process.env.ADMIN_EMAILS?.split(',') ?? [])
/* =================================== */
/* ============OPERATIONS============= */
/* =================================== */
@ -126,11 +128,12 @@ export async function login (
const isConfirmed = emailSocialId.verifiedOn != null
ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId })
const isAdmin: Record<string, string> = ADMIN_EMAILS.has(email.trim()) ? { admin: 'true' } : {}
ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId, ...isAdmin })
return {
account: existingAccount.uuid,
token: isConfirmed ? generateToken(existingAccount.uuid) : undefined
token: isConfirmed ? generateToken(existingAccount.uuid, undefined, isAdmin) : undefined
}
} catch (err: any) {
Analytics.handleError(err)
@ -835,7 +838,7 @@ export async function listWorkspaces (
): Promise<WorkspaceInfoWithStatus[]> {
const { extra } = decodeTokenVerbose(ctx, token)
if (!['tool', 'backup', 'admin'].includes(extra?.service)) {
if (!['tool', 'backup', 'admin'].includes(extra?.service) && extra?.admin !== 'true') {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}