locale.value.startsWith("zh"));
function localText(zh: string, en: string): string {
- return locale.value.startsWith("zh") ? zh : en;
+ return isZhLocale.value ? zh : en;
}
const paymentGuideHref = computed(() =>
@@ -5796,6 +6023,8 @@ type SettingsForm = Omit<
wechat_connect_mp_enabled: boolean;
wechat_connect_mobile_enabled: boolean;
oidc_connect_client_secret: string;
+ github_oauth_client_secret: string;
+ google_oauth_client_secret: string;
force_email_on_third_party_signup: boolean;
openai_advanced_scheduler_enabled: boolean;
};
@@ -5926,6 +6155,19 @@ const form = reactive({
oidc_connect_userinfo_email_path: "",
oidc_connect_userinfo_id_path: "",
oidc_connect_userinfo_username_path: "",
+ // GitHub / Google 邮箱快捷登录
+ github_oauth_enabled: false,
+ github_oauth_client_id: "",
+ github_oauth_client_secret: "",
+ github_oauth_client_secret_configured: false,
+ github_oauth_redirect_url: "",
+ github_oauth_frontend_redirect_url: "/auth/oauth/callback",
+ google_oauth_enabled: false,
+ google_oauth_client_id: "",
+ google_oauth_client_secret: "",
+ google_oauth_client_secret_configured: false,
+ google_oauth_redirect_url: "",
+ google_oauth_frontend_redirect_url: "/auth/oauth/callback",
// Model fallback
enable_model_fallback: false,
fallback_model_anthropic: "claude-3-5-sonnet-20241022",
@@ -5991,6 +6233,22 @@ const authSourceDefaultsMeta = computed(() => [
title: t("admin.settings.authSourceDefaults.sources.wechat.title"),
description: t("admin.settings.authSourceDefaults.sources.wechat.description"),
},
+ {
+ source: "github" as AuthSourceType,
+ title: "GitHub",
+ description: localText(
+ "通过 GitHub 已验证邮箱首次注册或首次绑定时应用。",
+ "Applied on first signup or first bind through a verified GitHub email.",
+ ),
+ },
+ {
+ source: "google" as AuthSourceType,
+ title: "Google",
+ description: localText(
+ "通过 Google 已验证邮箱首次注册或首次绑定时应用。",
+ "Applied on first signup or first bind through a verified Google email.",
+ ),
+ },
]);
// Proxies for web search emulation ProxySelector
@@ -6298,6 +6556,42 @@ async function setAndCopyLinuxdoRedirectUrl() {
);
}
+type EmailOAuthProvider = "github" | "google";
+
+const githubOAuthRedirectUrlSuggestion = computed(() => {
+ if (typeof window === "undefined") return "";
+ const origin =
+ window.location.origin ||
+ `${window.location.protocol}//${window.location.host}`;
+ return `${origin}/api/v1/auth/oauth/github/callback`;
+});
+
+const googleOAuthRedirectUrlSuggestion = computed(() => {
+ if (typeof window === "undefined") return "";
+ const origin =
+ window.location.origin ||
+ `${window.location.protocol}//${window.location.host}`;
+ return `${origin}/api/v1/auth/oauth/google/callback`;
+});
+
+async function setAndCopyEmailOAuthRedirectUrl(provider: EmailOAuthProvider) {
+ const url =
+ provider === "github"
+ ? githubOAuthRedirectUrlSuggestion.value
+ : googleOAuthRedirectUrlSuggestion.value;
+ if (!url) return;
+
+ if (provider === "github") {
+ form.github_oauth_redirect_url = url;
+ } else {
+ form.google_oauth_redirect_url = url;
+ }
+ await copyToClipboard(
+ url,
+ localText("回调地址已写入并复制。", "Callback URL set and copied."),
+ );
+}
+
const wechatRedirectUrlSuggestion = computed(() => {
if (typeof window === "undefined") return "";
const origin =
@@ -6488,6 +6782,8 @@ async function loadSettings() {
smtpPasswordManuallyEdited.value = false;
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
+ form.github_oauth_client_secret = "";
+ form.google_oauth_client_secret = "";
form.wechat_connect_app_secret = "";
form.wechat_connect_open_app_secret = "";
form.wechat_connect_mp_app_secret = "";
@@ -6846,6 +7142,20 @@ async function saveSettings() {
oidc_connect_userinfo_id_path: form.oidc_connect_userinfo_id_path,
oidc_connect_userinfo_username_path:
form.oidc_connect_userinfo_username_path,
+ github_oauth_enabled: form.github_oauth_enabled,
+ github_oauth_client_id: form.github_oauth_client_id,
+ github_oauth_client_secret:
+ form.github_oauth_client_secret || undefined,
+ github_oauth_redirect_url: form.github_oauth_redirect_url,
+ github_oauth_frontend_redirect_url:
+ form.github_oauth_frontend_redirect_url,
+ google_oauth_enabled: form.google_oauth_enabled,
+ google_oauth_client_id: form.google_oauth_client_id,
+ google_oauth_client_secret:
+ form.google_oauth_client_secret || undefined,
+ google_oauth_redirect_url: form.google_oauth_redirect_url,
+ google_oauth_frontend_redirect_url:
+ form.google_oauth_frontend_redirect_url,
enable_model_fallback: form.enable_model_fallback,
fallback_model_anthropic: form.fallback_model_anthropic,
fallback_model_openai: form.fallback_model_openai,
@@ -6960,6 +7270,8 @@ async function saveSettings() {
smtpPasswordManuallyEdited.value = false;
form.turnstile_secret_key = "";
form.linuxdo_connect_client_secret = "";
+ form.github_oauth_client_secret = "";
+ form.google_oauth_client_secret = "";
form.wechat_connect_app_secret = "";
form.wechat_connect_open_app_secret = "";
form.wechat_connect_mp_app_secret = "";
diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts
index bfd1861f..915d9425 100644
--- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts
+++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts
@@ -817,6 +817,24 @@ describe("admin SettingsView wechat connect controls", () => {
).toBe("/auth/wechat/callback");
});
+ it("links GitHub OAuth Apps guide to GitHub developer settings", async () => {
+ getSettings.mockResolvedValueOnce({
+ ...baseSettingsResponse,
+ github_oauth_enabled: true,
+ });
+
+ const wrapper = mountView();
+
+ await flushPromises();
+ await openSecurityTab(wrapper);
+
+ const link = wrapper.get('[data-testid="github-oauth-apps-guide-link"]');
+ expect(link.text()).toContain("OAuth Apps");
+ expect(link.attributes("href")).toBe("https://github.com/settings/developers");
+ expect(link.attributes("target")).toBe("_blank");
+ expect(link.attributes("rel")).toContain("noopener");
+ });
+
it("saves WeChat Connect fields using the backend contract and clears the secret after save", async () => {
const wrapper = mountView();
diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue
index 78ba4b9d..7d3846ef 100644
--- a/frontend/src/views/auth/LoginView.vue
+++ b/frontend/src/views/auth/LoginView.vue
@@ -10,33 +10,6 @@
{{ t('auth.signInToAccount') }}
-
-
-
-
-
-
-
-
- {{ t('auth.oauthOrContinue') }}
-
-
-
-
-
@@ -180,6 +187,7 @@ import { AuthLayout } from '@/components/layout'
import LinuxDoOAuthSection from '@/components/auth/LinuxDoOAuthSection.vue'
import OidcOAuthSection from '@/components/auth/OidcOAuthSection.vue'
import WechatOAuthSection from '@/components/auth/WechatOAuthSection.vue'
+import EmailOAuthButtons from '@/components/auth/EmailOAuthButtons.vue'
import TotpLoginModal from '@/components/auth/TotpLoginModal.vue'
import Icon from '@/components/icons/Icon.vue'
import TurnstileWidget from '@/components/TurnstileWidget.vue'
@@ -210,6 +218,8 @@ const wechatOAuthEnabled = ref