From ad343a61aa6436026b54dfa9af186b2091e3d8fa Mon Sep 17 00:00:00 2001 From: opensnail <598092184@qq.com> Date: Sat, 26 Apr 2025 18:00:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(test):=20=E6=8F=92=E4=BB=B6=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sso-plugin/package.json | 35 +++++ .../src/components/SSOLoginButton.vue | 63 +++++++++ packages/sso-plugin/src/hooks/useSSO.ts | 27 ++++ packages/sso-plugin/src/index.ts | 34 +++++ .../sso-plugin/src/services/SSOProvider.ts | 127 ++++++++++++++++++ packages/sso-plugin/src/types/index.ts | 36 +++++ packages/sso-plugin/tsconfig.json | 30 +++++ packages/sso-plugin/vite.config.ts | 27 ++++ 8 files changed, 379 insertions(+) create mode 100644 packages/sso-plugin/package.json create mode 100644 packages/sso-plugin/src/components/SSOLoginButton.vue create mode 100644 packages/sso-plugin/src/hooks/useSSO.ts create mode 100644 packages/sso-plugin/src/index.ts create mode 100644 packages/sso-plugin/src/services/SSOProvider.ts create mode 100644 packages/sso-plugin/src/types/index.ts create mode 100644 packages/sso-plugin/tsconfig.json create mode 100644 packages/sso-plugin/vite.config.ts diff --git a/packages/sso-plugin/package.json b/packages/sso-plugin/package.json new file mode 100644 index 0000000..8b28476 --- /dev/null +++ b/packages/sso-plugin/package.json @@ -0,0 +1,35 @@ +{ + "name": "@your-org/sso-plugin", + "version": "1.0.0", + "description": "Vue SSO authentication plugin", + "author": "Your Name", + "license": "MIT", + "keywords": ["vue", "sso", "authentication", "plugin"], + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "vue-tsc && vite build", + "dev": "vite", + "format": "prettier --write src/", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "preview": "vite preview" + }, + "peerDependencies": { + "vue": "^3.0.0" + }, + "dependencies": { + "@vueuse/core": "^10.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vitejs/plugin-vue": "^4.0.0", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.0.0", + "prettier": "^3.0.0", + "typescript": "^5.0.0", + "vite": "^4.0.0", + "vue-tsc": "^1.0.0" + } +} diff --git a/packages/sso-plugin/src/components/SSOLoginButton.vue b/packages/sso-plugin/src/components/SSOLoginButton.vue new file mode 100644 index 0000000..1cc961e --- /dev/null +++ b/packages/sso-plugin/src/components/SSOLoginButton.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/packages/sso-plugin/src/hooks/useSSO.ts b/packages/sso-plugin/src/hooks/useSSO.ts new file mode 100644 index 0000000..5cbea73 --- /dev/null +++ b/packages/sso-plugin/src/hooks/useSSO.ts @@ -0,0 +1,27 @@ +import { inject } from 'vue'; +import type { SSOProvider } from '../services/SSOProvider'; + +export function useSSO() { + const sso = inject('sso'); + + if (!sso) { + throw new Error('SSO provider not found. Make sure the SSO plugin is installed.'); + } + + return { + // 登录 + login: () => sso.login(), + + // 登出 + logout: () => sso.logout(), + + // 获取用户信息 + getUserInfo: () => sso.getUserInfo(), + + // 检查是否已登录 + isAuthenticated: () => sso.isAuthenticated(), + + // 获取访问令牌 + getAccessToken: () => sso.getAccessToken() + }; +} diff --git a/packages/sso-plugin/src/index.ts b/packages/sso-plugin/src/index.ts new file mode 100644 index 0000000..2342400 --- /dev/null +++ b/packages/sso-plugin/src/index.ts @@ -0,0 +1,34 @@ +import type { App } from 'vue'; +import type { SSOConfig, SSOUser } from './types'; +import { SSOProvider } from './services/SSOProvider'; +import { useSSO } from './hooks/useSSO'; + +export interface SSOPluginOptions { + config: SSOConfig; + onLogin?: (user: SSOUser) => void; + onLogout?: () => void; +} + +export const SSOPlugin = { + install: (app: App, options: SSOPluginOptions) => { + const ssoProvider = new SSOProvider(options.config); + + // 注册全局属性 + app.config.globalProperties.$sso = ssoProvider; + + // 注册全局组件 + app.component('SSOLoginButton', () => import('./components/SSOLoginButton.vue')); + + // 提供全局状态 + app.provide('sso', ssoProvider); + } +}; + +// 导出类型 +export * from './types'; + +// 导出hooks +export { useSSO }; + +// 导出服务 +export { SSOProvider }; diff --git a/packages/sso-plugin/src/services/SSOProvider.ts b/packages/sso-plugin/src/services/SSOProvider.ts new file mode 100644 index 0000000..e9b8602 --- /dev/null +++ b/packages/sso-plugin/src/services/SSOProvider.ts @@ -0,0 +1,127 @@ +import type { SSOConfig, SSOToken, SSOUser } from '../types'; + +export class SSOProvider { + private config: SSOConfig; + private token: SSOToken | null = null; + private user: SSOUser | null = null; + + constructor(config: SSOConfig) { + this.config = config; + } + + // 初始化SSO + async initialize(): Promise { + // 检查本地存储的token + const storedToken = localStorage.getItem('sso_token'); + if (storedToken) { + try { + this.token = JSON.parse(storedToken); + await this.fetchUserInfo(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + this.clearSession(); + } + } + } + + // 登录 + async login(): Promise { + const authUrl = this.buildAuthUrl(); + window.location.href = authUrl; + } + + // 处理回调 + async handleCallback(code: string): Promise { + try { + const token = await this.exchangeCodeForToken(code); + this.token = token; + localStorage.setItem('sso_token', JSON.stringify(token)); + await this.fetchUserInfo(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + throw new Error('Failed to handle SSO callback'); + } + } + + // 登出 + async logout(): Promise { + this.clearSession(); + const logoutUrl = `${this.config.serverUrl}/logout`; + window.location.href = logoutUrl; + } + + // 获取用户信息 + async getUserInfo(): Promise { + return this.user; + } + + // 检查是否已登录 + isAuthenticated(): boolean { + return Boolean(this.token) && Boolean(this.user); + } + + // 获取访问令牌 + getAccessToken(): string | null { + return this.token?.accessToken || null; + } + + private async fetchUserInfo(): Promise { + if (!this.token) return; + + try { + const response = await fetch(this.config.userInfoEndpoint || `${this.config.serverUrl}/userinfo`, { + headers: { + Authorization: `Bearer ${this.token.accessToken}` + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch user info'); + } + + this.user = await response.json(); + } catch (error) { + this.clearSession(); + throw error; + } + } + + private async exchangeCodeForToken(code: string): Promise { + const response = await fetch(this.config.tokenEndpoint || `${this.config.serverUrl}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + redirect_uri: this.config.redirectUri + }) + }); + + if (!response.ok) { + throw new Error('Failed to exchange code for token'); + } + + return response.json(); + } + + private buildAuthUrl(): string { + const params = new URLSearchParams({ + client_id: this.config.clientId, + redirect_uri: this.config.redirectUri, + response_type: this.config.responseType || 'code', + scope: (this.config.scope || ['openid', 'profile', 'email']).join(' ') + }); + + return `${this.config.serverUrl}/authorize?${params.toString()}`; + } + + private clearSession(): void { + this.token = null; + this.user = null; + localStorage.removeItem('sso_token'); + } +} diff --git a/packages/sso-plugin/src/types/index.ts b/packages/sso-plugin/src/types/index.ts new file mode 100644 index 0000000..b902c6f --- /dev/null +++ b/packages/sso-plugin/src/types/index.ts @@ -0,0 +1,36 @@ +export interface SSOConfig { + // SSO服务器配置 + serverUrl: string; + clientId: string; + clientSecret: string; + redirectUri: string; + + // 可选配置 + scope?: string[]; + responseType?: 'code' | 'token'; + tokenEndpoint?: string; + userInfoEndpoint?: string; +} + +export interface SSOUser { + id: string; + username: string; + email: string; + displayName: string; + avatar?: string; + roles?: string[]; + permissions?: string[]; +} + +export interface SSOToken { + accessToken: string; + refreshToken?: string; + expiresIn: number; + tokenType: string; +} + +export interface SSOError { + code: string; + message: string; + details?: any; +} diff --git a/packages/sso-plugin/tsconfig.json b/packages/sso-plugin/tsconfig.json new file mode 100644 index 0000000..aeba563 --- /dev/null +++ b/packages/sso-plugin/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "target": "ES2020", + "jsx": "preserve", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "useDefineForClassFields": true, + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"] + }, + "resolveJsonModule": true, + "types": ["node"], + "allowImportingTsExtensions": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "declaration": true, + "declarationDir": "./dist", + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true + } +} diff --git a/packages/sso-plugin/vite.config.ts b/packages/sso-plugin/vite.config.ts new file mode 100644 index 0000000..6ca948f --- /dev/null +++ b/packages/sso-plugin/vite.config.ts @@ -0,0 +1,27 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'SSOPlugin', + fileName: format => `index.${format === 'es' ? 'mjs' : 'js'}` + }, + rollupOptions: { + external: ['vue'], + output: { + globals: { + vue: 'Vue' + } + } + } + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + } +});