feat: 彩虹登录支持选择多种登录方式

This commit is contained in:
xiaojunnuo
2026-05-14 01:39:22 +08:00
parent 45dedf5bc7
commit 7aa0c7e491
16 changed files with 371 additions and 88 deletions
@@ -64,6 +64,7 @@ export class SysPublicSettings extends BaseSettings {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
}> = {}; }> = {};
notice?: string; notice?: string;
@@ -85,6 +85,7 @@ export type SysPublicSetting = {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
} }
>; >;
// 系统通知 // 系统通知
@@ -68,20 +68,23 @@ export async function GetOauthProviders() {
}); });
} }
export async function UnbindOauth(type: string) { export async function UnbindOauth(type: string, subtype?: string) {
return await request({ return await request({
url: "/oauth/unbind", url: "/oauth/unbind",
method: "POST", method: "POST",
data: { type }, data: {
type: subtype ? `${type}:${subtype}` : type,
},
}); });
} }
export async function OauthBoundUrl(type: string) { export async function OauthBoundUrl(type: string, subtype?: string) {
return await request({ return await request({
url: "/oauth/login", url: "/oauth/login",
method: "POST", method: "POST",
data: { data: {
type, type,
subtype,
forType: "bind", forType: "bind",
}, },
}); });
@@ -78,11 +78,11 @@
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag> <a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
</span> </span>
</div> </div>
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)"> <a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template> <template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑 解绑
</a-button> </a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)"> <a-button v-else type="primary" class="action-btn" @click="bind(item)">
<template #icon><fs-icon icon="ion:link-outline" /></template> <template #icon><fs-icon icon="ion:link-outline" /></template>
绑定 绑定
</a-button> </a-button>
@@ -214,7 +214,7 @@ async function loadOauthProviders() {
const computedOauthBounds = computed(() => { const computedOauthBounds = computed(() => {
const list = oauthProviders.value.map(item => { const list = oauthProviders.value.map(item => {
const bound = oauthBounds.value.find(bound => bound.type === item.name); const bound = oauthBounds.value.find(bound => bound.type === buildOauthBoundType(item));
return { return {
...item, ...item,
bound, bound,
@@ -223,20 +223,24 @@ const computedOauthBounds = computed(() => {
return list; return list;
}); });
async function unbind(type: string) { function buildOauthBoundType(item: any) {
return item.subtype ? `${item.name}:${item.subtype}` : item.name;
}
async function unbind(item: any) {
Modal.confirm({ Modal.confirm({
title: "确认解绑吗?", title: "确认解绑吗?",
okText: "确认", okText: "确认",
okType: "danger", okType: "danger",
onOk: async () => { onOk: async () => {
await api.UnbindOauth(type); await api.UnbindOauth(item.name, item.subtype);
await loadOauthBounds(); await loadOauthBounds();
}, },
}); });
} }
async function bind(type: string) { async function bind(item: any) {
const res = await api.OauthBoundUrl(type); const res = await api.OauthBoundUrl(item.name, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -2,7 +2,7 @@ import { request } from "/src/api/service";
const apiPrefix = "/oauth"; const apiPrefix = "/oauth";
export async function OauthLogin(type: string, forType?: string, from?: string) { export async function OauthLogin(type: string, forType?: string, from?: string, subtype?: string) {
return await request({ return await request({
url: apiPrefix + `/login`, url: apiPrefix + `/login`,
method: "post", method: "post",
@@ -10,6 +10,7 @@ export async function OauthLogin(type: string, forType?: string, from?: string)
type, type,
forType: forType || "login", forType: forType || "login",
from: from || "web", from: from || "web",
subtype,
}, },
}); });
} }
@@ -5,8 +5,8 @@
</div> </div>
<div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap"> <div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap">
<passkey-login></passkey-login> <passkey-login></passkey-login>
<template v-for="item in oauthProviderList" :key="item.type"> <template v-for="item in oauthProviderList" :key="buildProviderKey(item)">
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item.name)"> <div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item)">
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div> <div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
<div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div> <div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div>
</div> </div>
@@ -22,7 +22,17 @@ import { useSettingStore } from "/@/store/settings";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import PasskeyLogin from "../login/passkey-login.vue"; import PasskeyLogin from "../login/passkey-login.vue";
const oauthProviderList = ref([]); type OauthProviderItem = {
name: string;
type?: string;
subtype?: string;
title: string;
addonTitle?: string;
icon: string;
addonId?: number;
};
const oauthProviderList = ref<OauthProviderItem[]>([]);
const props = defineProps<{ const props = defineProps<{
oauthOnly?: boolean; oauthOnly?: boolean;
}>(); }>();
@@ -42,15 +52,19 @@ onMounted(async () => {
if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") { if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") {
const firstOauth = oauthProviderList.value.find(item => item.addonId > 0); const firstOauth = oauthProviderList.value.find(item => item.addonId > 0);
if (firstOauth) { if (firstOauth) {
goOauthLogin(firstOauth.name); goOauthLogin(firstOauth);
} }
} }
}); });
async function goOauthLogin(type: string) { function buildProviderKey(item: OauthProviderItem) {
return `${item.name}:${item.subtype || ""}`;
}
async function goOauthLogin(item: OauthProviderItem) {
//获取第三方登录URL //获取第三方登录URL
const from = "web"; const from = "web";
const res = await api.OauthLogin(type, from); const res = await api.OauthLogin(item.name, "login", from, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -114,7 +114,7 @@ export async function GetSmsTypeDefine(type: string) {
export async function GetOauthProviders() { export async function GetOauthProviders() {
return await request({ return await request({
url: "/oauth/providers", url: apiPrefix + "/oauth/providers",
method: "post", method: "post",
}); });
} }
@@ -109,6 +109,7 @@ const formState = reactive<Partial<SysSettings>>({
const oauthProviders = ref([]); const oauthProviders = ref([]);
async function loadOauthProviders() { async function loadOauthProviders() {
oauthProviders.value = await api.GetOauthProviders(); oauthProviders.value = await api.GetOauthProviders();
mergeOauthProviderSettings();
} }
const bindDomain = computed(() => { const bindDomain = computed(() => {
@@ -164,6 +165,16 @@ const onFinish = async (form: any) => {
function buildCallbackUrl(type: string) { function buildCallbackUrl(type: string) {
return `${window.location.origin}/api/oauth/callback/${type}`; return `${window.location.origin}/api/oauth/callback/${type}`;
} }
function mergeOauthProviderSettings() {
const savedProviders = formState.public?.oauthProviders || {};
for (const item of oauthProviders.value) {
const saved = savedProviders[item.name];
if (saved) {
item.addonId = saved.addonId;
}
}
}
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-oauth { .sys-settings-oauth {
@@ -14,55 +14,54 @@ input:
loginType: loginType:
title: 登录类型 title: 登录类型
component: component:
name: a-auto-complete name: a-select
vModel: value
mode: tags
multiple: true
options: options:
- label: QQ - label: QQ
value: qq value: qq
icon: logos:tencent-qq
- label: 微信 - label: 微信
value: wx value: wx
icon: logos:wechat-icon
- label: 支付宝 - label: 支付宝
value: alipay value: alipay
icon: logos:alipay
- label: 微博 - label: 微博
value: sina value: sina
icon: logos:sina-weibo
- label: 百度 - label: 百度
value: baidu value: baidu
icon: logos:baidu
- label: 华为 - label: 华为
value: huawei value: huawei
icon: simple-icons:huawei:#ff0000
- label: 小米 - label: 小米
value: xiaomi value: xiaomi
icon: logos:xiaomi-icon
- label: 谷歌 - label: 谷歌
value: google value: google
icon: logos:google-icon
- label: 微软 - label: 微软
value: microsoft value: microsoft
icon: logos:microsoft-icon
- label: Facebook - label: Facebook
value: facebook value: facebook
icon: logos:facebook
- label: Twitter - label: Twitter
value: twitter value: twitter
icon: logos:twitter
- label: 钉钉 - label: 钉钉
value: dingtalk value: dingtalk
icon: logos:dingtalk
- label: Gitee - label: Gitee
value: gitee value: gitee
icon: simple-icons:gitee:#c71d23
- label: Github - label: Github
value: github value: github
icon: logos:github-icon
required: true required: true
icon:
title: 自定义图标
component:
name: fs-icon-selector
vModel: modelValue
iconSets:
- streamline-logos
- logos
- fa-brands
- fa-solid
- fa-regular
- carbon
- ion
- ant-design
- mdi
- twemoji
- svg-spinners
required: false
appId: appId:
title: AppId title: AppId
helper: 彩虹聚合登录->应用列表->创建应用 获取 helper: 彩虹聚合登录->应用列表->创建应用 获取
@@ -11,6 +11,24 @@ import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js"; import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js"; import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
type OauthProviderSetting = {
type: string;
title: string;
icon?: string;
addonId: number;
types?: OauthProviderType[];
};
type OauthProviderType = {
type: string;
name: string;
icon?: string;
};
function getOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
/** /**
*/ */
@Provide() @Provide()
@@ -41,7 +59,7 @@ export class ConnectController extends BaseController {
if (!publicSettings?.oauthEnabled) { if (!publicSettings?.oauthEnabled) {
throw new Error("OAuth功能未启用"); throw new Error("OAuth功能未启用");
} }
const setting = publicSettings?.oauthProviders?.[type || ""] const setting = publicSettings?.oauthProviders?.[type || ""] as OauthProviderSetting | undefined;
if (!setting) { if (!setting) {
throw new Error(`未配置该OAuth类型:${type}`); throw new Error(`未配置该OAuth类型:${type}`);
} }
@@ -50,19 +68,30 @@ export class ConnectController extends BaseController {
if (!addon) { if (!addon) {
throw new Error("初始化OAuth插件失败"); throw new Error("初始化OAuth插件失败");
} }
return addon as IOauthProvider; return {
addon: addon as IOauthProvider,
setting,
};
} }
@Post('/login', { description: Constants.per.guest }) @Post('/login', { description: Constants.per.guest })
public async login(@Body(ALL) body: { type: string, forType?:string ,from?:string }) { public async login(@Body(ALL) body: { type: string, subtype?: string, forType?:string ,from?:string }) {
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo); const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
//构造登录url //构造登录url
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`; const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType ,from: body.from || "web" }); const { loginUrl, ticketValue } = await oauthProvider.addon.buildLoginUrl({
const ticket = this.codeService.setValidationValue(ticketValue) redirectUri: redirectUrl,
forType: body.forType,
from: body.from || "web",
subtype: body.subtype,
});
const ticket = this.codeService.setValidationValue({
...ticketValue,
subtype: body.subtype,
})
this.ctx.cookies.set("oauth_ticket", ticket, { this.ctx.cookies.set("oauth_ticket", ticket, {
httpOnly: true, httpOnly: true,
// secure: true, // secure: true,
@@ -78,7 +107,7 @@ export class ConnectController extends BaseController {
checkPlus() checkPlus()
//处理登录回调 //处理登录回调
const addon = await this.getOauthProvider(type); const oauthProvider = await this.getOauthProvider(type);
const request = this.ctx.request; const request = this.ctx.request;
// const ticketValue = this.codeService.getValidationValue(ticket); // const ticketValue = this.codeService.getValidationValue(ticket);
// if (!ticketValue) { // if (!ticketValue) {
@@ -98,7 +127,7 @@ export class ConnectController extends BaseController {
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}` const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
try { try {
const tokenRes = await addon.onCallback({ const tokenRes = await oauthProvider.addon.onCallback({
code: query.code, code: query.code,
state: query.state, state: query.state,
ticketValue, ticketValue,
@@ -108,7 +137,7 @@ export class ConnectController extends BaseController {
const userInfo = tokenRes.userInfo; const userInfo = tokenRes.userInfo;
const validationCode = await this.codeService.setValidationValue({ const validationCode = await this.codeService.setValidationValue({
type, type: getOauthBoundType(type, ticketValue.subtype),
userInfo, userInfo,
}); });
@@ -129,8 +158,10 @@ export class ConnectController extends BaseController {
@Post('/getLogoutUrl', { description: Constants.per.guest }) @Post('/getLogoutUrl', { description: Constants.per.guest })
public async logout(@Body(ALL) body: any) { public async logout(@Body(ALL) body: any) {
checkPlus() checkPlus()
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const { logoutUrl } = await addon.buildLogoutUrl(body); const { logoutUrl } = await oauthProvider.addon.buildLogoutUrl({
...body,
});
return this.ok({ logoutUrl }); return this.ok({ logoutUrl });
} }
@@ -144,7 +175,7 @@ export class ConnectController extends BaseController {
} }
const type = validationValue.type; const type = validationValue.type;
if (type !== body.type) { if (type !== body.type && !type.startsWith(`${body.type}:`)) {
throw new Error("校验码错误"); throw new Error("校验码错误");
} }
const userInfo = validationValue.userInfo; const userInfo = validationValue.userInfo;
@@ -262,16 +293,32 @@ export class ConnectController extends BaseController {
provider.addonId = conf.addonId; provider.addonId = conf.addonId;
provider.addonTitle = addonEntity.name; provider.addonTitle = addonEntity.name;
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null); const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null) as IOauthProvider & { icon?: string; types?: OauthProviderType[] };
const {logoutUrl} = await addon.buildLogoutUrl(); const {logoutUrl} = await addon.buildLogoutUrl({});
if (logoutUrl){ if (logoutUrl){
provider.logoutUrl = logoutUrl; provider.logoutUrl = logoutUrl;
} }
if(addon.icon){ if(addon.icon){
provider.icon = addon.icon; provider.icon = addon.icon;
} }
if(addon.types?.length){
provider.types = addon.types;
}
} }
} }
if (provider.addonId && provider.types?.length) {
for (const subtype of provider.types) {
list.push({
...provider,
name: type,
subtype: subtype.type,
title: subtype.name,
icon: subtype.icon || provider.icon,
addonTitle: subtype.name,
});
}
continue;
}
list.push(provider); list.push(provider);
} }
@@ -135,6 +135,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.savePrivateSettings(privateSettings); await this.service.savePrivateSettings(privateSettings);
return this.ok({}); return this.ok({});
} }
@Post('/stopOtherUserTimer', { description: 'sys:settings:edit' }) @Post('/stopOtherUserTimer', { description: 'sys:settings:edit' })
async stopOtherUserTimer(@Body(ALL) body) { async stopOtherUserTimer(@Body(ALL) body) {
await this.pipelineService.stopOtherUserPipeline(1); await this.pipelineService.stopOtherUserPipeline(1);
@@ -1,12 +1,14 @@
import assert from "assert"; import assert from "assert";
import esmock from "esmock"; import esmock from "esmock";
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js"; import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, buildOauthBoundType, parseStorageValue } from "./auto-fix.js";
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) { function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any; sysSettingsService?: any; oauthBoundService?: any }) {
const autoFix = new AutoFix(); const autoFix = new AutoFix();
autoFix.pluginConfigService = options.pluginConfigService; autoFix.pluginConfigService = options.pluginConfigService;
autoFix.accessService = options.accessService; autoFix.accessService = options.accessService;
autoFix.storageService = options.storageService; autoFix.storageService = options.storageService;
autoFix.sysSettingsService = options.sysSettingsService;
autoFix.oauthBoundService = options.oauthBoundService;
return autoFix; return autoFix;
} }
@@ -42,6 +44,11 @@ describe("AutoFix", () => {
}); });
}); });
it("builds OAuth subtype bound type", () => {
assert.equal(buildOauthBoundType("clogin", "alipay"), "clogin:alipay");
assert.equal(buildOauthBoundType("github"), "github");
});
it("finds legacy Google account config by exact email key only", async () => { it("finds legacy Google account config by exact email key only", async () => {
let findOneWhere: any; let findOneWhere: any;
let findCalled = false; let findCalled = false;
@@ -107,6 +114,25 @@ describe("AutoFix", () => {
} as any, } as any,
accessService: null as any, accessService: null as any,
storageService: null as any, storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne() {
return null;
},
async update() {
return { affected: 0 };
},
});
},
},
}); });
await autoFix.init(); await autoFix.init();
@@ -179,4 +205,97 @@ describe("AutoFix", () => {
}); });
}); });
it("fixes legacy OAuth bound type from string addon loginType and converts loginType to array", async () => {
const updates: any[] = [];
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne(entity: any, options: any) {
assert.equal(entity.name, "AddonEntity");
assert.deepEqual(options, { where: { id: 1 } });
return {
id: 1,
setting: JSON.stringify({
loginType: "alipay",
}),
};
},
async update(entity: any, where: any, value: any) {
updates.push({ entity: entity.name, where, value });
return { affected: entity.name === "OauthBoundEntity" ? 1 : 0 };
},
});
},
},
});
await autoFix.fixOauthSubtypeBoundType();
assert.deepEqual(updates[0], {
entity: "OauthBoundEntity",
where: { type: "clogin" },
value: { type: "clogin:alipay" },
});
assert.equal(updates[1].entity, "AddonEntity");
assert.deepEqual(updates[1].where, { id: 1 });
assert.deepEqual(JSON.parse(updates[1].value.setting).loginType, ["alipay"]);
});
it("skips OAuth subtype fix when addon loginType is already not legacy string", async () => {
let updateCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: null as any,
sysSettingsService: {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
},
oauthBoundService: {
async transaction(callback: any) {
return await callback({
async findOne() {
return {
id: 1,
setting: JSON.stringify({
loginType: ["alipay", "github"],
}),
};
},
async update() {
updateCalled = true;
return { affected: 0 };
},
});
},
},
});
await autoFix.fixOauthSubtypeBoundType();
assert.equal(updateCalled, false);
});
}); });
@@ -1,9 +1,11 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic"; import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server"; import { AccessService, AddonEntity, SysSettingsService } from "@certd/lib-server";
import { isComm } from "@certd/plus-core"; import { isComm } from "@certd/plus-core";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js"; import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
import { StorageService } from "../pipeline/service/storage-service.js"; import { StorageService } from "../pipeline/service/storage-service.js";
import { OauthBoundService } from "../login/service/oauth-bound-service.js";
import { OauthBoundEntity } from "../login/entity/oauth-bound.js";
export function parseStorageValue(value?: string) { export function parseStorageValue(value?: string) {
if (!value) { if (!value) {
@@ -33,6 +35,10 @@ export function buildLegacyGoogleAccountConfigWhere(email: string) {
}; };
} }
export function buildOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix { export class AutoFix {
@@ -45,9 +51,66 @@ export class AutoFix {
@Inject() @Inject()
storageService: StorageService; storageService: StorageService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
oauthBoundService: OauthBoundService;
async init() { async init() {
await this.fixGoogleCommonEabAccountKey(); await this.fixGoogleCommonEabAccountKey();
await this.fixOauthSubtypeBoundType();
} }
async fixOauthSubtypeBoundType() {
try {
const publicSettings = await this.sysSettingsService.getPublicSettings();
const oauthProviders = publicSettings.oauthProviders || {};
await this.oauthBoundService.transaction(async manager => {
for (const [type, provider] of Object.entries(oauthProviders)) {
if (!provider.addonId) {
continue;
}
const addonEntity = await manager.findOne(AddonEntity, { where: { id: provider.addonId } });
const legacyLoginType = this.getLegacyAddonLoginType(addonEntity?.setting);
if (!legacyLoginType) {
continue;
}
const newType = buildOauthBoundType(type, legacyLoginType);
const res = await manager.update(OauthBoundEntity, { type }, { type: newType });
if (res.affected) {
logger.info(`已修复OAuth绑定历史数据,${type} -> ${newType},数量=${res.affected}`);
}
await this.convertLegacyAddonLoginTypeToArray(addonEntity, legacyLoginType, manager);
}
});
} catch (e: any) {
logger.error("修复OAuth subtype绑定历史数据失败", e);
}
}
private getLegacyAddonLoginType(settingValue?: string) {
if (!settingValue) {
return null;
}
const setting = JSON.parse(settingValue);
return typeof setting.loginType === "string" && setting.loginType ? setting.loginType : null;
}
private async convertLegacyAddonLoginTypeToArray(addonEntity: AddonEntity | null, loginType: string, manager: any) {
if (!addonEntity?.setting) {
return;
}
const setting = JSON.parse(addonEntity.setting);
if (typeof setting.loginType !== "string") {
return;
}
setting.loginType = [loginType];
await manager.update(AddonEntity, { id: addonEntity.id }, { setting: JSON.stringify(setting) });
}
async fixGoogleCommonEabAccountKey() { async fixGoogleCommonEabAccountKey() {
if (!isComm()) { if (!isComm()) {
return; return;
@@ -100,7 +163,7 @@ export class AutoFix {
} }
return null; return null;
} }
parseStorageValue(value?: string) { parseStorageValue(value?: string) {
return parseStorageValue(value); return parseStorageValue(value);
} }
@@ -245,7 +245,8 @@ export class LoginService {
async loginByOpenId(req: { openId: string, type:string }) { async loginByOpenId(req: { openId: string, type:string }) {
const {openId, type} = req; const {openId, type} = req;
const oauthBound = await this.oauthBoundService.findOne({ const oauthBound = await this.oauthBoundService.findOne({
where:{openId, type} where:{openId, type: type.replace(':', '')')}
});
}); });
if (oauthBound == null) { if (oauthBound == null) {
return null return null
@@ -41,9 +41,11 @@ export type BuildLoginUrlReq = {
redirectUri: string; redirectUri: string;
forType?: string; forType?: string;
from?:string; from?:string;
subtype?: string;
} }
export type BuildLogoutUrlReq = { export type BuildLogoutUrlReq = {
subtype?: string;
} }
export type LogoutUrlReply = { export type LogoutUrlReply = {
@@ -54,4 +56,4 @@ export interface IOauthProvider {
buildLoginUrl: (params: BuildLoginUrlReq) => Promise<LoginUrlReply>; buildLoginUrl: (params: BuildLoginUrlReq) => Promise<LoginUrlReply>;
onCallback: (params: OnCallbackReq) => Promise<OauthToken>; onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
buildLogoutUrl: (params: BuildLogoutUrlReq) => Promise<LogoutUrlReply>; buildLogoutUrl: (params: BuildLogoutUrlReq) => Promise<LogoutUrlReply>;
} }
@@ -1,6 +1,31 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server"; import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js"; import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
import { IconSets } from "../iconsets.js";
const CLOGIN_TYPES = [
{ label: "QQ", value: "qq", icon: "logos:tencent-qq" },
{ label: "微信", value: "wx", icon: "logos:wechat-icon" },
{ label: "支付宝", value: "alipay", icon: "simple-icons:alipay:#0099ff" },
{ label: "微博", value: "sina", icon: "logos:sina-weibo" },
{ label: "百度", value: "baidu", icon: "logos:baidu" },
{ label: "华为", value: "huawei", icon: "simple-icons:huawei:#ff0000" },
{ label: "小米", value: "xiaomi", icon: "logos:xiaomi-icon" },
{ label: "谷歌", value: "google", icon: "logos:google-icon" },
{ label: "微软", value: "microsoft", icon: "logos:microsoft-icon" },
{ label: "Facebook", value: "facebook", icon: "logos:facebook" },
{ label: "Twitter", value: "twitter", icon: "logos:twitter" },
{ label: "钉钉", value: "dingtalk", icon: "logos:dingtalk" },
{ label: "Gitee", value: "gitee", icon: "simple-icons:gitee:#c71d23" },
{ label: "Github", value: "github", icon: "logos:github-icon" },
];
function getCloginType(subtype?: string, loginType?: string | string[]) {
const types = Array.isArray(loginType) ? loginType : [loginType];
const type = subtype || types.find(item => !!item);
if (!type) {
throw new Error("请选择彩虹聚合登录类型");
}
return type;
}
@IsAddon({ @IsAddon({
addonType: "oauth", addonType: "oauth",
@@ -22,38 +47,27 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
@AddonInput({ @AddonInput({
title: "登录类型", title: "登录类型",
component: { component: {
name: "a-auto-complete", name: "a-select",
options: [ vModel: "value",
{ label: "QQ", value: "qq" }, mode: "tags",
{ label: "微信", value: "wx" }, multiple: true,
{ label: "支付宝", value: "alipay" }, options: CLOGIN_TYPES,
{ label: "微博", value: "sina" },
{ label: "百度", value: "baidu" },
{ label: "华为", value: "huawei" },
{ label: "小米", value: "xiaomi" },
{ label: "谷歌", value: "google" },
{ label: "微软", value: "microsoft" },
{ label: "Facebook", value: "facebook" },
{ label: "Twitter", value: "twitter" },
{ label: "钉钉", value: "dingtalk" },
{ label: "Gitee", value: "gitee" },
{ label: "Github", value: "github" },
]
}, },
required: true, required: true,
}) })
loginType = ""; loginType: string[] | string = [];
@AddonInput({ get types() {
title: "自定义图标", const loginTypes = Array.isArray(this.loginType) ? this.loginType : [this.loginType].filter(Boolean);
component: { return loginTypes.map(type => {
name:"fs-icon-selector", const option = CLOGIN_TYPES.find(item => item.value === type);
vModel:"modelValue", return {
iconSets: IconSets, type,
}, name: option?.label || type,
required: false, icon: option?.icon,
}) };
icon = ""; });
}
@AddonInput({ @AddonInput({
title: "AppId", title: "AppId",
@@ -75,11 +89,12 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
async buildLoginUrl(params: BuildLoginUrlReq) { async buildLoginUrl(params: BuildLoginUrlReq) {
let redirectUri = params.redirectUri || "" let redirectUri = params.redirectUri || ""
const loginType = getCloginType(params.subtype, this.loginType);
// if(redirectUri.indexOf("localhost:3008")>=0){ // if(redirectUri.indexOf("localhost:3008")>=0){
// redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work") // redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work")
// } // }
const res = await this.ctx.http.request({ const res = await this.ctx.http.request({
url: `${this.endpoint}/connect.php?act=login&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&redirect_uri=${redirectUri}` url: `${this.endpoint}/connect.php?act=login&appid=${this.appId}&appkey=${this.appKey}&type=${loginType}&redirect_uri=${redirectUri}`
}) })
this.checkRes(res) this.checkRes(res)
@@ -103,8 +118,9 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
//校验state //校验state
const code = req.code || "" const code = req.code || ""
const loginType = getCloginType(req.ticketValue?.subtype, this.loginType);
const tokenEndpoint = `${this.endpoint}/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&code=${code}` const tokenEndpoint = `${this.endpoint}/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${loginType}&code=${code}`
const res = await this.ctx.utils.http.request({ const res = await this.ctx.utils.http.request({
url: tokenEndpoint, url: tokenEndpoint,
method: "post", method: "post",