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