feat(settings): support per-channel WeChat OAuth and persist payment options

This commit is contained in:
IanShaw027
2026-04-21 07:48:42 -07:00
parent d5819181ea
commit 54dc176725
16 changed files with 1015 additions and 404 deletions

View File

@@ -32,7 +32,7 @@ export type PaymentVisibleMethodSource =
| "easypay_alipay"
| "official_wxpay"
| "easypay_wxpay";
export type WeChatConnectMode = "open" | "mp";
export type WeChatConnectMode = "open" | "mp" | "mobile";
export interface PaymentVisibleMethodSourceOption {
value: PaymentVisibleMethodSource;
@@ -108,11 +108,16 @@ const PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES: Record<
},
};
const WECHAT_CONNECT_MODE_OPTIONS: WeChatConnectModeOption[] = [
{ value: "open", labelZh: "微信开放平台", labelEn: "WeChat Open Platform" },
{ value: "open", labelZh: "PC 应用", labelEn: "PC App" },
{
value: "mp",
labelZh: "微信公众号 / 小程序",
labelEn: "WeChat Official Account / Mini Program",
labelZh: "公众号",
labelEn: "Official Account",
},
{
value: "mobile",
labelZh: "移动应用",
labelEn: "Mobile App",
},
];
const WECHAT_CONNECT_MODE_ALIASES: Record<string, WeChatConnectMode> = {
@@ -124,6 +129,9 @@ const WECHAT_CONNECT_MODE_ALIASES: Record<string, WeChatConnectMode> = {
official_account: "mp",
wechat_mp: "mp",
mini_program: "mp",
mobile: "mobile",
mobile_app: "mobile",
native_app: "mobile",
};
export function normalizeDefaultSubscriptionSettings(
@@ -234,34 +242,52 @@ export function normalizeWeChatConnectMode(source: unknown): WeChatConnectMode {
}
export function defaultWeChatConnectScopesForMode(mode: unknown): string {
return normalizeWeChatConnectMode(mode) === "mp"
? "snsapi_userinfo"
: "snsapi_login";
switch (normalizeWeChatConnectMode(mode)) {
case "mp":
return "snsapi_userinfo";
case "mobile":
return "";
default:
return "snsapi_login";
}
}
export function resolveWeChatConnectModeCapabilities(
openEnabled: unknown,
mpEnabled: unknown,
mobileEnabled: unknown,
legacyMode: unknown,
): { openEnabled: boolean; mpEnabled: boolean } {
if (typeof openEnabled === "boolean" || typeof mpEnabled === "boolean") {
): { openEnabled: boolean; mpEnabled: boolean; mobileEnabled: boolean } {
if (
typeof openEnabled === "boolean" ||
typeof mpEnabled === "boolean" ||
typeof mobileEnabled === "boolean"
) {
return {
openEnabled: openEnabled === true,
mpEnabled: mpEnabled === true,
mobileEnabled: mobileEnabled === true,
};
}
return normalizeWeChatConnectMode(legacyMode) === "mp"
? { openEnabled: false, mpEnabled: true }
: { openEnabled: true, mpEnabled: false };
switch (normalizeWeChatConnectMode(legacyMode)) {
case "mp":
return { openEnabled: false, mpEnabled: true, mobileEnabled: false };
case "mobile":
return { openEnabled: false, mpEnabled: false, mobileEnabled: true };
default:
return { openEnabled: true, mpEnabled: false, mobileEnabled: false };
}
}
export function deriveWeChatConnectStoredMode(
openEnabled: boolean,
mpEnabled: boolean,
mobileEnabled: boolean,
legacyMode: unknown,
): WeChatConnectMode {
if (mpEnabled) return "mp";
if (mobileEnabled) return "mobile";
if (openEnabled) return "open";
return normalizeWeChatConnectMode(legacyMode);
}
@@ -342,8 +368,15 @@ export interface SystemSettings {
wechat_connect_enabled: boolean;
wechat_connect_app_id: string;
wechat_connect_app_secret_configured: boolean;
wechat_connect_open_app_id?: string;
wechat_connect_open_app_secret_configured?: boolean;
wechat_connect_mp_app_id?: string;
wechat_connect_mp_app_secret_configured?: boolean;
wechat_connect_mobile_app_id?: string;
wechat_connect_mobile_app_secret_configured?: boolean;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mobile_enabled?: boolean;
wechat_connect_mode: string;
wechat_connect_scopes: string;
wechat_connect_redirect_url: string;
@@ -501,8 +534,15 @@ export interface UpdateSettingsRequest {
wechat_connect_enabled?: boolean;
wechat_connect_app_id?: string;
wechat_connect_app_secret?: string;
wechat_connect_open_app_id?: string;
wechat_connect_open_app_secret?: string;
wechat_connect_mp_app_id?: string;
wechat_connect_mp_app_secret?: string;
wechat_connect_mobile_app_id?: string;
wechat_connect_mobile_app_secret?: string;
wechat_connect_open_enabled?: boolean;
wechat_connect_mp_enabled?: boolean;
wechat_connect_mobile_enabled?: boolean;
wechat_connect_mode?: string;
wechat_connect_scopes?: string;
wechat_connect_redirect_url?: string;

View File

@@ -57,6 +57,11 @@ const disabledHint = computed(() => {
return t('auth.oauthFlow.wechatSystemBrowserOnly')
case 'wechat_browser_required':
return t('auth.oauthFlow.wechatBrowserOnly')
case 'native_app_required':
return localizeWeChatHint(
'当前仅配置微信移动应用登录,需要在原生 App 中通过微信 SDK 发起授权。',
'This site only has WeChat mobile app login configured. Continue from the native app through the WeChat SDK.',
)
case 'not_configured':
return t('auth.oauthFlow.wechatNotConfigured')
default:

View File

@@ -344,6 +344,7 @@ export const useAppStore = defineStore('app', () => {
wechat_oauth_enabled: false,
wechat_oauth_open_enabled: false,
wechat_oauth_mp_enabled: false,
wechat_oauth_mobile_enabled: false,
oidc_oauth_enabled: false,
oidc_oauth_provider_name: 'OIDC',
backend_mode_enabled: false,

View File

@@ -168,6 +168,7 @@ export interface PublicSettings {
wechat_oauth_enabled: boolean
wechat_oauth_open_enabled?: boolean
wechat_oauth_mp_enabled?: boolean
wechat_oauth_mobile_enabled?: boolean
oidc_oauth_enabled: boolean
oidc_oauth_provider_name: string
backend_mode_enabled: boolean

View File

@@ -1398,101 +1398,253 @@
v-if="form.wechat_connect_enabled"
class="space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
<div class="space-y-4">
<div
class="rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="font-medium text-gray-900 dark:text-white">
{{ localText("PC 应用", "PC App") }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{
localText(
"桌面浏览器通过微信开放平台扫码登录。可与公众号或移动应用同时存在。",
"Desktop browsers sign in through WeChat Open Platform QR login. This can coexist with Official Account or Mobile App.",
)
}}
</p>
</div>
<Toggle
:model-value="form.wechat_connect_open_enabled"
data-testid="wechat-connect-open-enabled"
@update:model-value="handleWeChatOpenEnabledChange"
/>
</div>
<div
v-if="form.wechat_connect_open_enabled"
class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
>
{{ t("admin.settings.wechatConnect.appIdLabel") }}
</label>
<input
data-testid="wechat-connect-app-id"
v-model="form.wechat_connect_app_id"
type="text"
class="input font-mono text-sm"
:placeholder="t('admin.settings.wechatConnect.appIdPlaceholder')"
/>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ localText("PC AppID", "PC App ID") }}
</label>
<input
v-model="form.wechat_connect_open_app_id"
type="text"
class="input font-mono text-sm"
:placeholder="
localText(
'微信开放平台 PC 应用 AppID',
'WeChat Open Platform PC App ID',
)
"
/>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ localText("PC AppSecret", "PC App Secret") }}
</label>
<input
v-model="form.wechat_connect_open_app_secret"
type="password"
class="input font-mono text-sm"
:placeholder="
form.wechat_connect_open_app_secret_configured
? localText(
'密钥已配置,留空以保留当前值。',
'Secret configured. Leave empty to keep the current value.',
)
: localText(
'微信开放平台 PC 应用 AppSecret',
'WeChat Open Platform PC App Secret',
)
"
/>
</div>
</div>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
<div
class="rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="font-medium text-gray-900 dark:text-white">
{{ localText("公众号", "Official Account") }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{
localText(
"仅在微信内浏览器可用;非微信环境下会显示不可用。",
"Only available inside the WeChat browser. It is shown as unavailable outside WeChat.",
)
}}
</p>
</div>
<Toggle
:model-value="form.wechat_connect_mp_enabled"
data-testid="wechat-connect-mp-enabled"
@update:model-value="handleWeChatMPEnabledChange"
/>
</div>
<div
v-if="form.wechat_connect_mp_enabled"
class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
>
{{ t("admin.settings.wechatConnect.appSecretLabel") }}
</label>
<input
data-testid="wechat-connect-app-secret"
v-model="form.wechat_connect_app_secret"
type="password"
class="input font-mono text-sm"
:placeholder="
form.wechat_connect_app_secret_configured
? t('admin.settings.wechatConnect.appSecretConfiguredPlaceholder')
: t('admin.settings.wechatConnect.appSecretPlaceholder')
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
form.wechat_connect_app_secret_configured
? t('admin.settings.wechatConnect.appSecretConfiguredHint')
: t('admin.settings.wechatConnect.appSecretHint')
}}
</p>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ localText("公众号 AppID", "Official Account App ID") }}
</label>
<input
v-model="form.wechat_connect_mp_app_id"
type="text"
class="input font-mono text-sm"
:placeholder="
localText(
'公众号 AppID',
'Official Account App ID',
)
"
/>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
localText(
"公众号 AppSecret",
"Official Account App Secret",
)
}}
</label>
<input
v-model="form.wechat_connect_mp_app_secret"
type="password"
class="input font-mono text-sm"
:placeholder="
form.wechat_connect_mp_app_secret_configured
? localText(
'密钥已配置,留空以保留当前值。',
'Secret configured. Leave empty to keep the current value.',
)
: localText(
'公众号 AppSecret',
'Official Account App Secret',
)
"
/>
</div>
</div>
</div>
<div
class="rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="font-medium text-gray-900 dark:text-white">
{{ localText("移动应用", "Mobile App") }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{
localText(
"原生移动端通过微信 SDK 唤起授权,网页端不会直接发起该流程。",
"Native mobile clients start authorization through the WeChat SDK. The web UI does not launch this flow directly.",
)
}}
</p>
</div>
<Toggle
:model-value="form.wechat_connect_mobile_enabled"
data-testid="wechat-connect-mobile-enabled"
@update:model-value="handleWeChatMobileEnabledChange"
/>
</div>
<div
v-if="form.wechat_connect_mobile_enabled"
class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ localText("移动应用 AppID", "Mobile App ID") }}
</label>
<input
v-model="form.wechat_connect_mobile_app_id"
type="text"
class="input font-mono text-sm"
:placeholder="
localText(
'移动应用 AppID',
'Mobile App ID',
)
"
/>
</div>
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ localText("移动应用 AppSecret", "Mobile App Secret") }}
</label>
<input
v-model="form.wechat_connect_mobile_app_secret"
type="password"
class="input font-mono text-sm"
:placeholder="
form.wechat_connect_mobile_app_secret_configured
? localText(
'密钥已配置,留空以保留当前值。',
'Secret configured. Leave empty to keep the current value.',
)
: localText(
'移动应用 AppSecret',
'Mobile App Secret',
)
"
/>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div class="space-y-3">
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.wechatConnect.modeLabel") }}
</label>
<div
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
<div>
<div class="font-medium text-gray-900 dark:text-white">
{{ t("admin.settings.wechatConnect.openModeLabel") }}
</div>
<p
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.settings.wechatConnect.openModeHint") }}
</p>
</div>
<Toggle
v-model="form.wechat_connect_open_enabled"
data-testid="wechat-connect-open-enabled"
@update:model-value="syncWeChatConnectMode"
/>
</div>
<div
class="flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
<div>
<div class="font-medium text-gray-900 dark:text-white">
{{ t("admin.settings.wechatConnect.mpModeLabel") }}
</div>
<p
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.settings.wechatConnect.mpModeHint") }}
</p>
</div>
<Toggle
v-model="form.wechat_connect_mp_enabled"
data-testid="wechat-connect-mp-enabled"
@update:model-value="syncWeChatConnectMode"
/>
</div>
</div>
<div
v-if="
form.wechat_connect_open_enabled &&
(form.wechat_connect_mp_enabled ||
form.wechat_connect_mobile_enabled)
"
class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-900/40 dark:bg-amber-900/10 dark:text-amber-300"
>
{{
localText(
"如果同时启用 PC 应用和公众号/移动应用,这些应用需要挂在同一个微信开放平台主体下,否则 UnionID 无法稳定归并账号。",
"When PC App is enabled together with Official Account or Mobile App, they should belong to the same WeChat Open Platform account so UnionID can merge identities reliably.",
)
}}
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.wechatConnect.redirectUrlLabel") }}
{{
localText(
"浏览器回调地址",
"Browser Redirect URL",
)
}}
</label>
<input
data-testid="wechat-connect-redirect-url"
@@ -1501,6 +1653,14 @@
class="input font-mono text-sm"
:placeholder="t('admin.settings.wechatConnect.redirectUrlPlaceholder')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
localText(
"用于 PC 应用和公众号的网页回调。移动应用走原生 SDK 时不直接使用这个浏览器回调。",
"Used by PC App and Official Account browser callbacks. Native mobile SDK flows do not start from this browser callback directly.",
)
}}
</p>
<div
class="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
@@ -4594,6 +4754,7 @@ import type {
SystemSettings,
UpdateSettingsRequest,
DefaultSubscriptionSetting,
WeChatConnectMode,
WebSearchEmulationConfig,
WebSearchProviderConfig,
WebSearchTestResult,
@@ -4731,14 +4892,20 @@ interface DefaultSubscriptionGroupOption {
type SettingsForm = Omit<
SystemSettings,
"wechat_connect_open_enabled" | "wechat_connect_mp_enabled"
| "wechat_connect_open_enabled"
| "wechat_connect_mp_enabled"
| "wechat_connect_mobile_enabled"
> & {
smtp_password: string;
turnstile_secret_key: string;
linuxdo_connect_client_secret: string;
wechat_connect_app_secret: string;
wechat_connect_open_app_secret: string;
wechat_connect_mp_app_secret: string;
wechat_connect_mobile_app_secret: string;
wechat_connect_open_enabled: boolean;
wechat_connect_mp_enabled: boolean;
wechat_connect_mobile_enabled: boolean;
oidc_connect_client_secret: string;
force_email_on_third_party_signup: boolean;
payment_visible_method_alipay_source: string;
@@ -4833,8 +5000,18 @@ const form = reactive<SettingsForm>({
wechat_connect_app_id: "",
wechat_connect_app_secret: "",
wechat_connect_app_secret_configured: false,
wechat_connect_open_app_id: "",
wechat_connect_open_app_secret: "",
wechat_connect_open_app_secret_configured: false,
wechat_connect_mp_app_id: "",
wechat_connect_mp_app_secret: "",
wechat_connect_mp_app_secret_configured: false,
wechat_connect_mobile_app_id: "",
wechat_connect_mobile_app_secret: "",
wechat_connect_mobile_app_secret_configured: false,
wechat_connect_open_enabled: false,
wechat_connect_mp_enabled: false,
wechat_connect_mobile_enabled: false,
wechat_connect_mode: "open",
wechat_connect_scopes: "snsapi_login",
wechat_connect_redirect_url: "",
@@ -5315,17 +5492,28 @@ const wechatRedirectUrlSuggestion = computed(() => {
return `${origin}/api/v1/auth/oauth/wechat/callback`;
});
function syncWeChatConnectMode() {
function syncWeChatConnectMode(preferredMode?: WeChatConnectMode) {
if (form.wechat_connect_mp_enabled && form.wechat_connect_mobile_enabled) {
if (preferredMode === "mobile") {
form.wechat_connect_mp_enabled = false;
} else {
form.wechat_connect_mobile_enabled = false;
}
}
const capabilities = resolveWeChatConnectModeCapabilities(
form.wechat_connect_open_enabled,
form.wechat_connect_mp_enabled,
form.wechat_connect_mobile_enabled,
form.wechat_connect_mode,
);
form.wechat_connect_open_enabled = capabilities.openEnabled;
form.wechat_connect_mp_enabled = capabilities.mpEnabled;
form.wechat_connect_mobile_enabled = capabilities.mobileEnabled;
form.wechat_connect_mode = deriveWeChatConnectStoredMode(
capabilities.openEnabled,
capabilities.mpEnabled,
capabilities.mobileEnabled,
form.wechat_connect_mode,
);
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
@@ -5333,6 +5521,27 @@ function syncWeChatConnectMode() {
);
}
function handleWeChatOpenEnabledChange(value: boolean) {
form.wechat_connect_open_enabled = value;
syncWeChatConnectMode(value ? "open" : undefined);
}
function handleWeChatMPEnabledChange(value: boolean) {
form.wechat_connect_mp_enabled = value;
if (value) {
form.wechat_connect_mobile_enabled = false;
}
syncWeChatConnectMode(value ? "mp" : undefined);
}
function handleWeChatMobileEnabledChange(value: boolean) {
form.wechat_connect_mobile_enabled = value;
if (value) {
form.wechat_connect_mp_enabled = false;
}
syncWeChatConnectMode(value ? "mobile" : undefined);
}
async function setAndCopyWeChatRedirectUrl() {
const url = wechatRedirectUrlSuggestion.value;
if (!url) return;
@@ -5476,16 +5685,22 @@ async function loadSettings() {
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
form.wechat_connect_app_secret = "";
form.wechat_connect_open_app_secret = "";
form.wechat_connect_mp_app_secret = "";
form.wechat_connect_mobile_app_secret = "";
const wechatCapabilities = resolveWeChatConnectModeCapabilities(
settings.wechat_connect_open_enabled,
settings.wechat_connect_mp_enabled,
settings.wechat_connect_mobile_enabled,
settings.wechat_connect_mode,
);
form.wechat_connect_open_enabled = wechatCapabilities.openEnabled;
form.wechat_connect_mp_enabled = wechatCapabilities.mpEnabled;
form.wechat_connect_mobile_enabled = wechatCapabilities.mobileEnabled;
form.wechat_connect_mode = deriveWeChatConnectStoredMode(
wechatCapabilities.openEnabled,
wechatCapabilities.mpEnabled,
wechatCapabilities.mobileEnabled,
settings.wechat_connect_mode,
);
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(
@@ -5649,6 +5864,16 @@ async function saveSettings() {
return;
}
if (form.wechat_connect_mp_enabled && form.wechat_connect_mobile_enabled) {
appStore.showError(
localText(
"公众号和移动应用不能同时启用。",
"Official Account and Mobile App cannot be enabled at the same time.",
),
);
return;
}
// Validate URL fields — novalidate disables browser-native checks, so we validate here
const isValidHttpUrl = (url: string): boolean => {
if (!url) return true;
@@ -5666,6 +5891,7 @@ async function saveSettings() {
const wechatStoredMode = deriveWeChatConnectStoredMode(
form.wechat_connect_open_enabled,
form.wechat_connect_mp_enabled,
form.wechat_connect_mobile_enabled,
form.wechat_connect_mode,
);
@@ -5714,10 +5940,24 @@ async function saveSettings() {
form.linuxdo_connect_client_secret || undefined,
linuxdo_connect_redirect_url: form.linuxdo_connect_redirect_url,
wechat_connect_enabled: form.wechat_connect_enabled,
wechat_connect_app_id: form.wechat_connect_app_id,
wechat_connect_app_id:
form.wechat_connect_open_app_id ||
form.wechat_connect_mp_app_id ||
form.wechat_connect_mobile_app_id ||
form.wechat_connect_app_id,
wechat_connect_app_secret: form.wechat_connect_app_secret || undefined,
wechat_connect_open_app_id: form.wechat_connect_open_app_id,
wechat_connect_open_app_secret:
form.wechat_connect_open_app_secret || undefined,
wechat_connect_mp_app_id: form.wechat_connect_mp_app_id,
wechat_connect_mp_app_secret:
form.wechat_connect_mp_app_secret || undefined,
wechat_connect_mobile_app_id: form.wechat_connect_mobile_app_id,
wechat_connect_mobile_app_secret:
form.wechat_connect_mobile_app_secret || undefined,
wechat_connect_open_enabled: form.wechat_connect_open_enabled,
wechat_connect_mp_enabled: form.wechat_connect_mp_enabled,
wechat_connect_mobile_enabled: form.wechat_connect_mobile_enabled,
wechat_connect_mode: wechatStoredMode,
wechat_connect_scopes:
defaultWeChatConnectScopesForMode(wechatStoredMode),
@@ -5847,16 +6087,23 @@ async function saveSettings() {
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
form.wechat_connect_app_secret = "";
form.wechat_connect_open_app_secret = "";
form.wechat_connect_mp_app_secret = "";
form.wechat_connect_mobile_app_secret = "";
const updatedWechatCapabilities = resolveWeChatConnectModeCapabilities(
updated.wechat_connect_open_enabled,
updated.wechat_connect_mp_enabled,
updated.wechat_connect_mobile_enabled,
updated.wechat_connect_mode,
);
form.wechat_connect_open_enabled = updatedWechatCapabilities.openEnabled;
form.wechat_connect_mp_enabled = updatedWechatCapabilities.mpEnabled;
form.wechat_connect_mobile_enabled =
updatedWechatCapabilities.mobileEnabled;
form.wechat_connect_mode = deriveWeChatConnectStoredMode(
updatedWechatCapabilities.openEnabled,
updatedWechatCapabilities.mpEnabled,
updatedWechatCapabilities.mobileEnabled,
updated.wechat_connect_mode,
);
form.wechat_connect_scopes = defaultWeChatConnectScopesForMode(

View File

@@ -184,7 +184,7 @@ import TotpLoginModal from '@/components/auth/TotpLoginModal.vue'
import Icon from '@/components/icons/Icon.vue'
import TurnstileWidget from '@/components/TurnstileWidget.vue'
import { useAuthStore, useAppStore } from '@/stores'
import { getPublicSettings, isTotp2FARequired } from '@/api/auth'
import { getPublicSettings, isTotp2FARequired, isWeChatWebOAuthEnabled } from '@/api/auth'
import type { TotpLoginResponse } from '@/types'
const { t } = useI18n()
@@ -258,7 +258,7 @@ onMounted(async () => {
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
wechatOAuthEnabled.value = settings.wechat_oauth_enabled
wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings)
backendModeEnabled.value = settings.backend_mode_enabled
oidcOAuthEnabled.value = settings.oidc_oauth_enabled
oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC'

View File

@@ -282,7 +282,12 @@ import WechatOAuthSection from '@/components/auth/WechatOAuthSection.vue'
import Icon from '@/components/icons/Icon.vue'
import TurnstileWidget from '@/components/TurnstileWidget.vue'
import { useAuthStore, useAppStore } from '@/stores'
import { getPublicSettings, validatePromoCode, validateInvitationCode } from '@/api/auth'
import {
getPublicSettings,
isWeChatWebOAuthEnabled,
validatePromoCode,
validateInvitationCode
} from '@/api/auth'
import { buildAuthErrorMessage } from '@/utils/authError'
import {
isRegistrationEmailSuffixAllowed,
@@ -385,7 +390,7 @@ onMounted(async () => {
turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API'
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
wechatOAuthEnabled.value = settings.wechat_oauth_enabled
wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings)
oidcOAuthEnabled.value = settings.oidc_oauth_enabled
oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC'
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(

View File

@@ -504,6 +504,8 @@ function resolveWeChatOAuthUnavailableMessage(): string {
return t('auth.oauthFlow.wechatSystemBrowserOnly')
case 'wechat_browser_required':
return t('auth.oauthFlow.wechatBrowserOnly')
case 'native_app_required':
return 'This WeChat sign-in flow is only available from the native mobile app.'
case 'not_configured':
return t('auth.oauthFlow.wechatNotConfigured')
default: