crush-level-web/src/lib/oauth/google.ts

228 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Google Identity Services (GIS) 配置
// const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!
const GOOGLE_CLIENT_ID = "606396962663-9pagar3g9vuhovi37vq9jqob6q1gngns.apps.googleusercontent.com"
// Google OAuth scopes
const GOOGLE_SCOPES = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile'
].join(' ')
export interface GoogleUser {
id: string
email: string
verified_email: boolean
name: string
given_name: string
family_name: string
picture?: string
locale?: string
}
// Google Identity Services 响应类型
export interface GoogleCredentialResponse {
credential: string // JWT ID token
select_by?: string
clientId?: string
}
// Google OAuth Code Response (使用 Code Model)
export interface GoogleCodeResponse {
code: string // Authorization code
scope: string
authuser?: string
prompt?: string
}
// 声明 Google Identity Services 全局对象
declare global {
interface Window {
google?: {
accounts: {
id: {
initialize: (config: GoogleIdConfiguration) => void
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void
disableAutoSelect: () => void
cancel: () => void
}
oauth2: {
initCodeClient: (config: CodeClientConfig) => CodeClient
initTokenClient: (config: TokenClientConfig) => TokenClient
}
}
}
}
}
interface GoogleIdConfiguration {
client_id: string
callback?: (response: GoogleCredentialResponse) => void
auto_select?: boolean
cancel_on_tap_outside?: boolean
context?: 'signin' | 'signup' | 'use'
ux_mode?: 'popup' | 'redirect'
login_uri?: string
native_callback?: (response: GoogleCredentialResponse) => void
itp_support?: boolean
}
interface GsiButtonConfiguration {
type?: 'standard' | 'icon'
theme?: 'outline' | 'filled_blue' | 'filled_black'
size?: 'large' | 'medium' | 'small'
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin'
shape?: 'rectangular' | 'pill' | 'circle' | 'square'
logo_alignment?: 'left' | 'center'
width?: string
locale?: string
}
interface PromptMomentNotification {
isDisplayMoment: () => boolean
isDisplayed: () => boolean
isNotDisplayed: () => boolean
getNotDisplayedReason: () => string
isSkippedMoment: () => boolean
getSkippedReason: () => string
isDismissedMoment: () => boolean
getDismissedReason: () => string
getMomentType: () => string
}
interface CodeClientConfig {
client_id: string
scope: string
callback: (response: GoogleCodeResponse) => void
error_callback?: (error: { type: string; message: string }) => void
ux_mode?: 'popup' | 'redirect'
redirect_uri?: string
state?: string
}
interface CodeClient {
requestCode: () => void
}
interface TokenClientConfig {
client_id: string
scope: string
callback: (response: { access_token: string; expires_in: number; scope: string; token_type: string }) => void
error_callback?: (error: { type: string; message: string }) => void
}
interface TokenClient {
requestAccessToken: () => void
}
export const googleOAuth = {
clientId: GOOGLE_CLIENT_ID,
scopes: GOOGLE_SCOPES,
// 加载 Google Identity Services SDK
loadScript: (): Promise<void> => {
return new Promise((resolve, reject) => {
// 检查是否已加载
if (window.google?.accounts) {
resolve()
return
}
// 创建 script 标签
const script = document.createElement('script')
script.src = 'https://accounts.google.com/gsi/client'
script.async = true
script.defer = true
script.onload = () => resolve()
script.onerror = () => reject(new Error('Failed to load Google Identity Services SDK'))
document.head.appendChild(script)
})
},
// 初始化 Code Client推荐方式获取授权码
initCodeClient: (callback: (response: GoogleCodeResponse) => void, errorCallback?: (error: any) => void) => {
if (!window.google?.accounts?.oauth2) {
throw new Error('Google Identity Services SDK not loaded')
}
return window.google.accounts.oauth2.initCodeClient({
client_id: GOOGLE_CLIENT_ID,
scope: GOOGLE_SCOPES,
ux_mode: 'popup', // 使用 popup 模式,不需要 redirect_uri
callback,
error_callback: errorCallback
})
},
// 初始化 Token Client直接获取 access token
initTokenClient: (callback: (response: { access_token: string; expires_in: number; scope: string; token_type: string }) => void, errorCallback?: (error: any) => void) => {
if (!window.google?.accounts?.oauth2) {
throw new Error('Google Identity Services SDK not loaded')
}
return window.google.accounts.oauth2.initTokenClient({
client_id: GOOGLE_CLIENT_ID,
scope: GOOGLE_SCOPES,
callback,
error_callback: errorCallback
})
},
// 初始化 Google Identity (获取 ID Token - JWT)
initGoogleId: (callback: (response: GoogleCredentialResponse) => void) => {
if (!window.google?.accounts?.id) {
throw new Error('Google Identity Services SDK not loaded')
}
window.google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
callback,
auto_select: false,
cancel_on_tap_outside: true,
ux_mode: 'popup'
})
},
// 触发 One Tap 流程
promptOneTap: (callback: (response: GoogleCredentialResponse) => void) => {
googleOAuth.initGoogleId(callback)
if (window.google?.accounts?.id) {
window.google.accounts.id.prompt((notification) => {
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
console.log('One Tap not displayed:', notification.getNotDisplayedReason() || notification.getSkippedReason())
}
})
}
},
// 使用 FedCM (Federated Credential Management) 方式获取 ID Token
// 这种方式会弹出标准的 Google 登录窗口,无需用户预先登录
renderButton: (parent: HTMLElement, callback: (response: GoogleCredentialResponse) => void, options?: Partial<GsiButtonConfiguration>) => {
if (!window.google?.accounts?.id) {
throw new Error('Google Identity Services SDK not loaded')
}
// 先初始化
window.google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
callback,
auto_select: false,
cancel_on_tap_outside: true
})
// 渲染 Google 标准按钮
window.google.accounts.id.renderButton(parent, {
type: 'standard',
theme: 'outline',
size: 'large',
text: 'continue_with',
shape: 'rectangular',
logo_alignment: 'left',
...options
})
}
}