diff --git a/.env.sample b/.env.sample index e5e6049..b5fa3e0 100644 --- a/.env.sample +++ b/.env.sample @@ -5,10 +5,6 @@ REMNAWAVE_PANEL_URL=https://remnawave:3000 ### Replace with own API Token. Create in Remnawave Dashboard → Remnawave Settings → API Tokens section REMNAWAVE_API_TOKEN= -META_TITLE="Subscription page" -META_DESCRIPTION="Subscription page description" - - # Serve at custom root path, for example, this value can be: CUSTOM_SUB_PREFIX=sub # Do not place / at the start/end CUSTOM_SUB_PREFIX= diff --git a/backend/package-lock.json b/backend/package-lock.json index 3b8e991..b50488c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@remnawave/subscription-page", - "version": "7.0.3", + "version": "7.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@remnawave/subscription-page", - "version": "7.0.3", + "version": "7.0.4", "license": "AGPL-3.0-only", "dependencies": { "@kastov/request-ip": "^0.0.5", @@ -18,8 +18,8 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "11.1.9", "@nestjs/serve-static": "5.0.4", - "@remnawave/backend-contract": "2.4.0", - "@remnawave/subscription-page-types": "0.2.7", + "@remnawave/backend-contract": "2.4.1", + "@remnawave/subscription-page-types": "0.3.3", "axios": "^1.13.2", "class-transformer": "^0.5.1", "compression": "^1.8.1", @@ -1788,18 +1788,18 @@ } }, "node_modules/@remnawave/backend-contract": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.4.0.tgz", - "integrity": "sha512-klW7gluT6lQRRcH6lM+P/iu4AP5PfggQNPotC9A8ZeyPrdv6hnYgYGS86UjkIUOUhyHGMsdodNC/wgtkiO/anA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.4.1.tgz", + "integrity": "sha512-bMqJOa+BOLuUKijQugW1OrWkxkhCePt0wCnGtCrQSliVlX33T9A+VfLRfSoy60tqcjAKATAGG4rTyx/synV2fA==", "license": "AGPL-3.0-only", "dependencies": { "zod": "3.25.76" } }, "node_modules/@remnawave/subscription-page-types": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@remnawave/subscription-page-types/-/subscription-page-types-0.2.7.tgz", - "integrity": "sha512-9B15lqBZ474W6kOzOiQPEyHLuZUj73FV5h637vSmRv1ErE2/5KmCLCkil+dGMbrTTYwFXVQEDnRADSKIGpYViw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@remnawave/subscription-page-types/-/subscription-page-types-0.3.3.tgz", + "integrity": "sha512-r4W2UqJ9jvyAoV+dp+ORAov471Qj4VVaVrTyC6rMcyAayIdOUnlivLRz/cpLJ4aa8NNnSFyl+d3OZwE63T829g==", "license": "AGPL-3.0-only", "dependencies": { "zod": "3.25.76" diff --git a/backend/package.json b/backend/package.json index 80afa18..f14a62f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@remnawave/subscription-page", - "version": "7.0.3", + "version": "7.0.4", "description": "Remnawave Subscription Page", "private": false, "type": "commonjs", @@ -36,8 +36,8 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "11.1.9", "@nestjs/serve-static": "5.0.4", - "@remnawave/backend-contract": "2.4.0", - "@remnawave/subscription-page-types": "0.2.7", + "@remnawave/backend-contract": "2.4.1", + "@remnawave/subscription-page-types": "0.3.3", "axios": "^1.13.2", "class-transformer": "^0.5.1", "compression": "^1.8.1", diff --git a/backend/src/common/config/app-config/config.schema.ts b/backend/src/common/config/app-config/config.schema.ts index bcba25b..63194f7 100644 --- a/backend/src/common/config/app-config/config.schema.ts +++ b/backend/src/common/config/app-config/config.schema.ts @@ -16,29 +16,18 @@ export const configSchema = z .min(1, REQUIRED_REMNAWAVE_API_TOKEN_MESSAGE), SUBPAGE_CONFIG_UUID: z.string().default('00000000-0000-0000-0000-000000000000'), - - MARZBAN_LEGACY_LINK_ENABLED: z - .string() - .default('false') - .transform((val) => val === 'true'), - MARZBAN_LEGACY_SECRET_KEY: z.optional(z.string()), - - MARZBAN_LEGACY_SUBSCRIPTION_VALID_FROM: z.optional(z.string()), - CUSTOM_SUB_PREFIX: z.optional(z.string()), CADDY_AUTH_API_TOKEN: z.optional(z.string()), - - META_TITLE: z.string(), - META_DESCRIPTION: z.string(), - CLOUDFLARE_ZERO_TRUST_CLIENT_ID: z.optional(z.string()), CLOUDFLARE_ZERO_TRUST_CLIENT_SECRET: z.optional(z.string()), - SUBSCRIPTION_UI_DISPLAY_RAW_KEYS: z + MARZBAN_LEGACY_LINK_ENABLED: z .string() .default('false') .transform((val) => val === 'true'), + MARZBAN_LEGACY_SECRET_KEY: z.optional(z.string()), + MARZBAN_LEGACY_SUBSCRIPTION_VALID_FROM: z.optional(z.string()), }) .superRefine((data, ctx) => { if ( diff --git a/backend/src/modules/root/root.controller.ts b/backend/src/modules/root/root.controller.ts index b8d7bca..5c6fc7b 100644 --- a/backend/src/modules/root/root.controller.ts +++ b/backend/src/modules/root/root.controller.ts @@ -6,6 +6,7 @@ import { REQUEST_TEMPLATE_TYPE_VALUES, TRequestTemplateTypeKeys, } from '@remnawave/backend-contract'; +import { APP_CONFIG_ROUTE_WO_LEADING_PATH } from '@remnawave/subscription-page-types'; import { GetJWTPayload } from '@common/decorators/get-jwt-payload'; import { ClientIp } from '@common/decorators/get-ip'; @@ -13,7 +14,6 @@ import { IJwtPayload } from '@common/constants'; import { SubpageConfigService } from './subpage-config.service'; import { RootService } from './root.service'; -import { APP_CONFIG_ROUTE_WO_LEADING_PATH } from '@remnawave/subscription-page-types'; @Controller() export class RootController { diff --git a/backend/src/modules/root/root.service.ts b/backend/src/modules/root/root.service.ts index 2888012..adcf3b6 100644 --- a/backend/src/modules/root/root.service.ts +++ b/backend/src/modules/root/root.service.ts @@ -22,7 +22,6 @@ export class RootService { private readonly isMarzbanLegacyLinkEnabled: boolean; private readonly marzbanSecretKey?: string; - private readonly subscriptionUiDisplayRawKeys: boolean; constructor( private readonly configService: ConfigService, @@ -34,9 +33,6 @@ export class RootService { 'MARZBAN_LEGACY_LINK_ENABLED', ); this.marzbanSecretKey = this.configService.get('MARZBAN_LEGACY_SECRET_KEY'); - this.subscriptionUiDisplayRawKeys = this.configService.getOrThrow( - 'SUBSCRIPTION_UI_DISPLAY_RAW_KEYS', - ); } public async serveSubscriptionPage( @@ -203,9 +199,13 @@ export class RootService { return; } + const baseSettings = this.subpageConfigService.getBaseSettings( + subpageConfig.subpageConfigUuid, + ); + const subscriptionData = subscriptionDataResponse.response; - if (!this.subscriptionUiDisplayRawKeys) { + if (!baseSettings.showConnectionKeys) { subscriptionData.response.links = []; subscriptionData.response.ssConfLinks = {}; } @@ -217,12 +217,8 @@ export class RootService { }); res.render('index', { - metaTitle: this.configService - .getOrThrow('META_TITLE') - .replace(/^"|"$/g, ''), - metaDescription: this.configService - .getOrThrow('META_DESCRIPTION') - .replace(/^"|"$/g, ''), + metaTitle: baseSettings.metaTitle, + metaDescription: baseSettings.metaDescription, panelData: Buffer.from(JSON.stringify(subscriptionData)).toString('base64'), }); } catch (error) { diff --git a/backend/src/modules/root/subpage-config.service.ts b/backend/src/modules/root/subpage-config.service.ts index fefaa75..936dc2c 100644 --- a/backend/src/modules/root/subpage-config.service.ts +++ b/backend/src/modules/root/subpage-config.service.ts @@ -8,6 +8,7 @@ import { Logger } from '@nestjs/common'; import { SubscriptionPageRawConfigSchema, TSubscriptionPageRawConfig, + SUBPAGE_DEFAULT_CONFIG_UUID, } from '@remnawave/subscription-page-types'; import { decryptUuid, encryptUuid } from '@common/utils/crypt-utils'; @@ -107,7 +108,7 @@ export class SubpageConfigService implements OnApplicationBootstrap { public getEncryptedSubpageConfigUuid(subpageConfigUuidFromRemnawave: string | null): string { let uuidToEncrypt: string; - const isDefaultUuid = this.subpageConfigUuid === '00000000-0000-0000-0000-000000000000'; + const isDefaultUuid = this.subpageConfigUuid === SUBPAGE_DEFAULT_CONFIG_UUID; if (isDefaultUuid && subpageConfigUuidFromRemnawave) { uuidToEncrypt = subpageConfigUuidFromRemnawave; @@ -117,4 +118,26 @@ export class SubpageConfigService implements OnApplicationBootstrap { return encryptUuid(uuidToEncrypt, this.internalJwtSecret); } + + public getBaseSettings( + subpageConfigUuid: string | null, + ): TSubscriptionPageRawConfig['baseSettings'] { + const subpageConfig = this.subpageConfigMap.get( + subpageConfigUuid || SUBPAGE_DEFAULT_CONFIG_UUID, + ); + + if (!subpageConfig) { + return { + metaTitle: 'Subscription Page', + metaDescription: 'Subscription Page', + showConnectionKeys: false, + }; + } + + return { + metaTitle: subpageConfig.baseSettings.metaTitle, + metaDescription: subpageConfig.baseSettings.metaDescription, + showConnectionKeys: subpageConfig.baseSettings.showConnectionKeys, + }; + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62d599a..ac531be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@remnawave/subscription-page", - "version": "7.0.3", + "version": "7.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@remnawave/subscription-page", - "version": "7.0.3", + "version": "7.0.4", "license": "AGPL-3.0-only", "dependencies": { "@gfazioli/mantine-spinner": "^2.3.9", @@ -15,8 +15,8 @@ "@mantine/modals": "8.3.10", "@mantine/notifications": "8.3.10", "@mantine/nprogress": "8.3.10", - "@remnawave/backend-contract": "2.4.0", - "@remnawave/subscription-page-types": "0.2.7", + "@remnawave/backend-contract": "2.4.1", + "@remnawave/subscription-page-types": "0.3.3", "@tabler/icons-react": "^3.35.0", "clsx": "^2.1.1", "color-hash": "^2.0.2", @@ -1988,18 +1988,18 @@ } }, "node_modules/@remnawave/backend-contract": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.4.0.tgz", - "integrity": "sha512-klW7gluT6lQRRcH6lM+P/iu4AP5PfggQNPotC9A8ZeyPrdv6hnYgYGS86UjkIUOUhyHGMsdodNC/wgtkiO/anA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.4.1.tgz", + "integrity": "sha512-bMqJOa+BOLuUKijQugW1OrWkxkhCePt0wCnGtCrQSliVlX33T9A+VfLRfSoy60tqcjAKATAGG4rTyx/synV2fA==", "license": "AGPL-3.0-only", "dependencies": { "zod": "3.25.76" } }, "node_modules/@remnawave/subscription-page-types": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@remnawave/subscription-page-types/-/subscription-page-types-0.2.7.tgz", - "integrity": "sha512-9B15lqBZ474W6kOzOiQPEyHLuZUj73FV5h637vSmRv1ErE2/5KmCLCkil+dGMbrTTYwFXVQEDnRADSKIGpYViw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@remnawave/subscription-page-types/-/subscription-page-types-0.3.3.tgz", + "integrity": "sha512-r4W2UqJ9jvyAoV+dp+ORAov471Qj4VVaVrTyC6rMcyAayIdOUnlivLRz/cpLJ4aa8NNnSFyl+d3OZwE63T829g==", "license": "AGPL-3.0-only", "dependencies": { "zod": "3.25.76" diff --git a/frontend/package.json b/frontend/package.json index 139890f..68b9e2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "@remnawave/subscription-page", "private": false, "type": "module", - "version": "7.0.3", + "version": "7.0.4", "license": "AGPL-3.0-only", "author": "REMNAWAVE ", "homepage": "https://github.com/remnawave", @@ -34,8 +34,8 @@ "@mantine/modals": "8.3.10", "@mantine/notifications": "8.3.10", "@mantine/nprogress": "8.3.10", - "@remnawave/backend-contract": "2.4.0", - "@remnawave/subscription-page-types": "0.2.7", + "@remnawave/backend-contract": "2.4.1", + "@remnawave/subscription-page-types": "0.3.3", "@tabler/icons-react": "^3.35.0", "clsx": "^2.1.1", "color-hash": "^2.0.2", diff --git a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx index 3aae942..4943a18 100644 --- a/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx +++ b/frontend/src/widgets/main/installation-guide/installation-guide.connector.tsx @@ -4,7 +4,9 @@ import { TSubscriptionPagePlatformKey } from '@remnawave/subscription-page-types' import { Box, Button, ButtonVariant, Card, Group, NativeSelect, Stack, Title } from '@mantine/core' +import { notifications } from '@mantine/notifications' import { IconStar } from '@tabler/icons-react' +import { useClipboard } from '@mantine/hooks' import { useState } from 'react' import { constructSubscriptionUrl } from '@shared/utils/construct-subscription-url' @@ -33,6 +35,7 @@ export const InstallationGuideConnector = (props: IProps) => { const { t, currentLang, baseTranslations } = useTranslation() const { platforms, svgLibrary } = useAppConfig() + const { copy } = useClipboard({ timeout: 2_000 }) const subscription = useSubscription() const [selectedAppIndex, setSelectedAppIndex] = useState(0) @@ -69,14 +72,39 @@ export const InstallationGuideConnector = (props: IProps) => { ) const handleButtonClick = (button: TSubscriptionPageButtonConfig) => { - if (button.type === 'subscriptionLink') { - const formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { + let formattedUrl: string | undefined + + if (button.type === 'subscriptionLink' || button.type === 'copyButton') { + formattedUrl = TemplateEngine.formatWithMetaInfo(button.link, { username: subscription.user.username, subscriptionUrl }) - window.open(formattedUrl, '_blank') - } else if (button.type === 'external') { - window.open(button.link, '_blank') + } + + switch (button.type) { + case 'copyButton': { + if (!formattedUrl) return + + copy(formattedUrl) + notifications.show({ + title: t(baseTranslations.linkCopied), + message: t(baseTranslations.linkCopiedToClipboard), + color: 'cyan' + }) + break + } + case 'external': { + window.open(button.link, '_blank') + break + } + case 'subscriptionLink': { + if (!formattedUrl) return + + window.open(formattedUrl, '_blank') + break + } + default: + break } }