import { Prisma } from "@fenghuo/db"; // apps/web/lib/oidc-client.ts interface OIDCTokenResponse { access_token: string; token_type: string; expires_in: number; refresh_token?: string; id_token?: string; scope: string; } interface OIDCUserInfo { sub: string; username: string; email: string; name: string; department?: string; roles?: string[]; [key: string]: any; } interface OIDCError { error: string; error_description?: string; } export class OIDCClient { private baseUrl: string; private clientId: string; private clientSecret?: string; constructor() { this.baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; // 这里配置oidc服务器的地址 this.clientId = process.env.NEXT_PUBLIC_OIDC_CLIENT_ID || 'web-app'; // 这里配置对应的id this.clientSecret = process.env.NEXT_PUBLIC_OIDC_CLIENT_SECRET; } /** * 使用用户名密码登录获取访问令牌 */ async loginWithPassword(username: string, password: string): Promise { const response = await fetch(`${this.baseUrl}/auth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'password', username, password, client_id: this.clientId, scope: 'openid profile email', }), }); if (!response.ok) { const error: OIDCError = await response.json(); throw new Error(error.error_description || error.error || '登录失败'); } return response.json(); } /** * 使用刷新令牌获取新的访问令牌 */ async refreshToken(refreshToken: string): Promise { const response = await fetch(`${this.baseUrl}/auth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': this.clientSecret ? `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}` : '', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.clientId, }), }); if (!response.ok) { const error: OIDCError = await response.json(); throw new Error(error.error_description || error.error || '刷新令牌失败'); } return response.json(); } /** * 获取用户信息 */ async getUserInfo(accessToken: string): Promise { const response = await fetch(`${this.baseUrl}/auth/userinfo`, { headers: { 'Authorization': `Bearer ${accessToken}`, }, }); if (!response.ok) { const error: OIDCError = await response.json(); throw new Error(error.error_description || error.error || '获取用户信息失败'); } return response.json(); } /** * 注销登录 */ async logout(accessToken?: string): Promise { if (accessToken) { await fetch(`${this.baseUrl}/auth/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${accessToken}`, }, body: new URLSearchParams({ token: accessToken, token_type_hint: 'access_token', }), }); } } /** * 注册新用户 */ async register(userData: Prisma.UserCreateArgs): Promise<{ success: boolean; message: string }> { const response = await fetch(`${this.baseUrl}/trpc/user.create`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ json: userData, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error?.message || '注册失败'); } const result = await response.json(); return { success: true, message: '注册成功', }; } /** * 构建授权 URL(用于跳转到登录页面) */ buildAuthorizationUrl(options: { redirectUri: string; state?: string; scope?: string; nonce?: string; }): string { const { redirectUri, state = this.generateState(), scope = 'openid profile email', nonce = this.generateNonce() } = options; const params = new URLSearchParams({ response_type: 'code', client_id: this.clientId, redirect_uri: redirectUri, scope, state, nonce, }); return `${this.baseUrl}/auth/auth?${params.toString()}`; } /** * 处理授权回调(交换授权码获取令牌) */ async handleAuthorizationCallback( authorizationCode: string, redirectUri: string, state?: string ): Promise { const response = await fetch(`${this.baseUrl}/auth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, redirect_uri: redirectUri, client_id: this.clientId, ...(this.clientSecret && { client_secret: this.clientSecret }), }), }); if (!response.ok) { const error: OIDCError = await response.json(); throw new Error(error.error_description || error.error || '授权码交换失败'); } return response.json(); } /** * 生成随机状态值(防CSRF) */ private generateState(): string { return btoa(Math.random().toString(36).substr(2, 9)); } /** * 生成随机 nonce 值 */ private generateNonce(): string { return btoa(Math.random().toString(36).substr(2, 9)); } } export const oidcClient = new OIDCClient();