From 8de24ea7973d5c504d7457b36f81f166b12042b4 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Sun, 2 Mar 2025 22:31:25 +0700 Subject: [PATCH] UBERF-9537: Fix Invalid navigate to guest not authorised (#8121) Signed-off-by: Andrey Sobolev --- packages/presentation/src/utils.ts | 9 ++++- packages/ui/src/location.ts | 4 ++ .../src/components/Guest.svelte | 11 ++--- .../src/components/GuestApp.svelte | 15 ++++++- plugins/guest-resources/src/connect.ts | 22 +++++----- .../src/components/SelectWorkspace.svelte | 23 +---------- plugins/login-resources/src/utils.ts | 30 ++++++++++++-- .../src/components/SelectWorkspaceMenu.svelte | 21 +++++++--- .../src/components/Workbench.svelte | 12 ++++-- server/account/src/__tests__/token.test.ts | 40 +++++++++++++++++++ server/token/src/token.ts | 9 ++++- .../sanity/tests/model/tracker/issues-page.ts | 4 +- .../sanity/tests/workspace/archive.spec.ts | 13 +++--- 13 files changed, 147 insertions(+), 66 deletions(-) create mode 100644 server/account/src/__tests__/token.test.ts diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 087901f98f..40bc45866a 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -737,7 +737,14 @@ export function isCollabAttr (hierarchy: Hierarchy, key: KeyedAttribute): boolea */ export function decodeTokenPayload (token: string): any { try { - return JSON.parse(atob(token.split('.')[1])) + if (token === '') { + return {} + } + const tsplit = token.split('.') + if (tsplit.length < 2) { + return {} + } + return JSON.parse(atob(tsplit[1])) } catch (err: any) { console.error(err) return {} diff --git a/packages/ui/src/location.ts b/packages/ui/src/location.ts index 17bcedbbfc..3633784509 100644 --- a/packages/ui/src/location.ts +++ b/packages/ui/src/location.ts @@ -234,6 +234,10 @@ export const setTreeCollapsed = (_id: any, collapsed: boolean, prefix?: string): collapsed ? localStorage.setItem(key, COLLAPSED) : localStorage.removeItem(key) } +export function isSameSegments (a: PlatformLocation, b: PlatformLocation, len = 2): boolean { + return a.path.slice(0, len).every((segment, index) => segment === b.path[index]) +} + export function restoreLocation (loc: PlatformLocation, app: Plugin): void { const last = localStorage.getItem(`${locationStorageKeyId}_${app}`) diff --git a/plugins/guest-resources/src/components/Guest.svelte b/plugins/guest-resources/src/components/Guest.svelte index 7004a28a40..5e542dd7be 100644 --- a/plugins/guest-resources/src/components/Guest.svelte +++ b/plugins/guest-resources/src/components/Guest.svelte @@ -26,25 +26,22 @@ Popup, PopupAlignment, ResolvedLocation, - Separator, TooltipInstance, areLocationsEqual, closePanel, - getCurrentLocation, - getLocation, - navigate, - showPanel, defineSeparators, + deviceOptionsStore as deviceInfo, + getCurrentLocation, setResolvedLocation, - deviceOptionsStore as deviceInfo + showPanel } from '@hcengineering/ui' import view from '@hcengineering/view' import { ListSelectionProvider, parseLinkId, restrictionStore, updateFocus } from '@hcengineering/view-resources' import workbench, { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench' import { SpaceView, buildNavModel } from '@hcengineering/workbench-resources' + import { workbenchGuestSeparators } from '..' import guest from '../plugin' import { checkAccess } from '../utils' - import { workbenchGuestSeparators } from '..' const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? [] $deviceInfo.navigator.visible = false diff --git a/plugins/guest-resources/src/components/GuestApp.svelte b/plugins/guest-resources/src/components/GuestApp.svelte index f4b71de44a..e055e45464 100644 --- a/plugins/guest-resources/src/components/GuestApp.svelte +++ b/plugins/guest-resources/src/components/GuestApp.svelte @@ -16,18 +16,24 @@ import { getMetadata } from '@hcengineering/platform' import { Label, Loading, Notifications, location } from '@hcengineering/ui' import { upgradeDownloadProgress } from '@hcengineering/presentation' - import { connect, versionError } from '../connect' + import { connect, versionError, invalidError } from '../connect' import { guestId } from '@hcengineering/guest' import workbench from '@hcengineering/workbench' import Guest from './Guest.svelte' + import plugin from '../plugin' {#if $location.path[0] === guestId} {#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')} {:then client} - {#if $versionError} + {#if $invalidError} + {invalidError} +
+

+
+ {:else if $versionError}

@@ -52,14 +58,19 @@ diff --git a/plugins/guest-resources/src/connect.ts b/plugins/guest-resources/src/connect.ts index 3b021cff51..a32aa9a952 100644 --- a/plugins/guest-resources/src/connect.ts +++ b/plugins/guest-resources/src/connect.ts @@ -9,7 +9,7 @@ import core, { type Client, type Version } from '@hcengineering/core' -import login, { loginId } from '@hcengineering/login' +import login from '@hcengineering/login' import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' import presentation, { closeClient, @@ -23,12 +23,13 @@ import { desktopPlatform, fetchMetadataLocalStorage, getCurrentLocation, - navigate, setMetadataLocalStorage } from '@hcengineering/ui' import { writable, get } from 'svelte/store' export const versionError = writable(undefined) + +export const invalidError = writable(false) const versionStorageKey = 'last_server_version' let _token: string | undefined @@ -40,19 +41,16 @@ export async function connect (title: string): Promise { const token = loc.query?.token const ws = loc.path[1] if (ws === undefined || token == null) { - navigate({ - path: [loginId] - }) + invalidError.set(true) return } - setMetadata(presentation.metadata.Token, token) const selectWorkspace = await getResource(login.function.SelectWorkspace) const workspaceLoginInfo = (await selectWorkspace(ws, token))[1] if (workspaceLoginInfo == null) { - navigate({ - path: [loginId] - }) + // We just need to show guist link is revoked or invalid. + invalidError.set(true) + clearMetadata(ws) return } @@ -119,10 +117,7 @@ export async function connect (title: string): Promise { }, onUnauthorized: () => { clearMetadata(ws) - navigate({ - path: [loginId], - query: {} - }) + invalidError.set(true) }, // We need to refresh all active live queries and clear old queries. onConnect: async (event: ClientConnectEvent, data: any) => { @@ -214,6 +209,7 @@ export async function connect (title: string): Promise { } } + invalidError.set(false) versionError.set(undefined) // Update window title document.title = [ws, title].filter((it) => it).join(' - ') diff --git a/plugins/login-resources/src/components/SelectWorkspace.svelte b/plugins/login-resources/src/components/SelectWorkspace.svelte index 2d28355ec4..88373f7c21 100644 --- a/plugins/login-resources/src/components/SelectWorkspace.svelte +++ b/plugins/login-resources/src/components/SelectWorkspace.svelte @@ -17,7 +17,7 @@ import { isActiveMode, isArchivingMode, isRestoringMode, isUpgradingMode } from '@hcengineering/core' import { LoginInfo, Workspace } from '@hcengineering/login' import { OK, Severity, Status } from '@hcengineering/platform' - import presentation, { NavLink, isAdminUser, reduceCalls } from '@hcengineering/presentation' + import presentation, { NavLink, reduceCalls } from '@hcengineering/presentation' import MessageBox from '@hcengineering/presentation/src/components/MessageBox.svelte' import { Button, @@ -117,8 +117,6 @@ throw err } } - $: isAdmin = isAdminUser() - let search: string = '' @@ -135,7 +133,7 @@
- {#if isAdmin} + {#if workspaces.length > 10}
@@ -169,24 +167,7 @@ {/if} - {#if isAdmin} - {workspace.workspace} - {#if workspace.region !== undefined} - at ({workspace.region}) - {/if} - {/if}
- {#if isAdmin} - {#if workspace.backupInfo != null} - {@const sz = workspace.backupInfo.dataSize + workspace.backupInfo.blobsSize} - {@const szGb = Math.round((sz * 100) / 1024) / 100} - {#if szGb > 0} - - {Math.round((sz * 100) / 1024) / 100}Gb - - {:else} - - {Math.round(sz)}Mb - - {/if} - {/if} - {/if} ({lastUsageDays} days)
diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index cadb487406..3f785416e4 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -31,6 +31,7 @@ import presentation, { isAdminUser } from '@hcengineering/presentation' import { fetchMetadataLocalStorage, getCurrentLocation, + isSameSegments, locationStorageKeyId, locationToUrl, navigate, @@ -409,6 +410,20 @@ export async function getAccount (doNavigate: boolean = true): Promise
- {decodeTokenPayload(getMetadata(presentation.metadata.Token) ?? '').workspace} + {decodeTokenPayload(getMetadata(presentation.metadata.Token) ?? '').workspace ?? ''}
{/if}
diff --git a/plugins/workbench-resources/src/components/Workbench.svelte b/plugins/workbench-resources/src/components/Workbench.svelte index 6368091555..0b6dd8307b 100644 --- a/plugins/workbench-resources/src/components/Workbench.svelte +++ b/plugins/workbench-resources/src/components/Workbench.svelte @@ -78,7 +78,8 @@ showPopup, TooltipInstance, workbenchSeparators, - resizeObserver + resizeObserver, + isSameSegments } from '@hcengineering/ui' import view from '@hcengineering/view' import { @@ -429,9 +430,12 @@ const last = localStorage.getItem(`${locationStorageKeyId}_${loc.path[1]}`) if (last != null) { const lastValue = JSON.parse(last) - navigateDone = navigate(lastValue) - if (navigateDone) { - return + + if (isSameSegments(lastValue, loc, 2)) { + navigateDone = navigate(lastValue) + if (navigateDone) { + return + } } } if (app === undefined && !navigateDone) { diff --git a/server/account/src/__tests__/token.test.ts b/server/account/src/__tests__/token.test.ts new file mode 100644 index 0000000000..af878183f2 --- /dev/null +++ b/server/account/src/__tests__/token.test.ts @@ -0,0 +1,40 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { generateToken } from '@hcengineering/server-token' + +export function decodeTokenPayload (token: string): any { + try { + return JSON.parse(atob(token.split('.')[1])) + } catch (err: any) { + console.error(err) + return {} + } +} +describe('generate-tokens', () => { + it('should generate tokens', async () => { + const extra: Record = { confirmed: 'true' } + const token = generateToken('mike@some.host', { name: '' }, extra, 'secret') + console.log(token) + const decodedPayload = decodeTokenPayload(token) + expect(decodedPayload).toEqual({ + confirmed: 'true', + email: 'mike@some.host', + workspace: '' + }) + expect(decodedPayload.admin).not.toBe('true') + }) +}) diff --git a/server/token/src/token.ts b/server/token/src/token.ts index b345f6c273..4d7e0bdba9 100644 --- a/server/token/src/token.ts +++ b/server/token/src/token.ts @@ -19,8 +19,13 @@ const getSecret = (): string => { /** * @public */ -export function generateToken (email: string, workspace: WorkspaceId, extra?: Record): string { - return encode({ ...(extra ?? {}), email, workspace: workspace.name }, getSecret()) +export function generateToken ( + email: string, + workspace: WorkspaceId, + extra?: Record, + secret?: string +): string { + return encode({ ...(extra ?? {}), email, workspace: workspace.name }, secret ?? getSecret()) } /** diff --git a/tests/sanity/tests/model/tracker/issues-page.ts b/tests/sanity/tests/model/tracker/issues-page.ts index 52123ff600..6a2b87ebcc 100644 --- a/tests/sanity/tests/model/tracker/issues-page.ts +++ b/tests/sanity/tests/model/tracker/issues-page.ts @@ -167,7 +167,9 @@ export class IssuesPage extends CommonTrackerPage { appleseedJohnButton = (): Locator => this.page.locator('button.menu-item:has-text("Appleseed John")') estimationEditor = (): Locator => this.page.locator('#estimation-editor') dueDateButton = (): Locator => this.page.locator('button:has-text("Due date")') - specificDay = (day: string): Locator => this.page.locator(`.date-popup-container div.day >> text=${day}`).first() + specificDay = (day: string): Locator => + this.page.locator(`.date-popup-container div.day:not(.wrongMonth) >> text=${day}`).first() + inputTextPlaceholder = (): Locator => this.page.getByPlaceholder('Type text...') confirmInput = (): Locator => this.page.locator('.selectPopup button') diff --git a/ws-tests/sanity/tests/workspace/archive.spec.ts b/ws-tests/sanity/tests/workspace/archive.spec.ts index 96dc8a2988..d284e78cac 100644 --- a/ws-tests/sanity/tests/workspace/archive.spec.ts +++ b/ws-tests/sanity/tests/workspace/archive.spec.ts @@ -27,15 +27,16 @@ test.describe('Workspace Archive tests', () => { test('New workspace with date, archive, unarchive', async ({ page, browser, request }) => { const api: ApiEndpoint = new ApiEndpoint(request) - const workspaceInfo = await api.createWorkspaceWithLogin(generateId(), 'user1', '1234') + const wsId = generateId(5) + const workspaceInfo = await api.createWorkspaceWithLogin(wsId, 'user1', '1234') const newIssue: NewIssue = { - title: `Issue with all parameters and attachments-${generateId()}`, + title: `Issue with all parameters and attachments-${wsId}`, description: 'Created issue with all parameters and attachments description', status: 'In Progress', priority: 'Urgent', createLabel: true, - labels: `CREATE-ISSUE-${generateId()}`, + labels: `CREATE-ISSUE-${wsId}`, component: 'No component', estimation: '2', milestone: 'No Milestone', @@ -45,7 +46,7 @@ test.describe('Workspace Archive tests', () => { await loginPage.goto() await loginPage.login('user1', '1234') - await selectWorkspacePage.selectWorkspace(workspaceInfo.workspaceName) + await selectWorkspacePage.selectWorkspace(wsId) await trackerNavigationMenuPage.openIssuesForProject('Default') await issuesPage.clickModelSelectorAll() @@ -94,9 +95,11 @@ test.describe('Workspace Archive tests', () => { await test.step('Check workspace is active again', async () => { await page.reload() - await selectWorkspacePage.selectWorkspace(workspaceInfo.workspaceName) + await selectWorkspacePage.selectWorkspace(wsId) const issuesDetailsPage = new IssuesDetailsPage(page) + // Should be restored from previos remembered location. + // await issuesPage.openIssueByName(newIssue.title) await issuesDetailsPage.checkIssue(newIssue) }) })