diff --git a/apps/client/package.json b/apps/client/package.json
index b757f932..1a1bce95 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -47,6 +47,7 @@
"react-helmet-async": "^2.0.5",
"react-i18next": "^15.0.1",
"react-router-dom": "^7.0.1",
+ "semver": "^7.7.1",
"socket.io-client": "^4.8.1",
"tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.18",
diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json
index b69d0b28..7f809a4e 100644
--- a/apps/client/public/locales/en-US/translation.json
+++ b/apps/client/public/locales/en-US/translation.json
@@ -351,5 +351,7 @@
"Created at: {{time}}": "Created at: {{time}}",
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
- "Character count: {{characterCount}}": "Character count: {{characterCount}}"
+ "Character count: {{characterCount}}": "Character count: {{characterCount}}",
+ "New update": "New update",
+ "{{latestVersion}} is available": "{{latestVersion}} is available"
}
diff --git a/apps/client/src/components/settings/app-version.tsx b/apps/client/src/components/settings/app-version.tsx
new file mode 100644
index 00000000..f5097d51
--- /dev/null
+++ b/apps/client/src/components/settings/app-version.tsx
@@ -0,0 +1,53 @@
+import { useAppVersion } from "@/features/workspace/queries/workspace-query.ts";
+import { isCloud } from "@/lib/config.ts";
+import classes from "@/components/settings/settings.module.css";
+import { Indicator, Text, Tooltip } from "@mantine/core";
+import React from "react";
+import semverGt from "semver/functions/gt";
+import { useTranslation } from "react-i18next";
+
+export default function AppVersion() {
+ const { t } = useTranslation();
+ const { data: appVersion } = useAppVersion(!isCloud());
+ let hasUpdate = false;
+ try {
+ hasUpdate =
+ appVersion &&
+ parseFloat(appVersion.latestVersion) > 0 &&
+ semverGt(appVersion.latestVersion, appVersion.currentVersion);
+ } catch (err) {
+ console.error(err);
+ }
+
+ return (
+
+
+
+
+ v{APP_VERSION}
+
+
+
+
+ );
+}
diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx
index 551758ce..16d94c64 100644
--- a/apps/client/src/components/settings/settings-sidebar.tsx
+++ b/apps/client/src/components/settings/settings-sidebar.tsx
@@ -27,6 +27,7 @@ import {
prefetchSsoProviders,
prefetchWorkspaceMembers,
} from "@/components/settings/settings-queries.tsx";
+import AppVersion from "@/components/settings/app-version.tsx";
interface DataItem {
label: string;
@@ -205,19 +206,8 @@ export default function SettingsSidebar() {
{menuItems}
- {!isCloud() && (
-
-
- v{APP_VERSION}
-
-
- )}
+
+ {!isCloud() && }
{isCloud() && (
diff --git a/apps/client/src/features/workspace/queries/workspace-query.ts b/apps/client/src/features/workspace/queries/workspace-query.ts
index 81901381..1d1f1c73 100644
--- a/apps/client/src/features/workspace/queries/workspace-query.ts
+++ b/apps/client/src/features/workspace/queries/workspace-query.ts
@@ -15,6 +15,7 @@ import {
revokeInvitation,
getWorkspace,
getWorkspacePublicData,
+ getAppVersion,
} from "@/features/workspace/services/workspace-service";
import { IPagination, QueryParams } from "@/lib/types.ts";
import { notifications } from "@mantine/notifications";
@@ -22,6 +23,7 @@ import {
ICreateInvite,
IInvitation,
IPublicWorkspace,
+ IVersion,
IWorkspace,
} from "@/features/workspace/types/workspace.types.ts";
import { IUser } from "@/features/user/types/user.types.ts";
@@ -153,3 +155,15 @@ export function useGetInvitationQuery(
enabled: !!invitationId,
});
}
+
+export function useAppVersion(
+ isEnabled: boolean,
+): UseQueryResult {
+ return useQuery({
+ queryKey: ["version"],
+ queryFn: () => getAppVersion(),
+ staleTime: 60 * 60 * 1000, // 1 hr
+ enabled: isEnabled,
+ refetchOnMount: true,
+ });
+}
diff --git a/apps/client/src/features/workspace/services/workspace-service.ts b/apps/client/src/features/workspace/services/workspace-service.ts
index 4f099750..ecaae58a 100644
--- a/apps/client/src/features/workspace/services/workspace-service.ts
+++ b/apps/client/src/features/workspace/services/workspace-service.ts
@@ -7,6 +7,7 @@ import {
IAcceptInvite,
IPublicWorkspace,
IInvitationLink,
+ IVersion,
} from "../types/workspace.types";
import { IPagination, QueryParams } from "@/lib/types.ts";
import { ISetupWorkspace } from "@/features/auth/types/auth.types.ts";
@@ -73,7 +74,6 @@ export async function getInviteLink(data: {
export async function resendInvitation(data: {
invitationId: string;
}): Promise {
- console.log(data);
await api.post("/workspace/invites/resend", data);
}
@@ -97,6 +97,11 @@ export async function createWorkspace(
return req.data;
}
+export async function getAppVersion(): Promise {
+ const req = await api.post("/version");
+ return req.data;
+}
+
export async function uploadLogo(file: File) {
const formData = new FormData();
formData.append("type", "workspace-logo");
diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts
index 478f2a69..4112ee5c 100644
--- a/apps/client/src/features/workspace/types/workspace.types.ts
+++ b/apps/client/src/features/workspace/types/workspace.types.ts
@@ -57,3 +57,9 @@ export interface IPublicWorkspace {
authProviders: IAuthProvider[];
hasLicenseKey?: boolean;
}
+
+export interface IVersion {
+ currentVersion: string;
+ latestVersion: string;
+ releaseUrl: string;
+}
diff --git a/apps/server/src/integrations/security/security.module.ts b/apps/server/src/integrations/security/security.module.ts
index 1a65b14e..5acb53b2 100644
--- a/apps/server/src/integrations/security/security.module.ts
+++ b/apps/server/src/integrations/security/security.module.ts
@@ -1,7 +1,10 @@
import { Module } from '@nestjs/common';
import { RobotsTxtController } from './robots.txt.controller';
+import { VersionController } from './version.controller';
+import { VersionService } from './version.service';
@Module({
- controllers: [RobotsTxtController],
+ controllers: [RobotsTxtController, VersionController],
+ providers: [VersionService],
})
export class SecurityModule {}
diff --git a/apps/server/src/integrations/security/version.controller.ts b/apps/server/src/integrations/security/version.controller.ts
new file mode 100644
index 00000000..ac2cd47d
--- /dev/null
+++ b/apps/server/src/integrations/security/version.controller.ts
@@ -0,0 +1,27 @@
+import {
+ Controller,
+ HttpCode,
+ HttpStatus,
+ NotFoundException,
+ Post,
+ UseGuards,
+} from '@nestjs/common';
+import { VersionService } from './version.service';
+import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
+import { EnvironmentService } from '../environment/environment.service';
+
+@UseGuards(JwtAuthGuard)
+@Controller('version')
+export class VersionController {
+ constructor(
+ private readonly versionService: VersionService,
+ private readonly environmentService: EnvironmentService,
+ ) {}
+
+ @HttpCode(HttpStatus.OK)
+ @Post()
+ async getVersion() {
+ if (this.environmentService.isCloud()) throw new NotFoundException();
+ return this.versionService.getVersion();
+ }
+}
diff --git a/apps/server/src/integrations/security/version.service.ts b/apps/server/src/integrations/security/version.service.ts
new file mode 100644
index 00000000..2ceee86e
--- /dev/null
+++ b/apps/server/src/integrations/security/version.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@nestjs/common';
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const packageJson = require('./../../../package.json');
+
+@Injectable()
+export class VersionService {
+ constructor() {}
+
+ async getVersion() {
+ const url = `https://api.github.com/repos/docmost/docmost/releases/latest`;
+
+ let latestVersion = 0;
+ try {
+ const response = await fetch(url);
+ if (!response.ok) return;
+ const data = await response.json();
+ latestVersion = data?.tag_name?.replace('v', '');
+ } catch (err) {
+ /* empty */
+ }
+
+ return {
+ currentVersion: packageJson?.version,
+ latestVersion: latestVersion,
+ releaseUrl: 'https://github.com/docmost/docmost/releases',
+ };
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 74cc3120..d81c0246 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -311,6 +311,9 @@ importers:
react-router-dom:
specifier: ^7.0.1
version: 7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ semver:
+ specifier: ^7.7.1
+ version: 7.7.1
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
@@ -8056,6 +8059,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -13563,7 +13571,7 @@ snapshots:
fast-glob: 3.3.2
is-glob: 4.0.3
minimatch: 9.0.4
- semver: 7.6.3
+ semver: 7.7.1
ts-api-utils: 1.3.0(typescript@5.7.2)
optionalDependencies:
typescript: 5.7.2
@@ -18024,6 +18032,8 @@ snapshots:
semver@7.6.3: {}
+ semver@7.7.1: {}
+
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0