Push notification (#5364)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-04-16 11:54:29 +05:00 committed by GitHub
parent d2681eba2e
commit bb1abdc8cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 470 additions and 147 deletions

View File

@ -935,6 +935,9 @@ dependencies:
'@types/uuid': '@types/uuid':
specifier: ^8.3.1 specifier: ^8.3.1
version: 8.3.4 version: 8.3.4
'@types/web-push':
specifier: ~3.6.3
version: 3.6.3
'@types/ws': '@types/ws':
specifier: ^8.5.3 specifier: ^8.5.3
version: 8.5.10 version: 8.5.10
@ -1268,6 +1271,9 @@ dependencies:
uuid: uuid:
specifier: ^8.3.2 specifier: ^8.3.2
version: 8.3.2 version: 8.3.2
web-push:
specifier: ~3.6.7
version: 3.6.7
webpack: webpack:
specifier: ^5.75.0 specifier: ^5.75.0
version: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4) version: 5.90.3(esbuild@0.20.1)(webpack-cli@5.1.4)
@ -6548,6 +6554,12 @@ packages:
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
dev: false dev: false
/@types/web-push@3.6.3:
resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==}
dependencies:
'@types/node': 20.11.19
dev: false
/@types/webidl-conversions@7.0.3: /@types/webidl-conversions@7.0.3:
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
dev: false dev: false
@ -7016,6 +7028,15 @@ packages:
- supports-color - supports-color
dev: false dev: false
/agent-base@7.1.1:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: false
/aggregate-error@3.1.0: /aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -7276,6 +7297,15 @@ packages:
is-shared-array-buffer: 1.0.3 is-shared-array-buffer: 1.0.3
dev: false dev: false
/asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
dependencies:
bn.js: 4.12.0
inherits: 2.0.4
minimalistic-assert: 1.0.1
safer-buffer: 2.1.2
dev: false
/assert@2.1.0: /assert@2.1.0:
resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==}
dependencies: dependencies:
@ -7594,6 +7624,10 @@ packages:
readable-stream: 3.6.2 readable-stream: 3.6.2
dev: false dev: false
/bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
dev: false
/body-parser@1.20.2: /body-parser@1.20.2:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -7714,6 +7748,10 @@ packages:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: false dev: false
/buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
dev: false
/buffer-from@1.1.2: /buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false dev: false
@ -8869,6 +8907,12 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: false dev: false
/ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
dev: false
/ee-first@1.1.1: /ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false dev: false
@ -10784,6 +10828,11 @@ packages:
resolve-alpn: 1.2.1 resolve-alpn: 1.2.1
dev: false dev: false
/http_ece@1.2.0:
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
engines: {node: '>=16'}
dev: false
/https-proxy-agent@4.0.0: /https-proxy-agent@4.0.0:
resolution: {integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==} resolution: {integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@ -10804,6 +10853,16 @@ packages:
- supports-color - supports-color
dev: false dev: false
/https-proxy-agent@7.0.4:
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.1
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: false
/human-signals@2.1.0: /human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
@ -11981,6 +12040,21 @@ packages:
resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==} resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==}
dev: false dev: false
/jwa@2.0.0:
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
dev: false
/jws@4.0.0:
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
dependencies:
jwa: 2.0.0
safe-buffer: 5.2.1
dev: false
/jwt-simple@0.5.6: /jwt-simple@0.5.6:
resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==} resolution: {integrity: sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
@ -16341,6 +16415,20 @@ packages:
'@zxing/text-encoding': 0.9.0 '@zxing/text-encoding': 0.9.0
dev: false dev: false
/web-push@3.6.7:
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
engines: {node: '>= 16'}
hasBin: true
dependencies:
asn1.js: 5.4.1
http_ece: 1.2.0
https-proxy-agent: 7.0.4
jws: 4.0.0
minimist: 1.2.8
transitivePeerDependencies:
- supports-color
dev: false
/webidl-conversions@3.0.1: /webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false dev: false
@ -22283,12 +22371,13 @@ packages:
dev: false dev: false
file:projects/server-notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): file:projects/server-notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-syXpJRNP0osGwuo3+uTE08SmRqjBPEo4mv6B6hqSvwTXLc7ZbmkEAXbonr+ykj1YQk15u5hS8JjRnWSGStuy7A==, tarball: file:projects/server-notification-resources.tgz} resolution: {integrity: sha512-9ctaiwEU+M3RZ4Qz0/OuQ1lLh4Os9V8vlpf2FK/cpGOvAt5RUfV0qwiJLaImsLVbS62uz6hbZ1oy7Q3T+qex8Q==, tarball: file:projects/server-notification-resources.tgz}
id: file:projects/server-notification-resources.tgz id: file:projects/server-notification-resources.tgz
name: '@rush-temp/server-notification-resources' name: '@rush-temp/server-notification-resources'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@types/jest': 29.5.12 '@types/jest': 29.5.12
'@types/web-push': 3.6.3
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@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) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0 eslint: 8.56.0
@ -22301,6 +22390,7 @@ packages:
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11) prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
web-push: 3.6.7
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- '@jest/types' - '@jest/types'

View File

@ -156,6 +156,7 @@ services:
- MINIO_SECRET_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin
- REKONI_URL=http://rekoni:4004 - REKONI_URL=http://rekoni:4004
- FRONT_URL=http://localhost:8087 - FRONT_URL=http://localhost:8087
- UPLOAD_URL=http://localhost:8087/files
# - APM_SERVER_URL=http://apm-server:8200 # - APM_SERVER_URL=http://apm-server:8200
- SERVER_PROVIDER=ws - SERVER_PROVIDER=ws
- ACCOUNTS_URL=http://account:3000 - ACCOUNTS_URL=http://account:3000

View File

@ -30,7 +30,7 @@ import { imageCropperId } from '@hcengineering/image-cropper'
import { inventoryId } from '@hcengineering/inventory' import { inventoryId } from '@hcengineering/inventory'
import { leadId } from '@hcengineering/lead' import { leadId } from '@hcengineering/lead'
import login, { loginId } from '@hcengineering/login' import login, { loginId } from '@hcengineering/login'
import { notificationId } from '@hcengineering/notification' import notification, { notificationId } from '@hcengineering/notification'
import { recruitId } from '@hcengineering/recruit' import { recruitId } from '@hcengineering/recruit'
import rekoni from '@hcengineering/rekoni' import rekoni from '@hcengineering/rekoni'
import { requestId } from '@hcengineering/request' import { requestId } from '@hcengineering/request'
@ -96,6 +96,7 @@ interface Config {
CALENDAR_URL: string CALENDAR_URL: string
COLLABORATOR_URL: string COLLABORATOR_URL: string
COLLABORATOR_API_URL: string COLLABORATOR_API_URL: string
PUSH_PUBLIC_KEY: string
TITLE?: string TITLE?: string
LANGUAGES?: string LANGUAGES?: string
DEFAULT_LANGUAGE?: string DEFAULT_LANGUAGE?: string
@ -158,6 +159,7 @@ export async function configurePlatform() {
setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086') setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086')
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087') setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095') setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY)
setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT) setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT)

View File

@ -36,7 +36,42 @@ const doValidate = !prod || (process.env.DO_VALIDATE === 'true')
/** /**
* @type {Configuration} * @type {Configuration}
*/ */
module.exports = { module.exports = [
{
mode: dev ? 'development' : mode,
entry: {
serviceWorker: '@hcengineering/notification/src/serviceWorker.ts'
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /(node_modules|\.webpack)/,
use: {
loader: 'esbuild-loader',
options: {
target: 'es2021',
keepNames: true,
minify: !prod,
sourcemap: !prod
}
}
}
]
},
output: {
path: __dirname + '/dist',
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: '/',
pathinfo: false
},
resolve: {
extensions: ['.ts', '.js'],
conditionNames: ['svelte', 'browser', 'import']
}
},
{
entry: { entry: {
bundle: [ bundle: [
'@hcengineering/theme/styles/global.scss', '@hcengineering/theme/styles/global.scss',
@ -278,4 +313,4 @@ module.exports = {
} }
} }
} }
} }]

View File

@ -73,7 +73,9 @@ import {
type NotificationSetting, type NotificationSetting,
type NotificationStatus, type NotificationStatus,
type NotificationTemplate, type NotificationTemplate,
type NotificationType type PushSubscription,
type NotificationType,
type PushSubscriptionKeys
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform' import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
@ -96,6 +98,13 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
status!: NotificationStatus status!: NotificationStatus
} }
@Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_NOTIFICATION)
export class TPushSubscription extends TDoc implements PushSubscription {
user!: Ref<Account>
endpoint!: string
keys!: PushSubscriptionKeys
}
@Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL) @Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL)
export class TBaseNotificationType extends TDoc implements BaseNotificationType { export class TBaseNotificationType extends TDoc implements BaseNotificationType {
generated!: boolean generated!: boolean
@ -335,7 +344,8 @@ export function createModel (builder: Builder): void {
TActivityNotificationViewlet, TActivityNotificationViewlet,
TBaseNotificationType, TBaseNotificationType,
TCommonNotificationType, TCommonNotificationType,
TMentionInboxNotification TMentionInboxNotification,
TPushSubscription
) )
builder.createDoc( builder.createDoc(

View File

@ -29,7 +29,13 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import contact, { AvatarProvider, AvatarType, getFirstName, getLastName } from '@hcengineering/contact' import contact, {
AvatarProvider,
AvatarType,
getFirstName,
getLastName,
getAvatarProviderId
} from '@hcengineering/contact'
import { Asset, getMetadata, getResource } from '@hcengineering/platform' import { Asset, getMetadata, getResource } from '@hcengineering/platform'
import { getBlobURL, getClient } from '@hcengineering/presentation' import { getBlobURL, getClient } from '@hcengineering/presentation'
import { import {
@ -43,7 +49,6 @@
themeStore, themeStore,
resizeObserver resizeObserver
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { getAvatarProviderId } from '../utils'
import AvatarIcon from './icons/Avatar.svelte' import AvatarIcon from './icons/Avatar.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'

View File

@ -15,43 +15,42 @@
// //
import { import {
type AvatarProvider,
AvatarType, AvatarType,
type ChannelProvider,
type Contact,
type Employee,
type Person,
type PersonAccount,
contactId, contactId,
formatName, formatName,
getFirstName, getFirstName,
getLastName, getLastName,
getName, getName,
type Channel type Channel,
type ChannelProvider,
type Contact,
type Employee,
type Person,
type PersonAccount
} from '@hcengineering/contact' } from '@hcengineering/contact'
import { import {
getCurrentAccount,
toIdMap,
type Class,
type Client, type Client,
type Doc, type Doc,
type IdMap, type IdMap,
type ObjQueryType, type ObjQueryType,
type Ref, type Ref,
type Timestamp, type Timestamp,
type TxOperations, type TxOperations
getCurrentAccount,
toIdMap,
type Class
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform' import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { type TemplateDataProvider } from '@hcengineering/templates' import { type TemplateDataProvider } from '@hcengineering/templates'
import { import {
type Location,
type ResolvedLocation,
type TabItem,
getCurrentResolvedLocation, getCurrentResolvedLocation,
getPanelURI, getPanelURI,
type LabelAndProps type LabelAndProps,
type Location,
type ResolvedLocation,
type TabItem
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view, { type Filter } from '@hcengineering/view' import view, { type Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources' import { FilterQuery } from '@hcengineering/view-resources'
@ -370,24 +369,6 @@ export function getAvatarTypeDropdownItems (hasGravatar: boolean, imageOnly?: bo
] ]
} }
export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider> | undefined {
if (avatar === null || avatar === undefined || avatar === '') {
return
}
if (!avatar.includes('://')) {
return contact.avatarProvider.Image
}
const [schema] = avatar.split('://')
switch (schema) {
case AvatarType.GRAVATAR:
return contact.avatarProvider.Gravatar
case AvatarType.COLOR:
return contact.avatarProvider.Color
}
return contact.avatarProvider.Image
}
export async function contactTitleProvider (client: Client, ref: Ref<Contact>, doc?: Contact): Promise<string> { export async function contactTitleProvider (client: Client, ref: Ref<Contact>, doc?: Contact): Promise<string> {
const object = doc ?? (await client.findOne(contact.class.Contact, { _id: ref })) const object = doc ?? (await client.findOne(contact.class.Contact, { _id: ref }))
if (object === undefined) return '' if (object === undefined) return ''

View File

@ -16,7 +16,7 @@
import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } from '@hcengineering/core' import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } from '@hcengineering/core'
import { IconSize, ColorDefinition } from '@hcengineering/ui' import { IconSize, ColorDefinition } from '@hcengineering/ui'
import { MD5 } from 'crypto-js' import { MD5 } from 'crypto-js'
import { Channel, Contact, contactPlugin, Person } from '.' import { AvatarProvider, AvatarType, Channel, Contact, contactPlugin, Person } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types' import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
@ -55,6 +55,27 @@ export function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString() return MD5(email.trim().toLowerCase()).toString()
} }
/**
* @public
*/
export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider> | undefined {
if (avatar === null || avatar === undefined || avatar === '') {
return
}
if (!avatar.includes('://')) {
return contactPlugin.avatarProvider.Image
}
const [schema] = avatar.split('://')
switch (schema) {
case AvatarType.GRAVATAR:
return contactPlugin.avatarProvider.Gravatar
case AvatarType.COLOR:
return contactPlugin.avatarProvider.Color
}
return contactPlugin.avatarProvider.Image
}
/** /**
* @public * @public
*/ */

View File

@ -14,60 +14,76 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getCurrentAccount } from '@hcengineering/core' import { getCurrentAccount } from '@hcengineering/core'
import notification, { BrowserNotification, NotificationStatus } from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation' import { getMetadata } from '@hcengineering/platform'
import { getCurrentLocation, navigate } from '@hcengineering/ui' import { getClient } from '@hcengineering/presentation'
import { askPermission } from '../utils' import { getCurrentLocation, navigate, parseLocation } from '@hcengineering/ui'
let notifications: BrowserNotification[] = []
const query = createQuery()
query.query(
notification.class.BrowserNotification,
{
user: getCurrentAccount()._id,
status: NotificationStatus.New
},
(res) => {
notifications = res
}
)
const client = getClient() const client = getClient()
$: process(notifications) const publicKey = getMetadata(notification.metadata.PushPublicKey)
async function process (notifications: BrowserNotification[]): Promise<void> { async function subscribe (): Promise<void> {
if (notifications.length === 0) return if ('serviceWorker' in navigator && 'PushManager' in window && publicKey !== undefined) {
await askPermission() try {
if ('Notification' in window && Notification?.permission === 'granted') {
for (const value of notifications) {
const req: NotificationOptions = {
body: value.body,
tag: value._id,
silent: false
}
const notification = new Notification(value.title, req)
if (value.onClickLocation !== undefined) {
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path.length = 3 const registration = await navigator.serviceWorker.register('/serviceWorker.js', {
loc.path[2] = value.onClickLocation.path[2] scope: `./${loc.path[0]}/${loc.path[1]}`
if (value.onClickLocation.path[3]) { })
loc.path[3] = value.onClickLocation.path[3] const current = await registration.pushManager.getSubscription()
if (value.onClickLocation.path[4]) { if (current == null) {
loc.path[4] = value.onClickLocation.path[4] const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
})
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
user: getCurrentAccount()._id,
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
auth: arrayBufferToBase64(subscription.getKey('auth'))
}
})
} else {
const exists = await client.findOne(notification.class.PushSubscription, {
user: getCurrentAccount()._id,
endpoint: current.endpoint
})
if (exists === undefined) {
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
user: getCurrentAccount()._id,
endpoint: current.endpoint,
keys: {
p256dh: arrayBufferToBase64(current.getKey('p256dh')),
auth: arrayBufferToBase64(current.getKey('auth'))
}
})
} }
} }
loc.query = value.onClickLocation.query navigator.serviceWorker.addEventListener('message', (event) => {
loc.fragment = value.onClickLocation.fragment if (event.data && event.data.type === 'notification-click') {
const onClick = () => { const { url } = event.data
navigate(loc) if (url !== undefined) {
window.parent.parent.focus() navigate(parseLocation(new URL(url)))
} }
notification.onclick = onClick
} }
await client.update(value, { status: NotificationStatus.Notified }) })
} catch (err) {
console.error('Service Worker registration failed:', err)
} }
} }
} }
function arrayBufferToBase64 (buffer: ArrayBuffer | null): string {
if (buffer) {
const bytes = new Uint8Array(buffer)
const array = Array.from(bytes)
const binary = String.fromCharCode.apply(null, array)
return btoa(binary)
} else {
return ''
}
}
subscribe()
</script> </script>

View File

@ -30,7 +30,7 @@ import {
TxCUD, TxCUD,
TxOperations TxOperations
} from '@hcengineering/core' } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference' import { Preference } from '@hcengineering/preference'
import { IntegrationType } from '@hcengineering/setting' import { IntegrationType } from '@hcengineering/setting'
@ -51,6 +51,26 @@ export interface BrowserNotification extends Doc {
onClickLocation?: Location onClickLocation?: Location
} }
export interface PushData {
tag?: string
title: string
body: string
icon?: string
domain?: string
url?: string
}
export interface PushSubscriptionKeys {
p256dh: string
auth: string
}
export interface PushSubscription extends Doc {
user: Ref<Account>
endpoint: string
keys: PushSubscriptionKeys
}
/** /**
* @public * @public
*/ */
@ -332,6 +352,7 @@ const notification = plugin(notificationId, {
}, },
class: { class: {
BrowserNotification: '' as Ref<Class<BrowserNotification>>, BrowserNotification: '' as Ref<Class<BrowserNotification>>,
PushSubscription: '' as Ref<Class<PushSubscription>>,
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>, BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
NotificationType: '' as Ref<Class<NotificationType>>, NotificationType: '' as Ref<Class<NotificationType>>,
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>, CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
@ -353,6 +374,9 @@ const notification = plugin(notificationId, {
CollaboratoAddNotification: '' as Ref<NotificationType>, CollaboratoAddNotification: '' as Ref<NotificationType>,
MentionCommonNotificationType: '' as Ref<CommonNotificationType> MentionCommonNotificationType: '' as Ref<CommonNotificationType>
}, },
metadata: {
PushPublicKey: '' as Metadata<string>
},
providers: { providers: {
PlatformNotification: '' as Ref<NotificationProvider>, PlatformNotification: '' as Ref<NotificationProvider>,
BrowserNotification: '' as Ref<NotificationProvider>, BrowserNotification: '' as Ref<NotificationProvider>,

View File

@ -0,0 +1,58 @@
import type { PushData } from './index'
declare const self: any
interface PushEvent extends Event {
data: PushMessageData
}
interface PushMessageData {
json: () => any
}
self.addEventListener('push', (event: PushEvent) => {
const payload: PushData = event.data.json()
self.registration.showNotification(payload.title, {
body: payload.body,
icon: payload.icon,
tag: payload.tag,
data: {
domain: payload.domain,
url: payload.url
}
})
})
// Listen for notification click event
self.addEventListener('notificationclick', (event: any) => {
const clickedNotification = event.notification
const notificationData = clickedNotification.data
const notificationUrl = notificationData.url
const domain = notificationData.domain
if (notificationUrl !== undefined && domain !== undefined) {
// Check if any client with the same origin is already open
event.waitUntil(
// Check all active clients (browser windows or tabs)
self.clients
.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((clientList: any) => {
// Loop through each client
for (const client of clientList) {
// If a client has the same URL origin, focus and navigate to it
if ((client.url as string)?.startsWith(domain)) {
client.postMessage({
type: 'notification-click',
url: notificationUrl
})
return client.focus()
}
}
// If no client with the same URL origin is found, open a new window/tab
return self.clients.openWindow(notificationUrl)
})
)
}
})

View File

@ -22,49 +22,42 @@ import serverCore, { type StorageConfiguration } from '@hcengineering/server-cor
import serverNotification from '@hcengineering/server-notification' import serverNotification from '@hcengineering/server-notification'
import serverToken from '@hcengineering/server-token' import serverToken from '@hcengineering/server-token'
import { start } from '.' import { start } from '.'
import notification from '@hcengineering/notification'
const { const config = serverConfigFromEnv()
url,
frontUrl,
serverSecret,
sesUrl,
elasticUrl,
elasticIndexName,
accountsUrl,
rekoniUrl,
serverFactory,
serverPort,
enableCompression
} = serverConfigFromEnv()
const storageConfig: StorageConfiguration = storageConfigFromEnv() const storageConfig: StorageConfiguration = storageConfigFromEnv()
const cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS const cursorMaxTime = process.env.SERVER_CURSOR_MAXTIMEMS
const lastNameFirst = process.env.LAST_NAME_FIRST === 'true' const lastNameFirst = process.env.LAST_NAME_FIRST === 'true'
setMetadata(serverCore.metadata.CursorMaxTimeMS, cursorMaxTime) setMetadata(serverCore.metadata.CursorMaxTimeMS, cursorMaxTime)
setMetadata(serverCore.metadata.FrontUrl, frontUrl) setMetadata(serverCore.metadata.FrontUrl, config.frontUrl)
setMetadata(serverToken.metadata.Secret, serverSecret) setMetadata(serverCore.metadata.UploadURL, config.uploadUrl)
setMetadata(serverNotification.metadata.SesUrl, sesUrl ?? '') setMetadata(serverToken.metadata.Secret, config.serverSecret)
setMetadata(serverNotification.metadata.SesUrl, config.sesUrl ?? '')
setMetadata(notification.metadata.PushPublicKey, config.pushPublicKey)
setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey)
setMetadata(serverNotification.metadata.PushSubject, config.pushSubject)
setMetadata(contactPlugin.metadata.LastNameFirst, lastNameFirst) setMetadata(contactPlugin.metadata.LastNameFirst, lastNameFirst)
setMetadata(serverCore.metadata.ElasticIndexName, elasticIndexName) setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
console.log( console.log(
`starting server on ${serverPort} git_version: ${process.env.GIT_REVISION ?? ''} model_version: ${ `starting server on ${config.serverPort} git_version: ${process.env.GIT_REVISION ?? ''} model_version: ${
process.env.MODEL_VERSION ?? '' process.env.MODEL_VERSION ?? ''
}` }`
) )
const shutdown = start(url, { const shutdown = start(config.url, {
fullTextUrl: elasticUrl, fullTextUrl: config.elasticUrl,
storageConfig, storageConfig,
rekoniUrl, rekoniUrl: config.rekoniUrl,
port: serverPort, port: config.serverPort,
serverFactory, serverFactory: config.serverFactory,
indexParallel: 2, indexParallel: 2,
indexProcessing: 50, indexProcessing: 50,
productId: '', productId: '',
enableCompression, enableCompression: config.enableCompression,
accountsUrl accountsUrl: config.accountsUrl
}) })
const close = (): void => { const close = (): void => {

View File

@ -29,7 +29,8 @@
"typescript": "^5.3.3", "typescript": "^5.3.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"@types/jest": "^29.5.5" "@types/jest": "^29.5.5",
"@types/web-push": "~3.6.3"
}, },
"dependencies": { "dependencies": {
"@hcengineering/activity": "^0.6.0", "@hcengineering/activity": "^0.6.0",
@ -38,8 +39,10 @@
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/notification": "^0.6.16", "@hcengineering/notification": "^0.6.16",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/chunter": "^0.6.12", "@hcengineering/chunter": "^0.6.12",
"@hcengineering/view": "^0.6.9", "@hcengineering/view": "^0.6.9",
"@hcengineering/contact": "^0.6.20" "@hcengineering/contact": "^0.6.20",
"web-push": "~3.6.7"
} }
} }

View File

@ -14,8 +14,16 @@
// limitations under the License. // limitations under the License.
// //
import activity, { ActivityMessage } from '@hcengineering/activity'
import chunter, { ChatMessage } from '@hcengineering/chunter' import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { Employee, formatName, Person, PersonAccount } from '@hcengineering/contact' import contact, {
Employee,
formatName,
getAvatarProviderId,
getGravatarUrl,
Person,
PersonAccount
} from '@hcengineering/contact'
import core, { import core, {
Account, Account,
AnyAttribute, AnyAttribute,
@ -45,25 +53,25 @@ import core, {
import notification, { import notification, {
ActivityInboxNotification, ActivityInboxNotification,
BaseNotificationType, BaseNotificationType,
BrowserNotification,
ClassCollaborators, ClassCollaborators,
Collaborators, Collaborators,
CommonInboxNotification, CommonInboxNotification,
DocNotifyContext, DocNotifyContext,
InboxNotification, InboxNotification,
notificationId, notificationId,
NotificationStatus, NotificationType,
NotificationType PushData
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { getMetadata, getResource, translate } from '@hcengineering/platform' import { getMetadata, getResource, translate } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
import serverCore from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
getEmployee, getEmployee,
getPersonAccount, getPersonAccount,
getPersonAccountById getPersonAccountById
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import activity, { ActivityMessage } from '@hcengineering/activity' import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
import { Content, NotifyResult } from './types' import { Content, NotifyResult } from './types'
import { import {
getHTMLPresenter, getHTMLPresenter,
@ -139,6 +147,7 @@ export async function getCommonNotificationTxes (
data, data,
_class, _class,
modifiedOn, modifiedOn,
sender as Ref<PersonAccount>,
notifyResult.push notifyResult.push
) )
} }
@ -369,6 +378,7 @@ export async function pushInboxNotifications (
data: Partial<Data<InboxNotification>>, data: Partial<Data<InboxNotification>>,
_class: Ref<Class<InboxNotification>>, _class: Ref<Class<InboxNotification>>,
modifiedOn: Timestamp, modifiedOn: Timestamp,
senderId: Ref<PersonAccount>,
shouldPush: boolean, shouldPush: boolean,
shouldUpdateTimestamp = true shouldUpdateTimestamp = true
): Promise<void> { ): Promise<void> {
@ -410,10 +420,15 @@ export async function pushInboxNotifications (
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
res.push(notificationTx) res.push(notificationTx)
if (shouldPush) { if (shouldPush) {
const pushTx = await createPushFromInbox(control, targetUser, space, docNotifyContextId, notificationData, _class) await createPushFromInbox(
if (pushTx !== undefined) { control,
res.push(pushTx) targetUser,
} docNotifyContextId,
notificationData,
_class,
senderId,
notificationTx.objectId
)
} }
} }
} }
@ -467,11 +482,12 @@ async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>
export async function createPushFromInbox ( export async function createPushFromInbox (
control: TriggerControl, control: TriggerControl,
targetUser: Ref<Account>, targetUser: Ref<Account>,
space: Ref<Space>,
docNotifyContextId: Ref<DocNotifyContext>, docNotifyContextId: Ref<DocNotifyContext>,
data: Data<InboxNotification>, data: Data<InboxNotification>,
_class: Ref<Class<InboxNotification>> _class: Ref<Class<InboxNotification>>,
): Promise<Tx | undefined> { senderId: Ref<PersonAccount>,
_id: string
): Promise<void> {
let title: string = '' let title: string = ''
let body: string = '' let body: string = ''
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) { if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
@ -482,9 +498,14 @@ export async function createPushFromInbox (
if (title === '' || body === '') { if (title === '' || body === '') {
return return
} }
return await createPushNotification(control, targetUser, space, title, body, [ const sender = (await control.modelDb.findAll(contact.class.PersonAccount, { _id: senderId }))[0]
'',
'', let senderPerson: Person | undefined
if (sender !== undefined) {
senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
}
await createPushNotification(control, targetUser, title, body, _id, senderPerson?.avatar, [
notificationId, notificationId,
docNotifyContextId docNotifyContextId
]) ])
@ -493,24 +514,62 @@ export async function createPushFromInbox (
export async function createPushNotification ( export async function createPushNotification (
control: TriggerControl, control: TriggerControl,
targetUser: Ref<Account>, targetUser: Ref<Account>,
space: Ref<Space>,
title: string, title: string,
body: string, body: string,
onClick?: string[] _id: string,
): Promise<TxCreateDoc<BrowserNotification>> { senderAvatar?: string | null,
const data: Data<BrowserNotification> = { subPath?: string[]
user: targetUser, ): Promise<void> {
status: NotificationStatus.New, const publicKey = getMetadata(notification.metadata.PushPublicKey)
const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey)
const subject = getMetadata(serverNotification.metadata.PushSubject) ?? 'mailto:hey@huly.io'
if (privateKey === undefined || publicKey === undefined) return
const subscriptions = (await control.queryFind(notification.class.PushSubscription, {})).filter(
(p) => p.user === targetUser
)
const data: PushData = {
title, title,
body body
} }
if (onClick !== undefined) { if (_id !== undefined) {
data.onClickLocation = { data.tag = _id
path: onClick }
const front = getMetadata(serverCore.metadata.FrontUrl) ?? ''
const uploadUrl = getMetadata(serverCore.metadata.UploadURL) ?? ''
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
const domain = concatLink(front, domainPath)
data.domain = domain
if (subPath !== undefined) {
const path = [domainPath, ...subPath].join('/')
const url = concatLink(front, path)
data.url = url
}
if (senderAvatar != null) {
const provider = getAvatarProviderId(senderAvatar)
if (provider === contact.avatarProvider.Image) {
if (senderAvatar.includes('://')) {
data.icon = senderAvatar
} else {
data.icon = concatLink(uploadUrl, `?file=${senderAvatar}`)
}
} else if (provider === contact.avatarProvider.Gravatar) {
data.icon = getGravatarUrl(senderAvatar.split('://')[1], 'medium')
}
}
webpush.setVapidDetails(subject, publicKey, privateKey)
for (const subscription of subscriptions) {
try {
await webpush.sendNotification(subscription, JSON.stringify(data))
} catch (err) {
console.log('Cannot send push notification to', targetUser, err)
if (err instanceof WebPushError && err.body.includes('expired')) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply([tx], true)
}
} }
} }
const res = control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, space, data)
return res
} }
/** /**
@ -555,6 +614,7 @@ export async function pushActivityInboxNotifications (
data, data,
notification.class.ActivityInboxNotification, notification.class.ActivityInboxNotification,
activityMessage.modifiedOn, activityMessage.modifiedOn,
originTx.modifiedBy as Ref<PersonAccount>,
shouldPush, shouldPush,
shouldUpdateTimestamp shouldUpdateTimestamp
) )

View File

@ -134,7 +134,9 @@ export interface NotificationPresenter extends Class<Doc> {
*/ */
export default plugin(serverNotificationId, { export default plugin(serverNotificationId, {
metadata: { metadata: {
SesUrl: '' as Metadata<string> SesUrl: '' as Metadata<string>,
PushPrivateKey: '' as Metadata<string>,
PushSubject: '' as Metadata<string>
}, },
mixin: { mixin: {
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>, HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,

View File

@ -41,6 +41,7 @@ const serverCore = plugin(serverCoreId, {
}, },
metadata: { metadata: {
FrontUrl: '' as Metadata<string>, FrontUrl: '' as Metadata<string>,
UploadURL: '' as Metadata<string>,
CursorMaxTimeMS: '' as Metadata<string>, CursorMaxTimeMS: '' as Metadata<string>,
ElasticIndexName: '' as Metadata<string> ElasticIndexName: '' as Metadata<string>
} }

View File

@ -50,19 +50,25 @@ export function storageConfigFromEnv (): StorageConfiguration {
return storageConfig return storageConfig
} }
export function serverConfigFromEnv (): { export interface ServerEnv {
url: string url: string
elasticUrl: string elasticUrl: string
serverSecret: string serverSecret: string
rekoniUrl: string rekoniUrl: string
frontUrl: string frontUrl: string
uploadUrl: string
sesUrl: string | undefined sesUrl: string | undefined
accountsUrl: string accountsUrl: string
serverPort: number serverPort: number
serverFactory: ServerFactory serverFactory: ServerFactory
enableCompression: boolean enableCompression: boolean
elasticIndexName: string elasticIndexName: string
} { pushPublicKey: string | undefined
pushPrivateKey: string | undefined
pushSubject: string | undefined
}
export function serverConfigFromEnv (): ServerEnv {
const serverPort = parseInt(process.env.SERVER_PORT ?? '3333') const serverPort = parseInt(process.env.SERVER_PORT ?? '3333')
const serverFactory = serverFactories[(process.env.SERVER_PROVIDER as string) ?? 'ws'] ?? serverFactories.ws const serverFactory = serverFactories[(process.env.SERVER_PROVIDER as string) ?? 'ws'] ?? serverFactories.ws
const enableCompression = (process.env.ENABLE_COMPRESSION ?? 'true') === 'true' const enableCompression = (process.env.ENABLE_COMPRESSION ?? 'true') === 'true'
@ -102,6 +108,12 @@ export function serverConfigFromEnv (): {
process.exit(1) process.exit(1)
} }
const uploadUrl = process.env.UPLOAD_URL
if (uploadUrl === undefined) {
console.log('Please provide UPLOAD_URL url')
process.exit(1)
}
const sesUrl = process.env.SES_URL const sesUrl = process.env.SES_URL
const accountsUrl = process.env.ACCOUNTS_URL const accountsUrl = process.env.ACCOUNTS_URL
@ -109,6 +121,10 @@ export function serverConfigFromEnv (): {
console.log('Please provide ACCOUNTS_URL url') console.log('Please provide ACCOUNTS_URL url')
process.exit(1) process.exit(1)
} }
const pushPublicKey = process.env.PUSH_PUBLIC_KEY
const pushPrivateKey = process.env.PUSH_PRIVATE_KEY
const pushSubject = process.env.PUSH_SUBJECT
return { return {
url, url,
elasticUrl, elasticUrl,
@ -116,11 +132,15 @@ export function serverConfigFromEnv (): {
serverSecret, serverSecret,
rekoniUrl, rekoniUrl,
frontUrl, frontUrl,
uploadUrl,
sesUrl, sesUrl,
accountsUrl, accountsUrl,
serverPort, serverPort,
serverFactory, serverFactory,
enableCompression enableCompression,
pushPublicKey,
pushPrivateKey,
pushSubject
} }
} }

View File

@ -102,6 +102,7 @@ services:
- MINIO_SECRET_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin
- REKONI_URL=http://rekoni:4005 - REKONI_URL=http://rekoni:4005
- FRONT_URL=http://localhost:8083 - FRONT_URL=http://localhost:8083
- UPLOAD_URL=http://localhost:8083/files
- ACCOUNTS_URL=http://account:3003 - ACCOUNTS_URL=http://account:3003
- LAST_NAME_FIRST=true - LAST_NAME_FIRST=true
- ELASTIC_INDEX_NAME=local_storage_index - ELASTIC_INDEX_NAME=local_storage_index