UBERF-9537: Fix Invalid navigate to guest not authorised (#8121)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-03-02 22:31:25 +07:00 committed by GitHub
parent 53f5cae8ff
commit 8de24ea797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 147 additions and 66 deletions

View File

@ -737,7 +737,14 @@ export function isCollabAttr (hierarchy: Hierarchy, key: KeyedAttribute): boolea
*/ */
export function decodeTokenPayload (token: string): any { export function decodeTokenPayload (token: string): any {
try { 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) { } catch (err: any) {
console.error(err) console.error(err)
return {} return {}

View File

@ -234,6 +234,10 @@ export const setTreeCollapsed = (_id: any, collapsed: boolean, prefix?: string):
collapsed ? localStorage.setItem(key, COLLAPSED) : localStorage.removeItem(key) 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 { export function restoreLocation (loc: PlatformLocation, app: Plugin): void {
const last = localStorage.getItem(`${locationStorageKeyId}_${app}`) const last = localStorage.getItem(`${locationStorageKeyId}_${app}`)

View File

@ -26,25 +26,22 @@
Popup, Popup,
PopupAlignment, PopupAlignment,
ResolvedLocation, ResolvedLocation,
Separator,
TooltipInstance, TooltipInstance,
areLocationsEqual, areLocationsEqual,
closePanel, closePanel,
getCurrentLocation,
getLocation,
navigate,
showPanel,
defineSeparators, defineSeparators,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
setResolvedLocation, setResolvedLocation,
deviceOptionsStore as deviceInfo showPanel
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ListSelectionProvider, parseLinkId, restrictionStore, updateFocus } from '@hcengineering/view-resources' import { ListSelectionProvider, parseLinkId, restrictionStore, updateFocus } from '@hcengineering/view-resources'
import workbench, { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench' import workbench, { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
import { SpaceView, buildNavModel } from '@hcengineering/workbench-resources' import { SpaceView, buildNavModel } from '@hcengineering/workbench-resources'
import { workbenchGuestSeparators } from '..'
import guest from '../plugin' import guest from '../plugin'
import { checkAccess } from '../utils' import { checkAccess } from '../utils'
import { workbenchGuestSeparators } from '..'
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? [] const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
$deviceInfo.navigator.visible = false $deviceInfo.navigator.visible = false

View File

@ -16,18 +16,24 @@
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { Label, Loading, Notifications, location } from '@hcengineering/ui' import { Label, Loading, Notifications, location } from '@hcengineering/ui'
import { upgradeDownloadProgress } from '@hcengineering/presentation' import { upgradeDownloadProgress } from '@hcengineering/presentation'
import { connect, versionError } from '../connect' import { connect, versionError, invalidError } from '../connect'
import { guestId } from '@hcengineering/guest' import { guestId } from '@hcengineering/guest'
import workbench from '@hcengineering/workbench' import workbench from '@hcengineering/workbench'
import Guest from './Guest.svelte' import Guest from './Guest.svelte'
import plugin from '../plugin'
</script> </script>
{#if $location.path[0] === guestId} {#if $location.path[0] === guestId}
{#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')} {#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')}
<Loading /> <Loading />
{:then client} {:then client}
{#if $versionError} {#if $invalidError}
{invalidError}
<div class="version-wrapper">
<h1><Label label={plugin.string.LinkWasRevoked} /></h1>
</div>
{:else if $versionError}
<div class="version-wrapper"> <div class="version-wrapper">
<div class="antiPopup version-popup"> <div class="antiPopup version-popup">
<h1><Label label={workbench.string.ServerUnderMaintenance} /></h1> <h1><Label label={workbench.string.ServerUnderMaintenance} /></h1>
@ -52,14 +58,19 @@
<style lang="scss"> <style lang="scss">
.version-wrapper { .version-wrapper {
height: 100%; height: 100%;
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.please-update {
margin-bottom: 1rem;
}
.version-popup { .version-popup {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem; padding: 2rem;
flex-grow: 1;
} }
</style> </style>

View File

@ -9,7 +9,7 @@ import core, {
type Client, type Client,
type Version type Version
} from '@hcengineering/core' } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login' import login from '@hcengineering/login'
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
import presentation, { import presentation, {
closeClient, closeClient,
@ -23,12 +23,13 @@ import {
desktopPlatform, desktopPlatform,
fetchMetadataLocalStorage, fetchMetadataLocalStorage,
getCurrentLocation, getCurrentLocation,
navigate,
setMetadataLocalStorage setMetadataLocalStorage
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { writable, get } from 'svelte/store' import { writable, get } from 'svelte/store'
export const versionError = writable<string | undefined>(undefined) export const versionError = writable<string | undefined>(undefined)
export const invalidError = writable<boolean>(false)
const versionStorageKey = 'last_server_version' const versionStorageKey = 'last_server_version'
let _token: string | undefined let _token: string | undefined
@ -40,19 +41,16 @@ export async function connect (title: string): Promise<Client | undefined> {
const token = loc.query?.token const token = loc.query?.token
const ws = loc.path[1] const ws = loc.path[1]
if (ws === undefined || token == null) { if (ws === undefined || token == null) {
navigate({ invalidError.set(true)
path: [loginId]
})
return return
} }
setMetadata(presentation.metadata.Token, token)
const selectWorkspace = await getResource(login.function.SelectWorkspace) const selectWorkspace = await getResource(login.function.SelectWorkspace)
const workspaceLoginInfo = (await selectWorkspace(ws, token))[1] const workspaceLoginInfo = (await selectWorkspace(ws, token))[1]
if (workspaceLoginInfo == null) { if (workspaceLoginInfo == null) {
navigate({ // We just need to show guist link is revoked or invalid.
path: [loginId] invalidError.set(true)
}) clearMetadata(ws)
return return
} }
@ -119,10 +117,7 @@ export async function connect (title: string): Promise<Client | undefined> {
}, },
onUnauthorized: () => { onUnauthorized: () => {
clearMetadata(ws) clearMetadata(ws)
navigate({ invalidError.set(true)
path: [loginId],
query: {}
})
}, },
// We need to refresh all active live queries and clear old queries. // We need to refresh all active live queries and clear old queries.
onConnect: async (event: ClientConnectEvent, data: any) => { onConnect: async (event: ClientConnectEvent, data: any) => {
@ -214,6 +209,7 @@ export async function connect (title: string): Promise<Client | undefined> {
} }
} }
invalidError.set(false)
versionError.set(undefined) versionError.set(undefined)
// Update window title // Update window title
document.title = [ws, title].filter((it) => it).join(' - ') document.title = [ws, title].filter((it) => it).join(' - ')

View File

@ -17,7 +17,7 @@
import { isActiveMode, isArchivingMode, isRestoringMode, isUpgradingMode } from '@hcengineering/core' import { isActiveMode, isArchivingMode, isRestoringMode, isUpgradingMode } from '@hcengineering/core'
import { LoginInfo, Workspace } from '@hcengineering/login' import { LoginInfo, Workspace } from '@hcengineering/login'
import { OK, Severity, Status } from '@hcengineering/platform' 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 MessageBox from '@hcengineering/presentation/src/components/MessageBox.svelte'
import { import {
Button, Button,
@ -117,8 +117,6 @@
throw err throw err
} }
} }
$: isAdmin = isAdminUser()
let search: string = '' let search: string = ''
</script> </script>
@ -135,7 +133,7 @@
<div class="status"> <div class="status">
<StatusControl {status} /> <StatusControl {status} />
</div> </div>
{#if isAdmin} {#if workspaces.length > 10}
<div class="ml-2 mr-2 mb-2 flex-grow"> <div class="ml-2 mr-2 mb-2 flex-grow">
<SearchEdit bind:value={search} width={'100%'} /> <SearchEdit bind:value={search} width={'100%'} />
</div> </div>
@ -169,24 +167,7 @@
{/if} {/if}
</span> </span>
<span class="text-xs flex-row-center flex-center"> <span class="text-xs flex-row-center flex-center">
{#if isAdmin}
{workspace.workspace}
{#if workspace.region !== undefined}
at ({workspace.region})
{/if}
{/if}
<div class="text-sm"> <div class="text-sm">
{#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) ({lastUsageDays} days)
</div> </div>
</span> </span>

View File

@ -31,6 +31,7 @@ import presentation, { isAdminUser } from '@hcengineering/presentation'
import { import {
fetchMetadataLocalStorage, fetchMetadataLocalStorage,
getCurrentLocation, getCurrentLocation,
isSameSegments,
locationStorageKeyId, locationStorageKeyId,
locationToUrl, locationToUrl,
navigate, navigate,
@ -409,6 +410,20 @@ export async function getAccount (doNavigate: boolean = true): Promise<LoginInfo
} }
return result.result return result.result
} catch (err: any) { } catch (err: any) {
if (err instanceof PlatformError && err.status.code === platform.status.Unauthorized) {
setMetadata(presentation.metadata.Token, null)
setMetadata(presentation.metadata.Workspace, null)
setMetadata(presentation.metadata.WorkspaceId, null)
setMetadataLocalStorage(login.metadata.LastToken, null)
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
const loc = getCurrentLocation()
loc.path[1] = 'login'
loc.path.length = 2
navigate(loc)
return
}
Analytics.handleError(err) Analytics.handleError(err)
} }
} }
@ -658,11 +673,18 @@ export function navigateToWorkspace (
// Json parse error could be ignored // Json parse error could be ignored
} }
} }
const last = localStorage.getItem(`${locationStorageKeyId}_${workspace}`) const newLoc: Location = { path: [workbenchId, workspace] }
if (last !== null) { let last: Location | undefined
navigate(JSON.parse(last), replace) try {
last = JSON.parse(localStorage.getItem(`${locationStorageKeyId}_${workspace}`) ?? '')
} catch (err: any) {
// Ignore
}
if (last != null && isSameSegments(last, newLoc, 2)) {
// If last location in our workspace path, use it.
navigate(last, replace)
} else { } else {
navigate({ path: [workbenchId, workspace] }, replace) navigate(newLoc, replace)
} }
} }

View File

@ -28,6 +28,7 @@
closePopup, closePopup,
fetchMetadataLocalStorage, fetchMetadataLocalStorage,
getCurrentLocation, getCurrentLocation,
isSameSegments,
locationStorageKeyId, locationStorageKeyId,
locationToUrl, locationToUrl,
navigate, navigate,
@ -57,11 +58,19 @@
e.preventDefault() e.preventDefault()
closePopup() closePopup()
closePopup() closePopup()
if (ws !== getCurrentLocation().path[1]) { const current = getCurrentLocation()
const last = localStorage.getItem(`${locationStorageKeyId}_${ws}`) if (ws !== current.path[1]) {
if (last !== null) { let last: Location | undefined
navigate(JSON.parse(last)) try {
} else navigate({ path: [workbenchId, ws] }) last = JSON.parse(localStorage.getItem(`${locationStorageKeyId}_${ws}`) ?? '')
} catch (err: any) {
// Ignore
}
if (last != null && isSameSegments(last, current, 2)) {
navigate(last)
} else {
navigate({ path: [workbenchId, ws] })
}
} }
} }
} }
@ -147,7 +156,7 @@
{/if} {/if}
</div> </div>
<div class="p-2 ml-2 mb-4 select-text flex-col bordered"> <div class="p-2 ml-2 mb-4 select-text flex-col bordered">
{decodeTokenPayload(getMetadata(presentation.metadata.Token) ?? '').workspace} {decodeTokenPayload(getMetadata(presentation.metadata.Token) ?? '').workspace ?? ''}
</div> </div>
{/if} {/if}
<div class="ap-scroll"> <div class="ap-scroll">

View File

@ -78,7 +78,8 @@
showPopup, showPopup,
TooltipInstance, TooltipInstance,
workbenchSeparators, workbenchSeparators,
resizeObserver resizeObserver,
isSameSegments
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { import {
@ -429,9 +430,12 @@
const last = localStorage.getItem(`${locationStorageKeyId}_${loc.path[1]}`) const last = localStorage.getItem(`${locationStorageKeyId}_${loc.path[1]}`)
if (last != null) { if (last != null) {
const lastValue = JSON.parse(last) const lastValue = JSON.parse(last)
navigateDone = navigate(lastValue)
if (navigateDone) { if (isSameSegments(lastValue, loc, 2)) {
return navigateDone = navigate(lastValue)
if (navigateDone) {
return
}
} }
} }
if (app === undefined && !navigateDone) { if (app === undefined && !navigateDone) {

View File

@ -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<string, any> = { 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')
})
})

View File

@ -19,8 +19,13 @@ const getSecret = (): string => {
/** /**
* @public * @public
*/ */
export function generateToken (email: string, workspace: WorkspaceId, extra?: Record<string, string>): string { export function generateToken (
return encode({ ...(extra ?? {}), email, workspace: workspace.name }, getSecret()) email: string,
workspace: WorkspaceId,
extra?: Record<string, string>,
secret?: string
): string {
return encode({ ...(extra ?? {}), email, workspace: workspace.name }, secret ?? getSecret())
} }
/** /**

View File

@ -167,7 +167,9 @@ export class IssuesPage extends CommonTrackerPage {
appleseedJohnButton = (): Locator => this.page.locator('button.menu-item:has-text("Appleseed John")') appleseedJohnButton = (): Locator => this.page.locator('button.menu-item:has-text("Appleseed John")')
estimationEditor = (): Locator => this.page.locator('#estimation-editor') estimationEditor = (): Locator => this.page.locator('#estimation-editor')
dueDateButton = (): Locator => this.page.locator('button:has-text("Due date")') 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...') inputTextPlaceholder = (): Locator => this.page.getByPlaceholder('Type text...')
confirmInput = (): Locator => this.page.locator('.selectPopup button') confirmInput = (): Locator => this.page.locator('.selectPopup button')

View File

@ -27,15 +27,16 @@ test.describe('Workspace Archive tests', () => {
test('New workspace with date, archive, unarchive', async ({ page, browser, request }) => { test('New workspace with date, archive, unarchive', async ({ page, browser, request }) => {
const api: ApiEndpoint = new ApiEndpoint(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 = { 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', description: 'Created issue with all parameters and attachments description',
status: 'In Progress', status: 'In Progress',
priority: 'Urgent', priority: 'Urgent',
createLabel: true, createLabel: true,
labels: `CREATE-ISSUE-${generateId()}`, labels: `CREATE-ISSUE-${wsId}`,
component: 'No component', component: 'No component',
estimation: '2', estimation: '2',
milestone: 'No Milestone', milestone: 'No Milestone',
@ -45,7 +46,7 @@ test.describe('Workspace Archive tests', () => {
await loginPage.goto() await loginPage.goto()
await loginPage.login('user1', '1234') await loginPage.login('user1', '1234')
await selectWorkspacePage.selectWorkspace(workspaceInfo.workspaceName) await selectWorkspacePage.selectWorkspace(wsId)
await trackerNavigationMenuPage.openIssuesForProject('Default') await trackerNavigationMenuPage.openIssuesForProject('Default')
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
@ -94,9 +95,11 @@ test.describe('Workspace Archive tests', () => {
await test.step('Check workspace is active again', async () => { await test.step('Check workspace is active again', async () => {
await page.reload() await page.reload()
await selectWorkspacePage.selectWorkspace(workspaceInfo.workspaceName) await selectWorkspacePage.selectWorkspace(wsId)
const issuesDetailsPage = new IssuesDetailsPage(page) const issuesDetailsPage = new IssuesDetailsPage(page)
// Should be restored from previos remembered location.
// await issuesPage.openIssueByName(newIssue.title)
await issuesDetailsPage.checkIssue(newIssue) await issuesDetailsPage.checkIssue(newIssue)
}) })
}) })