perf: 优化天翼云cdn 等待5秒部署完成

This commit is contained in:
xiaojunnuo
2025-11-29 03:25:21 +08:00
parent 21585ca565
commit 53c88ad5af
13 changed files with 211 additions and 84 deletions

View File

@@ -9,7 +9,7 @@ export class Locker {
}
async execute(lockStr: string, callback: any, options?: { timeout?: number }) {
const timeout = options?.timeout ?? 20000;
const timeout = options?.timeout ?? 120000;
return this.asyncLocker.acquire(lockStr, callback, { timeout });
}
}

View File

@@ -36,7 +36,7 @@ export class TencentSslClient {
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error("请求失败:" + ret.Error.Code + "," + ret.Error.Message);
throw new Error("请求失败:" + ret.Error.Code + "," + ret.Error.Message + ",requestId" + ret.RequestId);
}
}
@@ -70,43 +70,33 @@ export class TencentSslClient {
}
async deployCertificateInstance(params: any) {
const client = await this.getSslClient();
const res = await client.DeployCertificateInstance(params);
this.checkRet(res);
return res;
return await this.doRequest("DeployCertificateInstance", params);
}
async DescribeHostUploadUpdateRecordDetail(params: any) {
const client = await this.getSslClient();
const res = await client.request("DescribeHostUploadUpdateRecordDetail", params);
this.checkRet(res);
return res;
return await this.doRequest("DescribeHostUploadUpdateRecordDetail", params);
}
async UploadUpdateCertificateInstance(params: any) {
const client = await this.getSslClient();
const res = await client.request("UploadUpdateCertificateInstance", params);
this.checkRet(res);
return res;
return await this.doRequest("UploadUpdateCertificateInstance", params);
}
async DescribeCertificates(params: { Limit?: number; Offset?: number; SearchKey?: string }) {
const client = await this.getSslClient();
const res = await client.DescribeCertificates({
return await this.doRequest("DescribeCertificates", {
ExpirationSort: "ASC",
...params,
});
this.checkRet(res);
return res;
}
async doRequest(action: string, params: any) {
const client = await this.getSslClient();
if (!client[action]) {
throw new Error(`action ${action} not found`);
try {
const res = await client.request(action, params);
this.checkRet(res);
return res;
} catch (e) {
this.logger.error(`action ${action} error: ${e.message},requestId=${e.RequestId}`);
throw e;
}
const res = await client[action](params);
this.checkRet(res);
return res;
}
}

View File

@@ -604,7 +604,7 @@ export default {
limitUserPipelineCountHelper: "0为不限制",
enableSelfRegistration: "开启自助注册",
enableUserValidityPeriod: "开启用户有效期",
userValidityPeriodHelper: "有效期内用户可正常使用,失效后流水线将被停用",
userValidityPeriodHelper: "有效期内用户可正常使用,失效后用户的流水线将被停用",
enableUsernameRegistration: "开启用户名注册",
enableEmailRegistration: "开启邮箱注册",
proFeature: "专业版功能",
@@ -762,12 +762,12 @@ export default {
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
fixedCertExpireDaysRecommend: "推荐90",
enableOauth: "启用OAuth2登录",
oauthEnabledHelper: "是否启用OAuth2登录",
oauthProviders: "OAuth2登录提供商",
oauthType: "OAuth2登录类型",
oauthConfig: "OAuth2登录配置",
oauthProviderSelectorPlaceholder: "请选择OAuth2登录提供商",
enableOauth: "启用第三方登录",
oauthEnabledHelper: "是否启用第三方登录",
oauthProviders: "第三方登录提供商",
oauthType: "第三方登录类型",
oauthConfig: "第三方登录配置",
oauthProviderSelectorPlaceholder: "请选择第三方登录提供商",
},
},
modal: {

View File

@@ -22,3 +22,36 @@ export async function UpdateProfile(form: any) {
data: form,
});
}
export async function GetOauthBounds() {
return await request({
url: "/oauth/bounds",
method: "POST",
});
}
export async function GetOauthProviders() {
return await request({
url: "/oauth/providers",
method: "POST",
});
}
export async function UnbindOauth(type: string) {
return await request({
url: "/oauth/unbind",
method: "POST",
data: { type },
});
}
export async function OauthBoundUrl(type: string) {
return await request({
url: "/oauth/login",
method: "POST",
data: {
type,
forType: "bind",
},
});
}

View File

@@ -15,7 +15,14 @@
</a-descriptions-item>
<a-descriptions-item :label="t('authentication.email')">{{ userInfo.email }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.phoneNumber')">{{ userInfo.phoneCode }}{{ userInfo.mobile }}</a-descriptions-item>
<a-descriptions-item></a-descriptions-item>
<a-descriptions-item v-if="settingStore.sysPublic.oauthEnabled && settingStore.isPlus" label="第三方账号绑定">
<div v-for="item in computedOauthBounds" :key="item.name" class="flex items-center gap-2">
<fs-icon :icon="item.icon" class="mr-2 text-blue-500" />
<span class="mr-2 w-36">{{ item.title }}</span>
<a-button v-if="item.bound" type="link" danger @click="unbind(item.name)">解绑</a-button>
<a-button v-else type="primary" @click="bind(item.name)">绑定</a-button>
</div>
</a-descriptions-item>
<a-descriptions-item :label="t('common.handle')">
<a-button type="primary" @click="doUpdate">{{ t("authentication.updateProfile") }}</a-button>
<change-password-button class="ml-10" :show-button="true"> </change-password-button>
@@ -27,10 +34,12 @@
<script lang="ts" setup>
import * as api from "./api";
import { Ref, ref } from "vue";
import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
import { Modal } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
const { t } = useI18n();
@@ -38,13 +47,13 @@ defineOptions({
name: "UserProfile",
});
const settingStore = useSettingStore();
const userInfo: Ref = ref({});
const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
};
getUserInfo();
const { openEditProfileDialog } = useUserProfile();
function doUpdate() {
@@ -54,4 +63,51 @@ function doUpdate() {
},
});
}
const oauthBounds = ref([]);
const oauthProviders = ref([]);
async function loadOauthBounds() {
const res = await api.GetOauthBounds();
oauthBounds.value = res;
}
async function loadOauthProviders() {
const res = await api.GetOauthProviders();
oauthProviders.value = res;
}
const computedOauthBounds = computed(() => {
const list = oauthProviders.value.map(item => {
const bound = oauthBounds.value.find(bound => bound.type === item.name);
return {
...item,
bound,
};
});
return list;
});
async function unbind(type: string) {
Modal.confirm({
title: "确认解绑吗?",
okText: "确认",
okType: "danger",
onOk: async () => {
await api.UnbindOauth(type);
await loadOauthBounds();
},
});
}
async function bind(type: string) {
//获取第三方登录URL
const res = await api.OauthBoundUrl(type);
const loginUrl = res.loginUrl;
window.location.href = loginUrl;
}
onMounted(async () => {
await getUserInfo();
await loadOauthBounds();
await loadOauthProviders();
});
</script>

View File

@@ -65,7 +65,7 @@
</div>
</a-form-item>
<div v-if="!queryBindCode" class="w-full">
<div v-if="!queryBindCode && settingStore.sysPublic.oauthEnabled && settingStore.isPlus" class="w-full">
<oauth-footer></oauth-footer>
</div>
</a-form>
@@ -82,7 +82,7 @@
<loading-button type="primary" size="large" html-type="button" class="login-button" :click="handleTwoFactorSubmit">OTP验证登录</loading-button>
</a-form-item>
<a-form-item class="user-login-other">
<a-form-item class="mt-10">
<a class="register" @click="twoFactor.loginId = null"> 返回 </a>
</a-form-item>
</a-form>

View File

@@ -2,12 +2,13 @@ import { request } from "/src/api/service";
const apiPrefix = "/oauth";
export async function OauthLogin(type: string) {
export async function OauthLogin(type: string, forType?: string) {
return await request({
url: apiPrefix + `/login`,
method: "post",
data: {
type,
forType: forType || "login",
},
});
}

View File

@@ -5,16 +5,16 @@
<span v-if="!error">登录中...</span>
<span v-else>{{ error }}</span>
</div>
<div v-else class="oauth-callback-title">
<div>第三方登录成功还未绑定账号请选择</div>
<div v-else class="oauth-callback-title mt-10">
<div>第三方{{ oauthType }}登录成功还未绑定账号请选择</div>
<div>
<a-button class="w-full mt-5" type="primary" @click="goBindUser">绑定已有账号</a-button>
<a-button class="w-full mt-5" type="primary" @click="autoRegister">创建新账号</a-button>
<div class="mt-10">
<a-button class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</a-button>
<a-button class="w-full mt-10" type="primary" @click="autoRegister">创建新账号</a-button>
</div>
<div class="w-full mt-5">
<router-link to="/login" class="w-full mt-5" type="primary">返回登录页</router-link>
<div class="w-full mt-10">
<router-link to="/login" class="w-full mt-10" type="primary">返回登录页</router-link>
</div>
</div>
</div>
@@ -26,11 +26,13 @@ import { ref, onMounted } from "vue";
import * as api from "./api";
import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "/@/store/user";
import { notification } from "ant-design-vue";
const route = useRoute();
const router = useRouter();
const oauthType = route.params.type as string;
const validationCode = route.query.validationCode as string;
const forType = route.query.forType as string;
const error = ref(route.query.error as string);
const userStore = useUserStore();
@@ -58,6 +60,18 @@ onMounted(async () => {
if (error.value) {
return;
}
if (forType === "bind") {
//绑定第三方账号
await api.BindUser(validationCode);
notification.success({
message: "绑定成功",
});
//跳转到首页
router.replace("/certd/mine/user-profile");
return;
}
await handleOauthToken();
});
@@ -86,7 +100,7 @@ async function autoRegister() {
justify-content: center;
align-items: center;
gap: 16px;
width: 100%;
.oauth-callback-content {
display: flex;
justify-content: center;
@@ -96,12 +110,14 @@ async function autoRegister() {
border-radius: 16px;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.1);
width: 500px;
max-width: 90%;
margin: 0 auto;
margin-top: 50px;
margin-bottom: 100px;
min-height: 200px;
.oauth-callback-title {
font-size: 24px;
font-size: 16px;
font-weight: 500;
}
}

View File

@@ -6,9 +6,10 @@ import { LoginService } from "../../../modules/login/service/login-service.js";
import { CodeService } from "../../../modules/basic/service/code-service.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
import { logger, simpleNanoId } from "@certd/basic";
import { logger, simpleNanoId, utils } from "@certd/basic";
import { OauthBoundService } from "../../../modules/login/service/oauth-bound-service.js";
import { OauthBoundEntity } from "../../../modules/login/entity/oauth-bound.js";
import { checkPlus } from "@certd/plus-core";
/**
*/
@@ -50,14 +51,14 @@ export class ConnectController extends BaseController {
}
@Post('/login', { summary: Constants.per.guest })
public async login(@Body(ALL) body: { type: string }) {
public async login(@Body(ALL) body: { type: string, forType?:string }) {
const addon = await this.getOauthProvider(body.type);
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || "";
//构造登录url
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl });
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType });
const ticket = this.codeService.setValidationValue(ticketValue)
this.ctx.cookies.set("oauth_ticket", ticket, {
httpOnly: true,
@@ -68,6 +69,9 @@ export class ConnectController extends BaseController {
}
@Get('/callback/:type', { summary: Constants.per.guest })
public async callback(@Param('type') type: string, @Query() query: Record<string, string>) {
checkPlus()
//处理登录回调
const addon = await this.getOauthProvider(type);
const request = this.ctx.request;
@@ -103,7 +107,9 @@ export class ConnectController extends BaseController {
userInfo,
});
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}`;
const state = JSON.parse(utils.hash.base64Decode(query.state));
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}&forType=${state.forType}`;
this.ctx.redirect(redirectUrl);
} catch (err) {
logger.error(err);
@@ -115,6 +121,7 @@ export class ConnectController extends BaseController {
@Post('/token', { summary: Constants.per.guest })
public async token(@Body(ALL) body: { validationCode: string, type: string }) {
checkPlus()
const validationValue = await this.codeService.getValidationValue(body.validationCode);
if (!validationValue) {
throw new Error("校验码错误");
@@ -140,24 +147,6 @@ export class ConnectController extends BaseController {
return this.ok(loginRes);
}
@Post('/bind', { summary: Constants.per.loginOnly })
public async bind(@Body(ALL) body: any) {
//需要已登录
const userId = this.getUserId();
const validationValue = this.codeService.getValidationValue(body.validationCode);
if (!validationValue) {
throw new Error("校验码错误");
}
const type = validationValue.type;
const userInfo = validationValue.userInfo;
const openId = userInfo.openId;
await this.oauthBoundService.bind({
userId,
type,
openId,
});
return this.ok(1);
}
@Post('/autoRegister', { summary: Constants.per.guest })
public async autoRegister(@Body(ALL) body: { validationCode: string, type: string }) {
@@ -185,6 +174,26 @@ export class ConnectController extends BaseController {
return this.ok(loginRes);
}
@Post('/bind', { summary: Constants.per.loginOnly })
public async bind(@Body(ALL) body: any) {
//需要已登录
const userId = this.getUserId();
const validationValue = this.codeService.getValidationValue(body.validationCode);
if (!validationValue) {
throw new Error("校验码错误");
}
const type = validationValue.type;
const userInfo = validationValue.userInfo;
const openId = userInfo.openId;
await this.oauthBoundService.bind({
userId,
type,
openId,
});
return this.ok(1);
}
@Post('/unbind', { summary: Constants.per.loginOnly })
public async unbind(@Body(ALL) body: any) {
//需要已登录
@@ -196,6 +205,18 @@ export class ConnectController extends BaseController {
return this.ok(1);
}
@Post('/bounds', { summary: Constants.per.loginOnly })
public async bounds(@Body(ALL) body: any) {
//需要已登录
const userId = this.getUserId();
const bounds = await this.oauthBoundService.find({
where :{
userId,
}
});
return this.ok(bounds);
}
@Post('/providers', { summary: Constants.per.guest })
public async providers() {
const list = addonRegistry.getDefineList("oauth");

View File

@@ -44,8 +44,11 @@ export class OauthBoundService extends BaseService<OauthBoundEntity> {
type,
},
});
if (exist) {
throw new Error('该第三方账号已绑定用户');
if (exist ) {
if(exist.userId === userId){
return;
}
throw new Error('该第三方账号已绑定其他用户');
}
const exist2 = await this.repository.findOne({

View File

@@ -38,6 +38,6 @@ export type LoginUrlReply = {
}
export interface IOauthProvider {
buildLoginUrl: (params: { redirectUri: string }) => Promise<LoginUrlReply>;
buildLoginUrl: (params: { redirectUri: string, forType?: string }) => Promise<LoginUrlReply>;
onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
}

View File

@@ -29,7 +29,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
@AddonInput({
title: "服务地址",
helper: "Issuer地址",
helper: "Issuer地址,去掉/.well-known/openid-configuration的服务发现地址",
component: {
placeholder: "https://oidc.example.com/oidc",
},
@@ -56,7 +56,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
}
}
async buildLoginUrl(params: { redirectUri: string }) {
async buildLoginUrl(params: { redirectUri: string, forType?: string }) {
const { config, client } = await this.getClient()
let redirect_uri = new URL(params.redirectUri)
@@ -69,7 +69,10 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
*/
let code_verifier = client.randomPKCECodeVerifier()
let code_challenge = await client.calculatePKCECodeChallenge(code_verifier)
let state = client.randomState()
let state:any = {
forType: params.forType || 'login',
}
state = this.ctx.utils.hash.base64(JSON.stringify(state))
let parameters: any = {
redirect_uri,
@@ -90,13 +93,11 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
// }
let redirectTo = client.buildAuthorizationUrl(config, parameters)
// now redirect the user to redirectTo.href
console.log('redirecting to', redirectTo.href)
return {
loginUrl: redirectTo.href,
ticketValue: {
codeVerifier: code_verifier,
state,
},
};
}
@@ -114,7 +115,6 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
}
)
console.log('Token Endpoint Response', tokens)
const claims = tokens.claims()
return {
token:{

View File

@@ -124,6 +124,9 @@ export class TencentRefreshCert extends AbstractTaskPlugin {
let resourceTypes = []
const resourceTypesRegions = []
if(!this.resourceTypesRegions){
this.resourceTypesRegions = []
}
for (const item of this.resourceTypesRegions) {
const [type,region] = item.split("_")
if (!resourceTypes.includes( type)){
@@ -156,13 +159,17 @@ export class TencentRefreshCert extends AbstractTaskPlugin {
break;
}
retryCount++
deployRes = await sslClient.UploadUpdateCertificateInstance({
OldCertificateId: certId,
const params = {
"OldCertificateId": certId,
"ResourceTypes": resourceTypes,
"CertificatePublicKey": this.cert.crt,
"CertificatePrivateKey": this.cert.key,
"CertificatePublicKey": "xxx",
"CertificatePrivateKey": "xxx",
"ResourceTypesRegions":resourceTypesRegions
});
}
this.logger.info(`请求参数:${JSON.stringify(params)}`);
params.CertificatePublicKey = this.cert.crt
params.CertificatePrivateKey = this.cert.key
deployRes = await sslClient.UploadUpdateCertificateInstance(params);
if (deployRes && deployRes.DeployRecordId>0){
this.logger.info(`任务创建成功,开始检查结果:${JSON.stringify(deployRes)}`);
break;
@@ -325,7 +332,7 @@ export class TencentRefreshCert extends AbstractTaskPlugin {
*/
const options = list.map((item: any) => {
return {
label: `${item.Alias}<${item.Domain}_${item.CertificateId}>`,
label: `${item.CertificateId}<${item.Domain}_${item.Alias}_${item.BoundResource.length}>`,
value: item.CertificateId,
domain: item.SubjectAltName,
};