feat(test): 插件测试

This commit is contained in:
opensnail 2025-04-26 18:00:04 +08:00
parent 186d8c109b
commit ad343a61aa
8 changed files with 379 additions and 0 deletions

View 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"
}
}

View 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>

View 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()
};
}

View 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 };

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

View 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;
}

View 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
}
}

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