UBERF-5870: Fix cache control and some minor enhancements (#4869)

UBERF-5870: Fix cache control and some minor enhancements

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-03-04 21:03:35 +07:00 committed by GitHub
parent a3e3767108
commit 1847ae6aaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 293 additions and 115 deletions

View File

@ -963,8 +963,8 @@ dependencies:
specifier: ^1.5.1
version: 1.5.1
body-parser:
specifier: ~1.19.1
version: 1.19.2
specifier: ^1.20.2
version: 1.20.2
browserslist:
specifier: 4.21.5
version: 4.21.5
@ -1065,10 +1065,10 @@ dependencies:
specifier: ^2.34.0
version: 2.35.1(eslint@8.56.0)(svelte@4.2.11)(ts-node@10.9.2)
express:
specifier: ^4.17.1
version: 4.18.2
specifier: ^4.18.3
version: 4.18.3
express-fileupload:
specifier: ^1.2.1
specifier: ^1.4.3
version: 1.4.3
faker:
specifier: ~5.5.3
@ -4797,7 +4797,7 @@ packages:
ejs: 3.1.9
esbuild: 0.18.20
esbuild-plugin-alias: 0.2.1
express: 4.18.2
express: 4.18.3
find-cache-dir: 3.3.2
fs-extra: 11.2.0
process: 0.11.10
@ -4834,7 +4834,7 @@ packages:
constants-browserify: 1.0.0
css-loader: 6.10.0(webpack@5.90.3)
es-module-lexer: 1.4.1
express: 4.18.2
express: 4.18.3
fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.3.3)(webpack@5.90.3)
fs-extra: 11.2.0
html-webpack-plugin: 5.6.0(webpack@5.90.3)
@ -4908,7 +4908,7 @@ packages:
detect-indent: 6.1.0
envinfo: 7.11.1
execa: 5.1.1
express: 4.18.2
express: 4.18.3
find-up: 5.0.0
fs-extra: 11.2.0
get-npm-tarball-url: 2.1.0
@ -5086,7 +5086,7 @@ packages:
cli-table3: 0.6.3
compression: 1.7.4
detect-port: 1.5.1
express: 4.18.2
express: 4.18.3
fs-extra: 11.2.0
globby: 11.1.0
ip: 2.0.1
@ -7581,24 +7581,8 @@ packages:
readable-stream: 3.6.2
dev: false
/body-parser@1.19.2:
resolution: {integrity: sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 1.1.2
http-errors: 1.8.1
iconv-lite: 0.4.24
on-finished: 2.3.0
qs: 6.9.7
raw-body: 2.4.3
type-is: 1.6.18
dev: false
/body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
/body-parser@1.20.2:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
@ -7610,7 +7594,7 @@ packages:
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.1
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
dev: false
@ -9756,13 +9740,13 @@ packages:
busboy: 1.6.0
dev: false
/express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
/express@4.18.3:
resolution: {integrity: sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.1
body-parser: 1.20.2
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.5.0
@ -13965,11 +13949,6 @@ packages:
side-channel: 1.0.5
dev: false
/qs@6.9.7:
resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==}
engines: {node: '>=0.6'}
dev: false
/query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'}
@ -14017,26 +13996,6 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/raw-body@2.4.3:
resolution: {integrity: sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 1.8.1
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/raw-body@2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
@ -16320,7 +16279,7 @@ packages:
compression: 1.7.4
connect-history-api-fallback: 2.0.0
default-gateway: 6.0.3
express: 4.18.2
express: 4.18.3
graceful-fs: 4.2.11
html-entities: 2.4.0
http-proxy-middleware: 2.0.6(@types/express@4.17.21)
@ -17749,7 +17708,7 @@ packages:
dev: false
file:projects/collaborator.tgz(@tiptap/pm@2.2.3)(bufferutil@4.0.8)(prosemirror-model@1.19.4)(svelte@4.2.11):
resolution: {integrity: sha512-HvLgY+NwEKic10k5ZvdykepwdECRL2FQHFp3XfjK3Li7pvHMPzKUcTYfnBsJFSHD+jjEZ31C2wP9oRD5s5lSag==, tarball: file:projects/collaborator.tgz}
resolution: {integrity: sha512-C2X7Yyjr7DoJenvaHw4y2eOCKejrUUI4IUXBNMWpSLvZx0L6O+6DwY2KP+N7UrKaifKX3vICt35Tyb0UA3K/dQ==, tarball: file:projects/collaborator.tgz}
id: file:projects/collaborator.tgz
name: '@rush-temp/collaborator'
version: 0.0.0
@ -17767,7 +17726,7 @@ packages:
'@types/ws': 8.5.10
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
body-parser: 1.19.2
body-parser: 1.20.2
compression: 1.7.4
cors: 2.8.5
cross-env: 7.0.3
@ -17777,7 +17736,7 @@ packages:
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.18.2
express: 4.18.3
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
mongodb: 6.3.0
prettier: 3.2.5
@ -18317,7 +18276,7 @@ packages:
dev: false
file:projects/front.tgz(esbuild@0.20.1)(svelte@4.2.11):
resolution: {integrity: sha512-Q1CZmbbm59dl6s4oEnEHBJPIxWzMEfaNIzWRPFJnV9wc4wckFya5G525eP1BwzwtWeQic7GY9nQrUQ96qhXtuQ==, tarball: file:projects/front.tgz}
resolution: {integrity: sha512-Oh/0YDbHbRu5+GmJeWOf9gVUaX0q31L9pdRbKq1zMt34GrzJGfSpRZO0bOxhu1Bsda3RSErk8q1rlY9EO2TCIA==, tarball: file:projects/front.tgz}
id: file:projects/front.tgz
name: '@rush-temp/front'
version: 0.0.0
@ -18334,7 +18293,7 @@ packages:
'@types/uuid': 8.3.4
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
body-parser: 1.19.2
body-parser: 1.20.2
compression: 1.7.4
cors: 2.8.5
cross-env: 7.0.3
@ -18343,7 +18302,7 @@ packages:
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.18.2
express: 4.18.3
express-fileupload: 1.4.3
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
morgan: 1.10.0
@ -20937,7 +20896,7 @@ packages:
dev: false
file:projects/pod-front.tgz(svelte@4.2.11):
resolution: {integrity: sha512-IbqAyQ8rYXPdITQ9dFovtQ6q89wIiUTzkhafA+K5cX65SUfeSM6iMx94gce9/LbLIWIs/v87/3ftO9Lf88gswA==, tarball: file:projects/pod-front.tgz}
resolution: {integrity: sha512-GL3Lo44ZK+ONrZYYP192Ixh6X9/Vq3wkWvgosdYesIrmQEvvZaxPmtLZUwJRpZ5HiYAequ5u015rUIz+gjhVcQ==, tarball: file:projects/pod-front.tgz}
id: file:projects/pod-front.tgz
name: '@rush-temp/pod-front'
version: 0.0.0
@ -20953,7 +20912,7 @@ packages:
'@types/uuid': 8.3.4
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
body-parser: 1.19.2
body-parser: 1.20.2
compression: 1.7.4
cors: 2.8.5
cross-env: 7.0.3
@ -20963,7 +20922,7 @@ packages:
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.18.2
express: 4.18.3
express-fileupload: 1.4.3
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
@ -20986,7 +20945,7 @@ packages:
dev: false
file:projects/pod-server.tgz(svelte@4.2.11):
resolution: {integrity: sha512-rRrgSjbgcgoFqfaUlCyx/b0kG4FG+ULExDOMrCX9xF8i3rWRE4H96NhEYCLRfW7Tc/lRgb9z3CEJKRGwRyVkjQ==, tarball: file:projects/pod-server.tgz}
resolution: {integrity: sha512-RfcF42iv64hXOnQ1OMrGVXV0wUXqKOtAx0nEXbsGETtY059o/Rt+rIVimT+R8GQVaYfqyY5AR9kC91eBbWls+w==, tarball: file:projects/pod-server.tgz}
id: file:projects/pod-server.tgz
name: '@rush-temp/pod-server'
version: 0.0.0
@ -22503,7 +22462,7 @@ packages:
dev: false
file:projects/server-request-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-Hh38VO8SURroi8TtBT0UfnxIqUwZjPhHf9nv3GfIpcbwkKgMr0qAWo9P6k9eEeKSRjriPcNhIM9VNOFwMsG1SQ==, tarball: file:projects/server-request-resources.tgz}
resolution: {integrity: sha512-bVFnGUZCaRd2aKmIc8CtCuY7Jgoyt2D/ocKsObNwd+iKsIhVCaZrcYoqSn8jhqKUdWMyc2784n9mmHeNqKSTQw==, tarball: file:projects/server-request-resources.tgz}
id: file:projects/server-request-resources.tgz
name: '@rush-temp/server-request-resources'
version: 0.0.0
@ -23124,7 +23083,7 @@ packages:
dev: false
file:projects/server-ws.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-IdOX7544NnlwLGuaLfDhiWdJkESqXP24M4fIa6Twlg0nK9d0sj6//TTD3JDrvrkkHwjFHDQjYywQjwGLYwONoQ==, tarball: file:projects/server-ws.tgz}
resolution: {integrity: sha512-owCA424nSv/RxJNLRWaOswSHP2ztwXKL/YhULEOFPo3sqPBustc/49QDoGjMFk/1aV3XEH4y4cPsl4xNsxqssg==, tarball: file:projects/server-ws.tgz}
id: file:projects/server-ws.tgz
name: '@rush-temp/server-ws'
version: 0.0.0
@ -23145,7 +23104,7 @@ packages:
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
express: 4.18.2
express: 4.18.3
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)

View File

@ -539,7 +539,12 @@ export function isCollectionAttr (hierarchy: Hierarchy, key: KeyedAttribute): bo
* @public
*/
export function decodeTokenPayload (token: string): any {
return JSON.parse(atob(token.split('.')[1]))
try {
return JSON.parse(atob(token.split('.')[1]))
} catch (err: any) {
console.error(err)
return {}
}
}
export function isAdminUser (): boolean {

View File

@ -16,8 +16,15 @@
<script lang="ts">
import { LoginInfo, Workspace } from '@hcengineering/login'
import { OK, Severity, Status } from '@hcengineering/platform'
import presentation, { NavLink } from '@hcengineering/presentation'
import { Button, Label, Scroller, deviceOptionsStore as deviceInfo, setMetadataLocalStorage } from '@hcengineering/ui'
import presentation, { NavLink, isAdminUser } from '@hcengineering/presentation'
import {
Button,
Label,
Scroller,
SearchEdit,
deviceOptionsStore as deviceInfo,
setMetadataLocalStorage
} from '@hcengineering/ui'
import { onMount } from 'svelte'
import login from '../plugin'
import { getAccount, getHref, getWorkspaces, goTo, navigateToWorkspace, selectWorkspace } from '../utils'
@ -84,6 +91,9 @@
throw err
}
}
$: isAdmin = isAdminUser()
let search: string = ''
</script>
<form class="container" style:padding={$deviceInfo.docWidth <= 480 ? '1.25rem' : '5rem'}>
@ -95,16 +105,32 @@
<div class="status">
<StatusControl {status} />
</div>
{#if isAdmin}
<div class="ml-2 mr-2 mb-2 flex-grow">
<SearchEdit bind:value={search} width={'100%'} />
</div>
{/if}
{#await _getWorkspaces() then}
<Scroller padding={'.125rem 0'}>
<div class="form">
{#each workspaces as workspace}
{#each workspaces.filter((it) => search === '' || (it.workspaceName?.includes(search) ?? false) || it.workspace.includes(search)) as workspace}
{@const wsName = workspace.workspaceName ?? workspace.workspace}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="workspace flex-center fs-title cursor-pointer focused-button bordered form-row"
on:click={() => select(workspace.workspace)}
>
{workspace.workspaceName ?? workspace.workspace}
<div class="flex flex-col flex-grow">
<span class="label overflow-label flex-center">
{wsName}
</span>
{#if isAdmin && wsName !== workspace.workspace}
<span class="text-xs flex-center">
{workspace.workspace}
</span>
{/if}
</div>
</div>
{/each}
{#if workspaces.length === 0 && account?.confirmed === true}

View File

@ -203,7 +203,8 @@ export async function getWorkspaces (): Promise<Workspace[]> {
if (endpoint !== undefined) {
return [
{
workspace: DEV_WORKSPACE
workspace: DEV_WORKSPACE,
workspaceId: DEV_WORKSPACE
}
]
}

View File

@ -26,8 +26,9 @@ export const loginId = 'login' as Plugin
* @public
*/
export interface Workspace {
workspace: string //
workspace: string // workspace Url
workspaceName?: string // A company name
workspaceId: string // A unique identifier for the workspace
}
/**

View File

@ -14,23 +14,27 @@
-->
<script lang="ts">
import login, { Workspace } from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
import { getMetadata, getResource } from '@hcengineering/platform'
import contact from '@hcengineering/contact'
import {
Icon,
IconCheck,
Loading,
Location,
SearchEdit,
closePopup,
fetchMetadataLocalStorage,
getCurrentLocation,
Loading,
Location,
locationStorageKeyId,
locationToUrl,
navigate,
resolvedLocationStore,
setMetadataLocalStorage,
IconCheck,
locationStorageKeyId
ticker
} from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte'
import { workspacesStore } from '../utils'
import presentation, { isAdminUser } from '@hcengineering/presentation'
// import Drag from './icons/Drag.svelte'
onMount(() => {
@ -89,15 +93,56 @@
ev.stopPropagation()
}
}
$: isAdmin = isAdminUser()
let search: string = ''
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
let endpoint = _endpoint.replace(/^ws/g, 'http')
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1)
}
let data: any
onDestroy(
ticker.subscribe(() => {
void fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
.then(async (json) => {
data = await json.json()
})
.catch((err) => {
console.error(err)
})
})
)
$: activeSessions =
(data?.statistics?.activeSessions as Record<
string,
Array<{
userId: string
data?: Record<string, any>
}>
>) ?? {}
</script>
{#if $workspacesStore.length}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="antiPopup" on:keydown={keyDown}>
<div class="ap-space x2" />
{#if isAdmin}
<div class="ml-2 mr-2 mb-2 flex-grow">
<SearchEdit bind:value={search} width={'100%'} />
</div>
{/if}
<div class="ap-scroll">
<div class="ap-box">
{#each $workspacesStore as ws, i}
{#each $workspacesStore.filter((it) => search === '' || (it.workspaceName?.includes(search) ?? false) || it.workspace.includes(search)) as ws, i}
{@const wsName = ws.workspaceName ?? ws.workspace}
{@const _activeSession = activeSessions[ws.workspaceId]}
<a
class="stealth"
href={getWorkspaceLink(ws)}
@ -108,6 +153,7 @@
<button
bind:this={btns[i]}
class="ap-menuItem flex-row-center flex-grow"
class:active={isAdmin && (_activeSession?.length ?? 0) > 0}
class:hover={btns[i] === activeElement}
on:mousemove={() => {
focusTarget(btns[i])
@ -116,7 +162,24 @@
<!-- <div class="drag"><Drag size={'small'} /></div> -->
<!-- <div class="logo empty" /> -->
<!-- <div class="flex-col flex-grow"> -->
<span class="label overflow-label flex-grow">{ws.workspaceName ?? ws.workspace}</span>
<div class="flex-col flex-grow">
<span class="label overflow-label flex-grow">
{wsName}
</span>
{#if isAdmin && wsName !== ws.workspace}
<span class="text-xs">
({ws.workspace})
</span>
{/if}
{#if isAdmin && (_activeSession?.length ?? 0) > 0}
<span class="text-xs flex-row-center">
<div class="mr-1">
<Icon icon={contact.icon.Person} size={'x-small'} />
</div>
{_activeSession?.length ?? 0}
</span>
{/if}
</div>
<!-- <span class="description overflow-label">Description</span> -->
<!-- </div> -->
<div class="ap-check">
@ -134,3 +197,9 @@
{:else}
<div class="antiPopup"><Loading /></div>
{/if}
<style lang="scss">
.active {
background-color: var(--theme-inbox-people-counter-bgcolor);
}
</style>

View File

@ -19,6 +19,7 @@
import { ObjectPresenter } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import MetricsInfo from './statistics/MetricsInfo.svelte'
import { workspacesStore } from '../utils'
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
@ -166,6 +167,7 @@
{:else if selectedTab === 'users'}
<div class="flex-column p-3 h-full" style:overflow="auto">
{#each Object.entries(activeSessions) as act}
{@const wsInstance = $workspacesStore.find((it) => it.workspaceId === act[0])}
{@const totalFind = act[1].reduce((it, itm) => itm.current.find + it, 0)}
{@const totalTx = act[1].reduce((it, itm) => itm.current.tx + it, 0)}
{@const employeeGroups = Array.from(new Set(act[1].map((it) => it.userId)))}
@ -173,7 +175,7 @@
<Expandable contentColor expanded={false} expandable={true} bordered>
<svelte:fragment slot="title">
<div class="fs-title">
Workspace: {act[0]}: {act[1].length} current 5 mins => {totalFind}/{totalTx}
Workspace: {wsInstance?.workspaceName ?? act[0]}: {act[1].length} current 5 mins => {totalFind}/{totalTx}
</div>
</svelte:fragment>
<div class="flex-col">

View File

@ -60,15 +60,15 @@
"@hcengineering/front": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/platform": "^0.6.9",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"express": "^4.18.3",
"express-fileupload": "^1.4.3",
"uuid": "^8.3.2",
"cors": "^2.8.5",
"@hcengineering/elastic": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/attachment": "^0.6.9",
"body-parser": "~1.19.1",
"body-parser": "^1.20.2",
"compression": "~1.7.4",
"sharp": "~0.32.0"
}

View File

@ -896,7 +896,7 @@ export async function getInviteLink (
/**
* @public
*/
export type ClientWorkspaceInfo = Omit<Workspace, '_id' | 'accounts' | 'workspaceUrl'>
export type ClientWorkspaceInfo = Omit<Workspace, '_id' | 'accounts' | 'workspaceUrl'> & { workspaceId: string }
/**
* @public
@ -905,7 +905,7 @@ export type WorkspaceInfo = Omit<Workspace, '_id' | 'accounts'>
function mapToClientWorkspace (ws: Workspace): ClientWorkspaceInfo {
const { _id, accounts, ...data } = ws
return { ...data, workspace: ws.workspaceUrl ?? ws.workspace }
return { ...data, workspace: ws.workspaceUrl ?? ws.workspace, workspaceId: ws.workspace }
}
function trimWorkspaceInfo (ws: Workspace): WorkspaceInfo {

View File

@ -69,8 +69,8 @@
"mongodb": "^6.3.0",
"yjs": "^13.5.52",
"y-prosemirror": "^1.2.1",
"express": "^4.17.1",
"body-parser": "~1.19.1",
"express": "^4.18.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"compression": "~1.7.4",
"ws": "^8.10.0"

View File

@ -46,15 +46,15 @@
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/platform": "^0.6.9",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"express": "^4.18.3",
"express-fileupload": "^1.4.3",
"uuid": "^8.3.2",
"cors": "^2.8.5",
"@hcengineering/elastic": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/attachment": "^0.6.9",
"body-parser": "~1.19.1",
"body-parser": "^1.20.2",
"compression": "~1.7.4",
"sharp": "~0.32.0",
"@hcengineering/minio": "^0.6.0",

View File

@ -23,10 +23,15 @@ import cors from 'cors'
import express, { Response } from 'express'
import fileUpload, { UploadedFile } from 'express-fileupload'
import https from 'https'
import morgan from 'morgan'
import { join, resolve } from 'path'
import { cwd } from 'process'
import sharp from 'sharp'
import { v4 as uuid } from 'uuid'
import morgan from 'morgan'
import { preConditions } from './utils'
const cacheControlValue = 'public, max-age=365d'
const cacheControlMaxAge = '365d'
async function minioUpload (minio: MinioService, workspace: WorkspaceId, file: UploadedFile): Promise<string> {
const id = uuid()
@ -103,15 +108,41 @@ async function getFileRange (
}
}
async function getFile (client: MinioService, workspace: WorkspaceId, uuid: string, res: Response): Promise<void> {
async function getFile (
client: MinioService,
workspace: WorkspaceId,
uuid: string,
req: Request,
res: Response
): Promise<void> {
const stat = await client.stat(workspace, uuid)
const etag = stat.etag
if (
preConditions.IfNoneMatch(req.headers, { etag }) === 'notModified' ||
preConditions.IfMatch(req.headers, { etag }) === 'notModified' ||
preConditions.IfModifiedSince(req.headers, { lastModified: stat.lastModified }) === 'notModified'
) {
// Matched, return not modified
res.statusCode = 304
res.end()
return
}
if (preConditions.IfUnmodifiedSince(req.headers, { lastModified: stat.lastModified }) === 'failed') {
// Send 412 (Precondition Failed)
res.statusCode = 412
res.end()
return
}
try {
const dataStream = await client.get(workspace, uuid)
res.writeHead(200, {
'Content-Type': stat.metaData['content-type'],
Etag: stat.etag,
'Last-Modified': stat.lastModified.toISOString()
'Last-Modified': stat.lastModified.toISOString(),
'Cache-Control': cacheControlValue
})
dataStream.on('data', function (chunk) {
@ -182,9 +213,7 @@ export function start (
// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.get('/config.json', async (req, res) => {
res.status(200)
res.set('Cache-Control', 'no-cache')
res.json({
const data = {
ACCOUNTS_URL: config.accountsUrl,
UPLOAD_URL: config.uploadUrl,
MODEL_VERSION: config.modelVersion,
@ -199,19 +228,19 @@ export function start (
DEFAULT_LANGUAGE: config.defaultLanguage,
LAST_NAME_FIRST: config.lastNameFirst,
...(extraConfig ?? {})
})
}
res.set('Cache-Control', cacheControlValue)
res.status(200)
res.json(data)
})
const dist = resolve(process.env.PUBLIC_DIR ?? __dirname, 'dist')
const dist = resolve(process.env.PUBLIC_DIR ?? cwd(), 'dist')
console.log('serving static files from', dist)
app.use(
express.static(dist, {
maxAge: '7d',
setHeaders (res, path) {
if (path.includes('index.html')) {
res.setHeader('Cache-Control', 'public, max-age=0')
}
}
maxAge: '365d',
etag: true,
lastModified: true
})
)
@ -275,7 +304,7 @@ export function start (
if (range !== undefined) {
await getFileRange(range, config.minio, payload.workspace, uuid, res)
} else {
await getFile(config.minio, payload.workspace, uuid, res)
await getFile(config.minio, payload.workspace, uuid, req, res)
}
} catch (error: any) {
if (error?.code === 'NoSuchKey' || error?.code === 'NotFound') {
@ -527,8 +556,11 @@ export function start (
})
app.get('*', function (request, response) {
response.setHeader('Cache-Control', 'max-age=0')
response.sendFile(join(dist, 'index.html'))
response.sendFile(join(dist, 'index.html'), {
maxAge: cacheControlMaxAge,
etag: true,
lastModified: true
})
})
const server = app.listen(port)

83
server/front/src/utils.ts Normal file
View File

@ -0,0 +1,83 @@
export const ETagSupport = {
isWeak (etag: string): boolean {
return etag.startsWith('W/"')
},
isStrong (etag: string): boolean {
return etag.startsWith('"')
},
opaqueTag (etag: string): string {
if (this.isWeak(etag)) {
return etag.substring(2)
}
return etag
},
weakMatch (a: string, b: string): boolean {
return this.opaqueTag(a) === this.opaqueTag(b)
},
strongMatch (a: string, b: string): boolean {
return this.isStrong(a) && this.isStrong(b) && a === b
}
}
function toList (value: string): string[] {
return value.split(',').map((s) => s.trim())
}
export const preConditions = {
IfMatch: (headers: Request['headers'], state: { etag: string }): 'fetch' | 'notModified' => {
const header = (headers as any)['if-match']
if (header == null) {
return 'fetch'
}
if (header === '*') {
return 'fetch'
}
return toList(header).some((etag) => ETagSupport.strongMatch(etag, state.etag)) ? 'notModified' : 'fetch'
},
IfNoneMatch: (headers: Request['headers'], state: { etag: string }): 'fetch' | 'notModified' => {
const header = (headers as any)['if-none-match']
if (header == null) {
return 'fetch'
}
if (header === '*') {
return 'fetch'
} else {
return toList(header).some((etag) => ETagSupport.weakMatch(etag, state.etag)) ? 'notModified' : 'fetch'
}
},
IfModifiedSince: (headers: Request['headers'], state: { lastModified: Date }): 'fetch' | 'notModified' => {
if ((headers as any)['if-none-match'] != null) {
return 'fetch'
}
const header = (headers as any)['if-modified-since']
if (header == null) {
return 'fetch'
}
const date = Date.parse(header)
if (isNaN(date)) {
return 'fetch'
}
return state.lastModified.getTime() <= date ? 'notModified' : 'fetch'
},
IfUnmodifiedSince: (headers: Request['headers'], state: { lastModified: Date }): 'fetch' | 'failed' => {
if ((headers as any)['if-match'] != null) {
return 'fetch'
}
const header = (headers as any)['if-unmodified-since']
if (header == null) {
return 'fetch'
}
const date = Date.parse(header)
if (isNaN(date)) {
return 'fetch'
}
return state.lastModified.getTime() > date ? 'failed' : 'fetch'
}
}

View File

@ -46,7 +46,7 @@
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/rpc": "^0.6.1",
"bufferutil": "^4.0.7",
"express": "^4.17.1",
"express": "^4.18.3",
"compression": "~1.7.4",
"cors": "^2.8.5"
}