Parcourir la source

纸飞机一键登录,zalo登录

urbanu il y a 1 mois
Parent
commit
9f50183ef3
5 fichiers modifiés avec 279 ajouts et 32 suppressions
  1. 2 1
      src/api/auth.ts
  2. 6 1
      src/locales/en.json
  3. 6 1
      src/locales/vi.json
  4. 6 1
      src/locales/zh.json
  5. 259 28
      src/views/login/index.vue

+ 2 - 1
src/api/auth.ts

@@ -105,6 +105,7 @@ interface OAuthLoginParams {
   avatar?: string;
   email?: string;
   inviteCode?: string;
+  extra?: string; // Zalo: code_verifier
 }
 
 // OAuth登录
@@ -117,7 +118,7 @@ export function requestOAuthLogin(data: OAuthLoginParams): Promise<CommonRespons
 }
 
 // 获取OAuth配置
-export function requestOAuthConfig(): Promise<CommonResponse<{ googleClientId?: string; zaloAppId?: string }>> {
+export function requestOAuthConfig(): Promise<CommonResponse<{ googleClientId?: string; zaloAppId?: string; telegramBotName?: string }>> {
   return http.request({
     url: "/api/v1/dt/config/oauth",
     method: "get"

+ 6 - 1
src/locales/en.json

@@ -62,7 +62,12 @@
     "privacyPolicy": "Privacy Policy",
     "or": "or",
     "googleLogin": "Sign in with Google",
-    "zaloLogin": "Sign in with Zalo"
+    "zaloLogin": "Sign in with Zalo",
+    "telegramLogin": "Sign in with Telegram",
+    "telegramLoginFailed": "Telegram login failed",
+    "telegramNotConfigured": "Telegram login not configured",
+    "zaloLoginFailed": "Zalo login failed",
+    "zaloNotConfigured": "Zalo login not configured"
   },
   "home": {
     "checkIn": "Check in",

+ 6 - 1
src/locales/vi.json

@@ -62,7 +62,12 @@
     "privacyPolicy": "Chính sách bảo mật",
     "or": "hoặc",
     "googleLogin": "Đăng nhập bằng Google",
-    "zaloLogin": "Đăng nhập bằng Zalo"
+    "zaloLogin": "Đăng nhập bằng Zalo",
+    "telegramLogin": "Đăng nhập bằng Telegram",
+    "telegramLoginFailed": "Đăng nhập Telegram thất bại",
+    "telegramNotConfigured": "Đăng nhập Telegram chưa được cấu hình",
+    "zaloLoginFailed": "Đăng nhập Zalo thất bại",
+    "zaloNotConfigured": "Đăng nhập Zalo chưa được cấu hình"
   },
   "home": {
     "checkIn": "Điểm danh",

+ 6 - 1
src/locales/zh.json

@@ -62,7 +62,12 @@
     "privacyPolicy": "隐私政策",
     "or": "或",
     "googleLogin": "Google 登录",
-    "zaloLogin": "Zalo 登录"
+    "zaloLogin": "Zalo 登录",
+    "telegramLogin": "Telegram 登录",
+    "telegramLoginFailed": "Telegram 登录失败",
+    "telegramNotConfigured": "Telegram 登录未配置",
+    "zaloLoginFailed": "Zalo 登录失败",
+    "zaloNotConfigured": "Zalo 登录未配置"
   },
   "home": {
     "checkIn": "签到",

+ 259 - 28
src/views/login/index.vue

@@ -140,6 +140,9 @@
           <van-loading v-if="googleLoading" size="16px" color="#fff" />
         </div>
 
+        <!-- Telegram 官方 Widget 按钮 -->
+        <div id="telegram-login-container" class="telegram-widget-wrapper" v-if="telegramBotName"></div>
+
         <!-- Zalo 登录按钮 -->
         <div class="oauth-btn zalo-btn" @click="handleZaloLogin">
           <svg class="zalo-icon" viewBox="0 0 24 24" width="20" height="20">
@@ -169,7 +172,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, nextTick } from 'vue';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { toast } from 'vue-sonner';
@@ -181,6 +184,10 @@ const router = useRouter();
 const userStore = useUserStore();
 const googleLoading = ref(false);
 const googleClientId = ref('');
+const telegramLoading = ref(false);
+const telegramBotName = ref('');
+const zaloLoading = ref(false);
+const zaloAppId = ref('');
 
 const loginType = ref<'phone' | 'email'>('phone');
 const showPassword = ref(false);
@@ -267,36 +274,146 @@ const handleEmailLogin = async () => {
   }
 };
 
-// 获取OAuth配置并初始化 Google Sign-In
-const initGoogleSignIn = async () => {
+// 获取OAuth配置并初始化
+const initOAuth = async () => {
   try {
     // 从后端获取OAuth配置
     const configRes = await requestOAuthConfig();
-    if (configRes.code === 200 && configRes.data.googleClientId) {
-      googleClientId.value = configRes.data.googleClientId;
-
-      // 动态加载 Google Identity Services SDK
-      const script = document.createElement('script');
-      script.src = 'https://accounts.google.com/gsi/client';
-      script.async = true;
-      script.defer = true;
-      script.onload = () => {
-        if (window.google && googleClientId.value) {
-          window.google.accounts.id.initialize({
-            client_id: googleClientId.value,
-            callback: handleGoogleCallback,
-            auto_select: false,
-            cancel_on_tap_outside: true
-          });
-        }
-      };
-      document.head.appendChild(script);
+    if (configRes.code === 200) {
+      // 初始化 Google Sign-In
+      if (configRes.data.googleClientId) {
+        googleClientId.value = configRes.data.googleClientId;
+        const script = document.createElement('script');
+        script.src = 'https://accounts.google.com/gsi/client';
+        script.async = true;
+        script.defer = true;
+        script.onload = () => {
+          if (window.google && googleClientId.value) {
+            window.google.accounts.id.initialize({
+              client_id: googleClientId.value,
+              callback: handleGoogleCallback,
+              auto_select: false,
+              cancel_on_tap_outside: true
+            });
+          }
+        };
+        document.head.appendChild(script);
+      }
+
+      // 初始化 Telegram Login
+      if (configRes.data.telegramBotName) {
+        telegramBotName.value = configRes.data.telegramBotName;
+        // 等待 DOM 更新后再初始化
+        nextTick(() => {
+          initTelegramLogin();
+        });
+      }
+
+      // 初始化 Zalo Login
+      if (configRes.data.zaloAppId) {
+        zaloAppId.value = configRes.data.zaloAppId;
+        initZaloLogin();
+      }
     }
   } catch (error) {
     console.warn('Failed to load OAuth config:', error);
   }
 };
 
+// 初始化 Telegram 登录
+const initTelegramLogin = () => {
+  console.log('initTelegramLogin called, botName:', telegramBotName.value);
+
+  // 设置全局回调函数
+  (window as any).onTelegramAuth = (user: any) => {
+    console.log('Telegram auth callback:', user);
+    handleTelegramCallback(user);
+  };
+
+  // 动态创建 Telegram Widget 脚本
+  const container = document.getElementById('telegram-login-container');
+  console.log('Container found:', container);
+
+  if (container) {
+    container.innerHTML = '';
+    const script = document.createElement('script');
+    script.async = true;
+    script.src = 'https://telegram.org/js/telegram-widget.js?22';
+    script.setAttribute('data-telegram-login', telegramBotName.value);
+    script.setAttribute('data-size', 'large');
+    script.setAttribute('data-radius', '8');
+    script.setAttribute('data-onauth', 'onTelegramAuth(user)');
+    script.setAttribute('data-request-access', 'write');
+    script.onload = () => console.log('Telegram widget script loaded');
+    script.onerror = (e) => console.error('Telegram widget script error:', e);
+    container.appendChild(script);
+    console.log('Script appended to container');
+  }
+};
+
+// Telegram 登录回调
+const handleTelegramCallback = async (user: any) => {
+  if (!user || !user.id) {
+    toast.error(t('auth.telegramLoginFailed') || 'Telegram 登录失败');
+    return;
+  }
+
+  telegramLoading.value = true;
+  try {
+    const res = await requestOAuthLogin({
+      provider: 'telegram',
+      openId: String(user.id),
+      nickname: user.first_name + (user.last_name ? ' ' + user.last_name : ''),
+      avatar: user.photo_url || ''
+    });
+
+    if (res.code === 200) {
+      localStorage.setItem('token', res.data.token);
+      userStore.setUserInfo(res.data.user);
+      toast.success(t('auth.loginSuccess'));
+      router.replace('/home');
+    }
+  } catch (error) {
+    console.error('Telegram login error:', error);
+    toast.error(t('auth.telegramLoginFailed') || 'Telegram 登录失败');
+  } finally {
+    telegramLoading.value = false;
+  }
+};
+
+// 手动触发 Telegram 登录 - 点击隐藏的 Widget 按钮
+const handleTelegramLogin = () => {
+  if (!telegramBotName.value) {
+    toast.error(t('auth.telegramNotConfigured') || 'Telegram 登录未配置');
+    return;
+  }
+
+  // 查找并点击 Telegram Widget 生成的按钮
+  const container = document.getElementById('telegram-login-container');
+  const widgetBtn = container?.querySelector('iframe');
+
+  if (widgetBtn) {
+    // Widget 已加载,模拟点击
+    widgetBtn.style.position = 'fixed';
+    widgetBtn.style.top = '50%';
+    widgetBtn.style.left = '50%';
+    widgetBtn.style.zIndex = '9999';
+    widgetBtn.click();
+
+    // 稍后隐藏回去
+    setTimeout(() => {
+      widgetBtn.style.position = '';
+      widgetBtn.style.top = '';
+      widgetBtn.style.left = '';
+      widgetBtn.style.zIndex = '';
+    }, 100);
+  } else {
+    // Widget 未加载,重新初始化
+    initTelegramLogin();
+    toast.info('请稍后再试');
+  }
+};
+
 // Google 登录回调处理
 const handleGoogleCallback = async (response: any) => {
   if (!response.credential) {
@@ -359,14 +476,106 @@ const handleGoogleLogin = () => {
   }
 };
 
-// 组件挂载时初始化 Google Sign-In
+// 组件挂载时初始化 OAuth
 onMounted(() => {
-  initGoogleSignIn();
+  initOAuth();
 });
 
-const handleZaloLogin = () => {
-  // TODO: Zalo OAuth登录
-  toast.info('Zalo Login - Coming Soon');
+// 生成随机字符串
+const generateRandomString = (length: number): string => {
+  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+  let result = '';
+  for (let i = 0; i < length; i++) {
+    result += chars.charAt(Math.floor(Math.random() * chars.length));
+  }
+  return result;
+};
+
+// 生成 PKCE code verifier 和 code challenge
+const generatePKCE = async () => {
+  const codeVerifier = generateRandomString(43);
+  const encoder = new TextEncoder();
+  const data = encoder.encode(codeVerifier);
+  const digest = await crypto.subtle.digest('SHA-256', data);
+  const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)))
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_')
+    .replace(/=+$/, '');
+  return { codeVerifier, codeChallenge: base64 };
+};
+
+// 初始化 Zalo 登录
+const initZaloLogin = () => {
+  // 检查 URL 是否有 Zalo 回调参数
+  const urlParams = new URLSearchParams(window.location.search);
+  const code = urlParams.get('code');
+  const codeVerifier = sessionStorage.getItem('zalo_code_verifier');
+
+  if (code && codeVerifier) {
+    // 清除 URL 参数
+    window.history.replaceState({}, document.title, window.location.pathname + window.location.hash);
+    sessionStorage.removeItem('zalo_code_verifier');
+    // 使用 code 获取用户信息
+    handleZaloCallback(code, codeVerifier);
+  }
+};
+
+// Zalo 登录回调处理
+const handleZaloCallback = async (code: string, codeVerifier: string) => {
+  zaloLoading.value = true;
+  try {
+    // 调用后端接口,让后端用 code 换取 access_token 和用户信息
+    const res = await requestOAuthLogin({
+      provider: 'zalo',
+      openId: code, // 先传 code,后端会处理
+      nickname: '',
+      avatar: '',
+      extra: codeVerifier // 传递 code_verifier 给后端
+    });
+
+    if (res.code === 200) {
+      localStorage.setItem('token', res.data.token);
+      userStore.setUserInfo(res.data.user);
+      toast.success(t('auth.loginSuccess'));
+      router.replace('/home');
+    } else {
+      toast.error(res.msg || t('auth.zaloLoginFailed') || 'Zalo 登录失败');
+    }
+  } catch (error) {
+    console.error('Zalo login error:', error);
+    toast.error(t('auth.zaloLoginFailed') || 'Zalo 登录失败');
+  } finally {
+    zaloLoading.value = false;
+  }
+};
+
+// 触发 Zalo 登录
+const handleZaloLogin = async () => {
+  if (!zaloAppId.value) {
+    toast.error(t('auth.zaloNotConfigured') || 'Zalo 登录未配置');
+    return;
+  }
+
+  zaloLoading.value = true;
+  try {
+    // 生成 PKCE
+    const { codeVerifier, codeChallenge } = await generatePKCE();
+    // 保存 code_verifier 到 sessionStorage
+    sessionStorage.setItem('zalo_code_verifier', codeVerifier);
+
+    // 构建 Zalo OAuth URL
+    const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
+    const state = generateRandomString(16);
+
+    const authUrl = `https://oauth.zaloapp.com/v4/permission?app_id=${zaloAppId.value}&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&state=${state}`;
+
+    // 跳转到 Zalo 授权页面
+    window.location.href = authUrl;
+  } catch (error) {
+    console.error('Zalo login error:', error);
+    toast.error('Zalo 登录失败');
+    zaloLoading.value = false;
+  }
 };
 
 const goForgotPassword = () => {
@@ -542,7 +751,7 @@ const goRegister = () => {
       opacity: 0.7;
     }
 
-    .google-icon, .zalo-icon {
+    .google-icon, .zalo-icon, .telegram-icon {
       flex-shrink: 0;
     }
 
@@ -564,6 +773,15 @@ const goRegister = () => {
         background: #0055cc;
       }
     }
+
+    &.telegram-btn {
+      background: #0088cc;
+      color: #fff;
+
+      &:active {
+        background: #0077b5;
+      }
+    }
   }
 }
 
@@ -579,4 +797,17 @@ const goRegister = () => {
     cursor: pointer;
   }
 }
+
+.telegram-widget-wrapper {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 12px;
+  min-height: 48px;
+
+  iframe {
+    border-radius: 8px !important;
+    width: 100% !important;
+    max-width: 100% !important;
+  }
+}
 </style>