mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(connection): keep the Happ add-subscription button working on Remnawave 2.8.0
Panel 2.8.0 removed the server-side happ-encrypt endpoint, so for users
without a stored crypto link the backend leaves {{HAPP_CRYPT4_LINK}}
unresolved and the subscriptionLink button silently disappeared
(isValidDeepLink requires '://').
- BlockButtons: resolve button templates client-side at render (with username,
like the panel's own subpage) instead of dropping the button; collapse
double-prefixed happ://crypt4/happ://cryptN/... resolvedUrl from old backends
- templateEngine: collapse a hardcoded happ://cryptN/ prefix before
{{HAPP_CRYPT[34]_LINK}}; cache crypt-link generation (jsencrypt RSA-4096 is
slow on weak devices and re-randomizes every call)
- Connection: in happ_cryptolink mode force the crypt link only when the button
fell back to the plain subscription link or kept unresolved templates - an
explicit Subpage link (e.g. happ://add/...) now wins, so Subpage edits apply
This commit is contained in:
@@ -48,6 +48,7 @@ interface Props {
|
|||||||
isTelegramWebApp: boolean;
|
isTelegramWebApp: boolean;
|
||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
onOpenQR?: () => void;
|
onOpenQR?: () => void;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InstallationGuide({
|
export default function InstallationGuide({
|
||||||
@@ -56,6 +57,7 @@ export default function InstallationGuide({
|
|||||||
isTelegramWebApp,
|
isTelegramWebApp,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
onOpenQR,
|
onOpenQR,
|
||||||
|
username,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
@@ -131,6 +133,7 @@ export default function InstallationGuide({
|
|||||||
subscriptionUrl={appConfig.subscriptionUrl}
|
subscriptionUrl={appConfig.subscriptionUrl}
|
||||||
hideLink={appConfig.hideLink}
|
hideLink={appConfig.hideLink}
|
||||||
deepLink={selectedApp?.deepLink}
|
deepLink={selectedApp?.deepLink}
|
||||||
|
username={username}
|
||||||
getLocalizedText={getLocalizedText}
|
getLocalizedText={getLocalizedText}
|
||||||
getBaseTranslation={getBaseTranslation}
|
getBaseTranslation={getBaseTranslation}
|
||||||
getSvgHtml={getSvgHtml}
|
getSvgHtml={getSvgHtml}
|
||||||
@@ -141,6 +144,7 @@ export default function InstallationGuide({
|
|||||||
appConfig.subscriptionUrl,
|
appConfig.subscriptionUrl,
|
||||||
appConfig.hideLink,
|
appConfig.hideLink,
|
||||||
selectedApp?.deepLink,
|
selectedApp?.deepLink,
|
||||||
|
username,
|
||||||
isLight,
|
isLight,
|
||||||
getLocalizedText,
|
getLocalizedText,
|
||||||
getBaseTranslation,
|
getBaseTranslation,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { CheckIcon, CopyIcon } from '@/components/icons';
|
import { CheckIcon, CopyIcon } from '@/components/icons';
|
||||||
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
||||||
import { copyToClipboard } from '@/utils/clipboard';
|
import { copyToClipboard } from '@/utils/clipboard';
|
||||||
|
import { collapseDoubledCryptPrefix, hasTemplates, resolveTemplate } from '@/utils/templateEngine';
|
||||||
import { blockButtonClass } from './buttonStyles';
|
import { blockButtonClass } from './buttonStyles';
|
||||||
|
|
||||||
// eslint-disable-next-line no-script-url
|
// eslint-disable-next-line no-script-url
|
||||||
@@ -29,6 +30,7 @@ interface BlockButtonsProps {
|
|||||||
subscriptionUrl: string | null;
|
subscriptionUrl: string | null;
|
||||||
hideLink?: boolean;
|
hideLink?: boolean;
|
||||||
deepLink?: string | null;
|
deepLink?: string | null;
|
||||||
|
username?: string;
|
||||||
getLocalizedText: (text: LocalizedText | undefined) => string;
|
getLocalizedText: (text: LocalizedText | undefined) => string;
|
||||||
getBaseTranslation: (key: string, i18nKey: string) => string;
|
getBaseTranslation: (key: string, i18nKey: string) => string;
|
||||||
getSvgHtml: (key: string | undefined) => string;
|
getSvgHtml: (key: string | undefined) => string;
|
||||||
@@ -42,6 +44,7 @@ export function BlockButtons({
|
|||||||
subscriptionUrl,
|
subscriptionUrl,
|
||||||
hideLink,
|
hideLink,
|
||||||
deepLink,
|
deepLink,
|
||||||
|
username,
|
||||||
getLocalizedText,
|
getLocalizedText,
|
||||||
getBaseTranslation,
|
getBaseTranslation,
|
||||||
getSvgHtml,
|
getSvgHtml,
|
||||||
@@ -73,8 +76,17 @@ export function BlockButtons({
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
if (btn.type === 'subscriptionLink') {
|
if (btn.type === 'subscriptionLink') {
|
||||||
const url = btn.resolvedUrl || btn.url || btn.link || deepLink || subscriptionUrl;
|
const raw = btn.resolvedUrl || btn.url || btn.link || deepLink || subscriptionUrl;
|
||||||
if (!url || !isValidDeepLink(url)) return null;
|
// The backend resolves {{HAPP_CRYPT*_LINK}} only when a crypto link is
|
||||||
|
// stored in the DB; since Remnawave 2.8.0 removed the encrypt endpoint,
|
||||||
|
// new users can get the raw template here. Resolve it client-side (the
|
||||||
|
// panel's own subpage does the same) instead of dropping the button.
|
||||||
|
const resolved =
|
||||||
|
raw && subscriptionUrl && hasTemplates(raw)
|
||||||
|
? resolveTemplate(raw, { subscriptionUrl, username })
|
||||||
|
: raw;
|
||||||
|
const url = resolved ? collapseDoubledCryptPrefix(resolved) : resolved;
|
||||||
|
if (!url || hasTemplates(url) || !isValidDeepLink(url)) return null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
|
|||||||
@@ -114,12 +114,20 @@ export default function Connection() {
|
|||||||
const openDeepLink = useCallback(
|
const openDeepLink = useCallback(
|
||||||
(deepLink: string) => {
|
(deepLink: string) => {
|
||||||
let resolved = deepLink;
|
let resolved = deepLink;
|
||||||
if (isHappCryptolinkMode(connectionLink?.connect_mode) && qrConnectionUrl) {
|
if (hasTemplates(resolved)) {
|
||||||
// In HAPP cryptolink mode always open the resolved happ://crypt... URL.
|
|
||||||
resolved = qrConnectionUrl;
|
|
||||||
} else if (hasTemplates(resolved)) {
|
|
||||||
resolved = resolveUrl(resolved);
|
resolved = resolveUrl(resolved);
|
||||||
}
|
}
|
||||||
|
// In HAPP cryptolink mode keep hiding the plain subscription link: force the
|
||||||
|
// happ://crypt... URL only when the button fell back to it or its template
|
||||||
|
// could not be resolved. An explicit link from the panel's Subpage config
|
||||||
|
// (e.g. happ://add/...) wins — admins expect Subpage edits to apply here.
|
||||||
|
if (
|
||||||
|
isHappCryptolinkMode(connectionLink?.connect_mode) &&
|
||||||
|
qrConnectionUrl &&
|
||||||
|
(!resolved || resolved === appConfig?.subscriptionUrl || hasTemplates(resolved))
|
||||||
|
) {
|
||||||
|
resolved = qrConnectionUrl;
|
||||||
|
}
|
||||||
const isHttpUrl = /^https?:\/\//i.test(resolved);
|
const isHttpUrl = /^https?:\/\//i.test(resolved);
|
||||||
const finalUrlForTelegram = isHttpUrl
|
const finalUrlForTelegram = isHttpUrl
|
||||||
? resolved
|
? resolved
|
||||||
@@ -140,7 +148,14 @@ export default function Connection() {
|
|||||||
// still navigate normally. (Telegram bug #654272.)
|
// still navigate normally. (Telegram bug #654272.)
|
||||||
openAppScheme(resolved);
|
openAppScheme(resolved);
|
||||||
},
|
},
|
||||||
[isTelegramWebApp, i18n.language, resolveUrl, connectionLink?.connect_mode, qrConnectionUrl],
|
[
|
||||||
|
isTelegramWebApp,
|
||||||
|
i18n.language,
|
||||||
|
resolveUrl,
|
||||||
|
connectionLink?.connect_mode,
|
||||||
|
qrConnectionUrl,
|
||||||
|
appConfig?.subscriptionUrl,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if any platform has configured apps
|
// Check if any platform has configured apps
|
||||||
@@ -217,6 +232,7 @@ export default function Connection() {
|
|||||||
isTelegramWebApp={isTelegramWebApp}
|
isTelegramWebApp={isTelegramWebApp}
|
||||||
onGoBack={handleGoBack}
|
onGoBack={handleGoBack}
|
||||||
onOpenQR={handleOpenQR}
|
onOpenQR={handleOpenQR}
|
||||||
|
username={user?.username ?? undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ export function hasTemplates(url: string): boolean {
|
|||||||
return TEMPLATE_RE.test(url);
|
return TEMPLATE_RE.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old bot backends resolve prefix-hardcoded Subpage templates
|
||||||
|
* (happ://crypt4/{{HAPP_CRYPT4_LINK}}) into happ://crypt4/happ://cryptN/... —
|
||||||
|
* strip the outer prefix so the deep link stays openable.
|
||||||
|
*/
|
||||||
|
export function collapseDoubledCryptPrefix(url: string): string {
|
||||||
|
return url.replace(/^happ:\/\/crypt\d+\/(?=happ:\/\/crypt)/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsencrypt's RSA-4096 takes up to tens of ms on weak devices and its padding is
|
||||||
|
// random (a new string every call) — cache per subscription URL so render-time
|
||||||
|
// resolution is cheap and stable within a session. Any single ciphertext is valid.
|
||||||
|
const cryptLinkCache = new Map<string, string | null>();
|
||||||
|
|
||||||
|
function cachedHappCryptoLink(url: string, version: 'v3' | 'v4'): string | null {
|
||||||
|
const key = `${version}|${url}`;
|
||||||
|
let link = cryptLinkCache.get(key);
|
||||||
|
if (link === undefined) {
|
||||||
|
if (cryptLinkCache.size >= 100) cryptLinkCache.clear();
|
||||||
|
link = createHappCryptoLink(url, version, true);
|
||||||
|
cryptLinkCache.set(key, link);
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
interface ResolveContext {
|
interface ResolveContext {
|
||||||
subscriptionUrl: string;
|
subscriptionUrl: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
@@ -14,6 +39,11 @@ interface ResolveContext {
|
|||||||
export function resolveTemplate(template: string, ctx: ResolveContext): string {
|
export function resolveTemplate(template: string, ctx: ResolveContext): string {
|
||||||
let result = template;
|
let result = template;
|
||||||
|
|
||||||
|
// {{HAPP_CRYPT*_LINK}} resolves to a FULL happ://crypt.../ deep link; when the
|
||||||
|
// template also hardcodes the prefix (happ://crypt4/{{HAPP_CRYPT4_LINK}}),
|
||||||
|
// collapse it so we don't produce happ://crypt4/happ://crypt5/...
|
||||||
|
result = result.replace(/happ:\/\/crypt\d+\/(?=\{\{HAPP_CRYPT[34]_LINK\}\})/gi, '');
|
||||||
|
|
||||||
result = result.replace(/\{\{SUBSCRIPTION_LINK\}\}/g, ctx.subscriptionUrl);
|
result = result.replace(/\{\{SUBSCRIPTION_LINK\}\}/g, ctx.subscriptionUrl);
|
||||||
|
|
||||||
if (ctx.username) {
|
if (ctx.username) {
|
||||||
@@ -21,11 +51,11 @@ export function resolveTemplate(template: string, ctx: ResolveContext): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = result.replace(/\{\{HAPP_CRYPT3_LINK\}\}/g, () => {
|
result = result.replace(/\{\{HAPP_CRYPT3_LINK\}\}/g, () => {
|
||||||
return createHappCryptoLink(ctx.subscriptionUrl, 'v3', true) ?? ctx.subscriptionUrl;
|
return cachedHappCryptoLink(ctx.subscriptionUrl, 'v3') ?? ctx.subscriptionUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
result = result.replace(/\{\{HAPP_CRYPT4_LINK\}\}/g, () => {
|
result = result.replace(/\{\{HAPP_CRYPT4_LINK\}\}/g, () => {
|
||||||
return createHappCryptoLink(ctx.subscriptionUrl, 'v4', true) ?? ctx.subscriptionUrl;
|
return cachedHappCryptoLink(ctx.subscriptionUrl, 'v4') ?? ctx.subscriptionUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user