feat(test): 插件测试
This commit is contained in:
parent
186d8c109b
commit
ad343a61aa
35
packages/sso-plugin/package.json
Normal file
35
packages/sso-plugin/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
63
packages/sso-plugin/src/components/SSOLoginButton.vue
Normal file
63
packages/sso-plugin/src/components/SSOLoginButton.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { useSSO } from '../hooks/useSSO';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SSOLoginButton',
|
||||
setup() {
|
||||
const { login } = useSSO();
|
||||
const loading = ref(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await login();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
handleLogin
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="sso-login-button" :class="{ 'is-loading': loading }" :disabled="loading" @click="handleLogin">
|
||||
<slot>
|
||||
<span v-if="!loading">SSO登录</span>
|
||||
<span v-else>登录中...</span>
|
||||
</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sso-login-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sso-login-button:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.sso-login-button:disabled {
|
||||
background-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sso-login-button.is-loading {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
27
packages/sso-plugin/src/hooks/useSSO.ts
Normal file
27
packages/sso-plugin/src/hooks/useSSO.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { inject } from 'vue';
|
||||
import type { SSOProvider } from '../services/SSOProvider';
|
||||
|
||||
export function useSSO() {
|
||||
const sso = inject<SSOProvider>('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()
|
||||
};
|
||||
}
|
34
packages/sso-plugin/src/index.ts
Normal file
34
packages/sso-plugin/src/index.ts
Normal file
@ -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 };
|
127
packages/sso-plugin/src/services/SSOProvider.ts
Normal file
127
packages/sso-plugin/src/services/SSOProvider.ts
Normal file
@ -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<void> {
|
||||
// 检查本地存储的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<void> {
|
||||
const authUrl = this.buildAuthUrl();
|
||||
window.location.href = authUrl;
|
||||
}
|
||||
|
||||
// 处理回调
|
||||
async handleCallback(code: string): Promise<void> {
|
||||
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<void> {
|
||||
this.clearSession();
|
||||
const logoutUrl = `${this.config.serverUrl}/logout`;
|
||||
window.location.href = logoutUrl;
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
async getUserInfo(): Promise<SSOUser | null> {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
isAuthenticated(): boolean {
|
||||
return Boolean(this.token) && Boolean(this.user);
|
||||
}
|
||||
|
||||
// 获取访问令牌
|
||||
getAccessToken(): string | null {
|
||||
return this.token?.accessToken || null;
|
||||
}
|
||||
|
||||
private async fetchUserInfo(): Promise<void> {
|
||||
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<SSOToken> {
|
||||
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');
|
||||
}
|
||||
}
|
36
packages/sso-plugin/src/types/index.ts
Normal file
36
packages/sso-plugin/src/types/index.ts
Normal file
@ -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;
|
||||
}
|
30
packages/sso-plugin/tsconfig.json
Normal file
30
packages/sso-plugin/tsconfig.json
Normal file
@ -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
|
||||
}
|
||||
}
|
27
packages/sso-plugin/vite.config.ts
Normal file
27
packages/sso-plugin/vite.config.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user