Skip to content

Commit fc72430

Browse files
committed
feat(admin): add server update notification support
1 parent a683b57 commit fc72430

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+344
-50
lines changed

backend/src/app.constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
* This file is part of Sync-in | The open source file sync and share solution
44
* See the LICENSE file for licensing details
55
*/
6+
import { version } from '../../package.json'
67

8+
export const VERSION = version
9+
export const USER_AGENT = `sync-in-server/${version}`
710
export const CONTENT_SECURITY_POLICY = (onlyOfficeServer: string) => ({
811
useDefaults: false,
912
directives: {

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { HttpModule } from '@nestjs/axios'
88
import { Module } from '@nestjs/common'
99
import { ConfigModule } from '@nestjs/config'
1010
import { LoggerModule } from 'nestjs-pino'
11+
import { USER_AGENT } from './app.constants'
1112
import { AppService } from './app.service'
1213
import { ApplicationsModule } from './applications/applications.module'
1314
import { AuthModule } from './authentication/auth.module'
@@ -36,6 +37,9 @@ import { SchedulerModule } from './infrastructure/scheduler/scheduler.module'
3637
ApplicationsModule,
3738
HttpModule.register({
3839
global: true,
40+
headers: {
41+
'User-Agent': USER_AGENT
42+
},
3943
timeout: 5000,
4044
maxRedirects: 5
4145
})

backend/src/applications/admin/admin.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
*/
66

77
import { Module } from '@nestjs/common'
8+
import { AdminSchedulerService } from './services/admin-scheduler.service'
9+
import { AdminService } from './services/admin.service'
810

911
@Module({
1012
controllers: [],
11-
providers: []
13+
providers: [AdminService, AdminSchedulerService]
1214
})
1315
export class AdminModule {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <[email protected]>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
export interface ServerReleaseVersionManifest {
8+
// tag_name: `v1.0.0`
9+
tag_name: string
10+
}
11+
12+
export interface ServerReleaseNotification {
13+
version: string
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <[email protected]>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
import { Injectable } from '@nestjs/common'
8+
import { Cron, CronExpression } from '@nestjs/schedule'
9+
import { setTimeout } from 'node:timers/promises'
10+
import { AdminService } from './admin.service'
11+
12+
@Injectable()
13+
export class AdminSchedulerService {
14+
constructor(private readonly adminService: AdminService) {}
15+
16+
@Cron(CronExpression.EVERY_DAY_AT_9AM)
17+
async checkServerUpdateAndNotify() {
18+
// Apply a random delay so instances don't trigger the check simultaneously
19+
const randomDelay = Math.floor(Math.random() * 900 * 1000)
20+
await setTimeout(randomDelay)
21+
await this.adminService.checkServerUpdateAndNotify()
22+
}
23+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <[email protected]>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
import { HttpService } from '@nestjs/axios'
8+
import { Test, TestingModule } from '@nestjs/testing'
9+
import { Cache } from '../../../infrastructure/cache/services/cache.service'
10+
import { NotificationsManager } from '../../notifications/services/notifications-manager.service'
11+
import { AdminUsersQueries } from '../../users/services/admin-users-queries.service'
12+
import { AdminService } from './admin.service'
13+
14+
describe(AdminService.name, () => {
15+
let service: AdminService
16+
17+
beforeAll(async () => {
18+
const module: TestingModule = await Test.createTestingModule({
19+
providers: [
20+
AdminService,
21+
{ provide: HttpService, useValue: {} },
22+
{ provide: Cache, useValue: {} },
23+
{ provide: AdminUsersQueries, useValue: {} },
24+
{ provide: NotificationsManager, useValue: {} }
25+
]
26+
}).compile()
27+
28+
service = module.get<AdminService>(AdminService)
29+
})
30+
31+
it('should be defined', () => {
32+
expect(service).toBeDefined()
33+
})
34+
})
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <[email protected]>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
import { HttpService } from '@nestjs/axios'
8+
import { Injectable, Logger } from '@nestjs/common'
9+
import type { AxiosResponse } from 'axios'
10+
import { VERSION } from '../../../app.constants'
11+
import { APP_URL } from '../../../common/shared'
12+
import { Cache } from '../../../infrastructure/cache/services/cache.service'
13+
import { HTTP_METHOD } from '../../applications.constants'
14+
import { NOTIFICATION_APP, NOTIFICATION_APP_EVENT } from '../../notifications/constants/notifications'
15+
import { NotificationContent } from '../../notifications/interfaces/notification-properties.interface'
16+
import type { UserMailNotification } from '../../notifications/interfaces/user-mail-notification.interface'
17+
import { NotificationsManager } from '../../notifications/services/notifications-manager.service'
18+
import { AdminUsersQueries } from '../../users/services/admin-users-queries.service'
19+
import type { ServerReleaseNotification, ServerReleaseVersionManifest } from '../interfaces/check-update.interfaces'
20+
import { isServerUpdateAvailable } from '../utils/check-update'
21+
22+
@Injectable()
23+
export class AdminService {
24+
private readonly logger = new Logger(AdminService.name)
25+
26+
constructor(
27+
private readonly http: HttpService,
28+
private readonly cache: Cache,
29+
private readonly notificationsManager: NotificationsManager,
30+
private readonly adminUsersQueries: AdminUsersQueries
31+
) {}
32+
33+
async checkServerUpdateAndNotify() {
34+
let lastVersion: string
35+
try {
36+
const res: AxiosResponse<ServerReleaseVersionManifest> = await this.http.axiosRef({
37+
method: HTTP_METHOD.GET,
38+
url: APP_URL.SERVER_VERSION_MANIFEST
39+
})
40+
lastVersion = res.data?.tag_name || ''
41+
} catch (e) {
42+
this.logger.warn(`${this.checkServerUpdateAndNotify.name} - unable to check update: ${e}`)
43+
return
44+
}
45+
if (!lastVersion.startsWith('v')) {
46+
this.logger.warn(`${this.checkServerUpdateAndNotify.name} - unable to check version: ${lastVersion}`)
47+
return
48+
}
49+
lastVersion = lastVersion.slice(1) // remove 'v' to compare with the current version
50+
if (!isServerUpdateAvailable(VERSION, lastVersion)) {
51+
return
52+
}
53+
// Get the last version that was notified to administrators
54+
const notifiedVersion: ServerReleaseNotification = await this.cache.get(this.checkServerUpdateAndNotify.name)
55+
if (!notifiedVersion?.version) {
56+
// The version was never stored, do it
57+
await this.cache.set(this.checkServerUpdateAndNotify.name, { version: VERSION } satisfies ServerReleaseNotification, 0)
58+
return
59+
}
60+
if (notifiedVersion.version === lastVersion) {
61+
// Notification was already sent to administrators
62+
return
63+
}
64+
const adminsToNotify: UserMailNotification[] = await this.adminUsersQueries.listAdminsToNotify()
65+
if (!adminsToNotify.length) {
66+
return
67+
}
68+
const notification: NotificationContent = {
69+
app: NOTIFICATION_APP.UPDATE_AVAILABLE,
70+
event: NOTIFICATION_APP_EVENT.UPDATE_AVAILABLE,
71+
element: lastVersion,
72+
externalUrl: APP_URL.RELEASES,
73+
url: null
74+
}
75+
this.notificationsManager
76+
.create(adminsToNotify, notification)
77+
.then(() => this.cache.set(this.checkServerUpdateAndNotify.name, { version: lastVersion } satisfies ServerReleaseNotification, 0))
78+
.catch((e: Error) => this.logger.error(`${this.checkServerUpdateAndNotify.name} - ${e}`))
79+
}
80+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <[email protected]>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
export function isServerUpdateAvailable(current: string, latest: string) {
8+
const c = current.split('.').map(Number)
9+
const l = latest.split('.').map(Number)
10+
const max = Math.max(c.length, l.length)
11+
12+
for (let i = 0; i < max; i++) {
13+
const cv = c[i] || 0
14+
const lv = l[i] || 0
15+
16+
if (lv > cv) return true
17+
if (lv < cv) return false
18+
}
19+
return false
20+
}

backend/src/applications/comments/services/comments-manager.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { FilesQueries } from '../../files/services/files-queries.service'
1111
import { dirName, fileName, getProps, isPathExists } from '../../files/utils/files'
1212
import { NOTIFICATION_APP, NOTIFICATION_APP_EVENT } from '../../notifications/constants/notifications'
1313
import { NotificationContent } from '../../notifications/interfaces/notification-properties.interface'
14-
import { UserMailNotification } from '../../notifications/interfaces/user-mail-notification'
14+
import { UserMailNotification } from '../../notifications/interfaces/user-mail-notification.interface'
1515
import { NotificationsManager } from '../../notifications/services/notifications-manager.service'
1616
import { SpaceEnv } from '../../spaces/models/space-env.model'
1717
import { UserModel } from '../../users/models/user.model'

backend/src/applications/comments/services/comments-queries.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { DB_TOKEN_PROVIDER } from '../../../infrastructure/database/constants'
1111
import type { DBSchema } from '../../../infrastructure/database/interfaces/database.interface'
1212
import { dbCheckAffectedRows, dbGetInsertedId } from '../../../infrastructure/database/utils'
1313
import { filePathSQL, files } from '../../files/schemas/files.schema'
14-
import { UserMailNotification } from '../../notifications/interfaces/user-mail-notification'
14+
import { UserMailNotification } from '../../notifications/interfaces/user-mail-notification.interface'
1515
import { shares } from '../../shares/schemas/shares.schema'
1616
import { SharesQueries } from '../../shares/services/shares-queries.service'
1717
import { SPACE_ALIAS, SPACE_REPOSITORY } from '../../spaces/constants/spaces'

0 commit comments

Comments
 (0)