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')
+ }
+ }
+});