Skip to content

useAuth

完整的 Telegram 认证流程封装,支持前端验证和后端认证。

导入

tsx
import { useAuth } from '@xcloud/ui-telegram'

示例

基础使用

自动认证模式:

tsx
import { useAuth } from '@xcloud/ui-telegram'

function App() {
  const { user, isAuthenticated, isLoading } = useAuth({
    endpoint: '/api/auth/telegram',
    autoAuth: true,
  })

  if (isLoading) return <div>Authenticating...</div>
  if (!isAuthenticated) return <div>Not authenticated</div>

  return <div>Welcome, {user?.firstName}!</div>
}

手动认证

用户主动触发登录:

tsx
import { useAuth } from '@xcloud/ui-telegram'

function LoginPage() {
  const { authenticate, isLoading, error } = useAuth()

  const handleLogin = async () => {
    try {
      await authenticate('/api/auth/telegram')
    } catch (err) {
      console.error('Login failed:', err)
    }
  }

  return (
    <div>
      <button onClick={handleLogin} disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login with Telegram'}
      </button>
      {error && <p className="error">{error.message}</p>}
    </div>
  )
}

使用回调

处理认证成功和失败:

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { useNavigate } from 'react-router-dom'

function App() {
  const navigate = useNavigate()

  const { user, token } = useAuth({
    endpoint: '/api/auth/telegram',
    autoAuth: true,
    onSuccess: (user, token) => {
      console.log('Logged in:', user.firstName)

      // 保存 token
      if (token) {
        localStorage.setItem('auth_token', token)
      }

      // 跳转到首页
      navigate('/home')
    },
    onError: (error) => {
      console.error('Auth failed:', error)
      // 显示错误提示
      toast.error('Authentication failed')
    },
  })

  return <div>{/* Your app */}</div>
}

仅前端验证

不使用后端,仅使用 Telegram 提供的数据:

tsx
import { useAuth } from '@xcloud/ui-telegram'

function App() {
  // 不传 endpoint,仅使用 Telegram 提供的用户数据
  const { user, isAuthenticated } = useAuth({ autoAuth: true })

  // user 数据来自 Telegram,已经过 Telegram 签名验证
  return isAuthenticated ? (
    <div>Hello, {user?.firstName}</div>
  ) : (
    <div>Not in Telegram</div>
  )
}

登出功能

tsx
import { useAuth } from '@xcloud/ui-telegram'

function Header() {
  const { user, isAuthenticated, logout } = useAuth({
    endpoint: '/api/auth/telegram',
    autoAuth: true,
  })

  const handleLogout = () => {
    logout()
    // 清除本地存储的 token
    localStorage.removeItem('auth_token')
    // 重定向到登录页
    window.location.href = '/login'
  }

  if (!isAuthenticated) return null

  return (
    <header>
      <span>Welcome, {user?.firstName}</span>
      <button onClick={handleLogout}>Logout</button>
    </header>
  )
}

Hook API

参数 (AuthOptions)

typescript
interface AuthOptions {
  // 后端认证接口地址
  endpoint?: string

  // 自动认证(组件挂载时自动调用认证)
  autoAuth?: boolean  // default: false

  // 认证成功回调
  onSuccess?: (user: User, token?: string) => void

  // 认证失败回调
  onError?: (error: Error) => void

  // 自定义请求头
  headers?: Record<string, string>
}

返回值 (AuthState & Methods)

typescript
{
  // 状态
  user: User | null                    // 当前认证的用户
  isAuthenticated: boolean             // 是否已认证
  isLoading: boolean                   // 是否正在认证
  error: Error | null                  // 认证错误
  token: string | null                 // 后端返回的 token

  // 方法
  authenticate: (endpoint?: string) => Promise<void>  // 手动认证
  logout: () => void                   // 登出

  // 额外信息
  initData: string | undefined         // 原始 initData
  startParam: string | undefined       // 启动参数
}

认证流程

1. 前端收集数据

typescript
// Hook 自动从 Telegram 获取
const initData = retrieveLaunchParams().initDataRaw
const platform = retrieveLaunchParams().platform

2. 发送到后端

typescript
fetch('/api/auth/telegram', {
  method: 'POST',
  headers: {
    'Authorization': `tma ${initDataRaw}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    initData: initDataRaw,
    platform: platform,
  }),
})

3. 后端验证

后端需要验证签名和有效期:

typescript
// Node.js 示例
import crypto from 'crypto'

function verifyTelegramAuth(initDataRaw: string, botToken: string) {
  const parsed = new URLSearchParams(initDataRaw)
  const hash = parsed.get('hash')
  if (!hash) return false

  parsed.delete('hash')

  // 创建验证字符串
  const dataCheckString = Array.from(parsed.entries())
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k, v]) => `${k}=${v}`)
    .join('\n')

  // 生成密钥
  const secretKey = crypto
    .createHmac('sha256', 'WebAppData')
    .update(botToken)
    .digest()

  // 计算哈希
  const computedHash = crypto
    .createHmac('sha256', secretKey)
    .update(dataCheckString)
    .digest('hex')

  return computedHash === hash
}

// 验证有效期 (建议 1 小时)
const authDate = parseInt(parsed.get('auth_date') || '0')
const now = Math.floor(Date.now() / 1000)
if (now - authDate > 3600) {
  throw new Error('Init data expired')
}

4. 返回 Token

typescript
// 后端返回格式
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs...",  // JWT token
  "user": {
    "id": 123456,
    "firstName": "John",
    "username": "johndoe"
  }
}

常见场景

受保护的路由

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { Navigate } from 'react-router-dom'

function ProtectedRoute({ children }) {
  const { isAuthenticated, isLoading } = useAuth({
    endpoint: '/api/auth',
    autoAuth: true,
  })

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />
  }

  return children
}

// 使用
<Route path="/dashboard" element={
  <ProtectedRoute>
    <Dashboard />
  </ProtectedRoute>
} />

API 请求携带 Token

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { useEffect } from 'react'

function useApiClient() {
  const { token } = useAuth()

  return {
    fetch: (url: string, options = {}) => {
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${token}`,
        },
      })
    },
  }
}

// 使用
function DataComponent() {
  const api = useApiClient()
  const [data, setData] = useState(null)

  useEffect(() => {
    api.fetch('/api/user/profile')
      .then(res => res.json())
      .then(setData)
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Token 刷新

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { useEffect } from 'react'

function TokenRefresher() {
  const { token, authenticate } = useAuth()

  useEffect(() => {
    if (!token) return

    // 每 50 分钟刷新一次 token
    const interval = setInterval(() => {
      authenticate('/api/auth/refresh')
    }, 50 * 60 * 1000)

    return () => clearInterval(interval)
  }, [token, authenticate])

  return null
}

多端点支持

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { useState } from 'react'

function MultiEndpointAuth() {
  const [activeEndpoint, setActiveEndpoint] = useState('production')

  const endpoints = {
    production: 'https://api.example.com/auth',
    staging: 'https://staging-api.example.com/auth',
    development: 'http://localhost:3000/auth',
  }

  const { authenticate, isAuthenticated } = useAuth()

  const handleLogin = (env: string) => {
    setActiveEndpoint(env)
    authenticate(endpoints[env])
  }

  return (
    <div>
      <button onClick={() => handleLogin('production')}>Production</button>
      <button onClick={() => handleLogin('staging')}>Staging</button>
      <button onClick={() => handleLogin('development')}>Development</button>
    </div>
  )
}

错误处理

tsx
import { useAuth } from '@xcloud/ui-telegram'
import { useState } from 'react'

function LoginWithErrorHandling() {
  const { authenticate, error, isLoading } = useAuth()
  const [customError, setCustomError] = useState<string | null>(null)

  const handleLogin = async () => {
    setCustomError(null)

    try {
      await authenticate('/api/auth/telegram')
    } catch (err) {
      if (err instanceof Error) {
        if (err.message.includes('Network')) {
          setCustomError('Network error, please check your connection')
        } else if (err.message.includes('401')) {
          setCustomError('Invalid credentials')
        } else {
          setCustomError('An unexpected error occurred')
        }
      }
    }
  }

  return (
    <div>
      <button onClick={handleLogin} disabled={isLoading}>
        Login
      </button>
      {(error || customError) && (
        <div className="error">
          {customError || error?.message}
        </div>
      )}
    </div>
  )
}

注意事项

  1. 必须在 TelegramProvider 中使用: 确保应用被 TelegramProvider 包裹
  2. 后端验证必需: 永远不要仅信任客户端数据,必须在后端验证签名
  3. Token 安全: 不要将 token 存储在不安全的地方
  4. 有效期检查: 后端应该检查 auth_date,建议有效期为 1 小时
  5. HTTPS: 生产环境必须使用 HTTPS
  6. 错误处理: 始终处理认证失败的情况

最佳实践

完整的认证系统

tsx
// AuthProvider.tsx
import { useAuth } from '@xcloud/ui-telegram'
import { createContext, useContext } from 'react'

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const auth = useAuth({
    endpoint: '/api/auth/telegram',
    autoAuth: true,
    onSuccess: (user, token) => {
      // 保存到 localStorage
      if (token) {
        localStorage.setItem('token', token)
      }

      // 发送到分析
      analytics.identify(user.id, {
        name: user.firstName,
        username: user.username,
      })
    },
    onError: (error) => {
      // 日志记录
      console.error('Auth error:', error)

      // 清除旧数据
      localStorage.removeItem('token')
    },
  })

  return (
    <AuthContext.Provider value={auth}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuthContext = () => useContext(AuthContext)

// App.tsx
import { AuthProvider } from './AuthProvider'

function App() {
  return (
    <TelegramProvider>
      <AuthProvider>
        <Routes />
      </AuthProvider>
    </TelegramProvider>
  )
}

相关链接

基于 MIT 许可发布