From 693a4a663385ced3176286bf4b5f3566da83d90e Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 5 Feb 2026 01:10:01 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20oauth=E6=94=AF=E6=8C=81github=20?= =?UTF-8?q?=E5=92=8Cgoogle=EF=BC=8C=20=E4=BF=AE=E5=A4=8D=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certd-client/src/layout/layout-basic.vue | 8 +- .../src/views/certd/mine/user-profile.vue | 13 ++- .../src/plugins/plugin-oauth/index.ts | 4 +- .../plugin-oauth/oauth2/plugin-github.ts | 103 ++++++++++++++++++ .../plugin-oauth/oauth2/plugin-google.ts | 103 ++++++++++++++++++ 5 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-github.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-google.ts diff --git a/packages/ui/certd-client/src/layout/layout-basic.vue b/packages/ui/certd-client/src/layout/layout-basic.vue index 22b83a645..df15dbeac 100644 --- a/packages/ui/certd-client/src/layout/layout-basic.vue +++ b/packages/ui/certd-client/src/layout/layout-basic.vue @@ -35,7 +35,13 @@ const menus = computed(() => [ const avatar = computed(() => { const avt = userStore.getUserInfo?.avatar; - return avt ? `/api/basic/file/download?key=${avt}` : ""; + if (!avt) { + return ""; + } + if (avt.startsWith("http")) { + return avt; + } + return `/api/basic/file/download?key=${avt}`; }); async function handleLogout() { diff --git a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue index 61c0bf546..2b87efb95 100644 --- a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue +++ b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue @@ -8,7 +8,7 @@ {{ userInfo.username }} {{ userInfo.nickName }} - + {{ userInfo.username }} @@ -108,6 +108,17 @@ async function bind(type: string) { window.location.href = loginUrl; } +const userAvatar = computed(() => { + if (isEmpty(userInfo.value.avatar)) { + return ""; + } + if (userInfo.value.avatar.startsWith("http")) { + return userInfo.value.avatar; + } + + return "api/basic/file/download?&key=" + userInfo.value.avatar; +}); + onMounted(async () => { await getUserInfo(); await loadOauthBounds(); diff --git a/packages/ui/certd-server/src/plugins/plugin-oauth/index.ts b/packages/ui/certd-server/src/plugins/plugin-oauth/index.ts index 8d853c0ff..3a43d4351 100644 --- a/packages/ui/certd-server/src/plugins/plugin-oauth/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-oauth/index.ts @@ -2,4 +2,6 @@ export * from './api.js' export * from './oidc/plugin-oidc.js' export * from './wx/plugin-wx.js' export * from './oauth2/plugin-gitee.js' -export * from './oauth2/plugin-clogin.js' \ No newline at end of file +export * from './oauth2/plugin-clogin.js' +export * from './oauth2/plugin-github.js' +export * from './oauth2/plugin-google.js' \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-github.ts b/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-github.ts new file mode 100644 index 000000000..0cf9958d3 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-github.ts @@ -0,0 +1,103 @@ +import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server"; +import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js"; + +@IsAddon({ + addonType: "oauth", + name: 'github', + title: 'GitHub认证', + desc: 'GitHub OAuth2登录', + icon:"simple-icons:github", + showTest: false, +}) +export class GithubOauthProvider extends BaseAddon implements IOauthProvider { + + @AddonInput({ + title: "ClientId", + helper: "[GitHub Developer Settings](https://github.com/settings/developers)创建应用后获取", + required: true, + }) + clientId = ""; + + @AddonInput({ + title: "ClientSecretKey", + component: { + placeholder: "ClientSecretKey / appSecretKey", + }, + required: true, + }) + clientSecretKey = ""; + + async buildLoginUrl(params: BuildLoginUrlReq) { + + let scope = "user:email" // Scope of the access request + let state:any = { + forType: params.forType || 'login', + } + state = this.ctx.utils.hash.base64(JSON.stringify(state)) + + const authorizeEndpoint = "https://github.com/login/oauth/authorize" + const redirectUrl = encodeURIComponent(params.redirectUri) + const loginUrl = `${authorizeEndpoint}?client_id=${this.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}` + return { + loginUrl, + ticketValue: { + state, + }, + }; + } + + async onCallback(req: OnCallbackReq) { + + const code = req.code || "" + + const tokenEndpoint = "https://github.com/login/oauth/access_token" + + const uri = new URL(req.currentURL) + const redirectUri = `${uri.origin}${uri.pathname}` + const res = await this.ctx.utils.http.request( { + url: tokenEndpoint, + method: "post", + headers: { + "Accept": "application/json" + }, + data:{ + client_id: this.clientId, + client_secret: this.clientSecretKey, + code, + redirect_uri: redirectUri + } + }) + + const tokens = res + + const userInfoEndpoint = "https://api.github.com/user" + + // 获取用户信息 + const userInfoRes = await this.ctx.utils.http.request( { + url: userInfoEndpoint, + method: "get", + headers: { + "Authorization": `Bearer ${tokens.access_token}`, + "Accept": "application/json" + } + }) + const userInfo = userInfoRes + + return { + token:{ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + }, + userInfo: { + openId: userInfo.id, + nickName: userInfo.login || userInfo.name || "", + avatar: userInfo.avatar_url, + }, + } + }; + + async buildLogoutUrl(params: BuildLogoutUrlReq) { + return {}; + } +} \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-google.ts b/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-google.ts new file mode 100644 index 000000000..6d1c12241 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-oauth/oauth2/plugin-google.ts @@ -0,0 +1,103 @@ +import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server"; +import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js"; + +@IsAddon({ + addonType: "oauth", + name: 'google', + title: 'Google认证', + desc: 'Google OAuth2登录', + icon:"simple-icons:google", + showTest: false, +}) +export class GoogleOauthProvider extends BaseAddon implements IOauthProvider { + + @AddonInput({ + title: "ClientId", + helper: "[Google Cloud Console](https://console.cloud.google.com/apis/credentials)创建应用后获取", + required: true, + }) + clientId = ""; + + @AddonInput({ + title: "ClientSecretKey", + component: { + placeholder: "ClientSecretKey / appSecretKey", + }, + required: true, + }) + clientSecretKey = ""; + + async buildLoginUrl(params: BuildLoginUrlReq) { + + let scope = "email profile" // Scope of the access request + let state:any = { + forType: params.forType || 'login', + } + state = this.ctx.utils.hash.base64(JSON.stringify(state)) + + const authorizeEndpoint = "https://accounts.google.com/o/oauth2/auth" + const redirectUrl = encodeURIComponent(params.redirectUri) + const loginUrl = `${authorizeEndpoint}?client_id=${this.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}` + return { + loginUrl, + ticketValue: { + state, + }, + }; + } + + async onCallback(req: OnCallbackReq) { + + const code = req.code || "" + + const tokenEndpoint = "https://oauth2.googleapis.com/token" + + const uri = new URL(req.currentURL) + const redirectUri = `${uri.origin}${uri.pathname}` + const res = await this.ctx.utils.http.request( { + url: tokenEndpoint, + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data:{ + client_id: this.clientId, + client_secret: this.clientSecretKey, + code, + redirect_uri: redirectUri, + grant_type: "authorization_code" + } + }) + + const tokens = res + + const userInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo" + + // 获取用户信息 + const userInfoRes = await this.ctx.utils.http.request( { + url: userInfoEndpoint, + method: "get", + headers: { + "Authorization": `Bearer ${tokens.access_token}` + } + }) + const userInfo = userInfoRes + + return { + token:{ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + }, + userInfo: { + openId: userInfo.sub, + nickName: userInfo.name || userInfo.email || "", + avatar: userInfo.picture, + }, + } + }; + + async buildLogoutUrl(params: BuildLogoutUrlReq) { + return {}; + } +} \ No newline at end of file