mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +08:00
perf: passkey登录放到下方其他登录位置
This commit is contained in:
@@ -46,21 +46,11 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane v-if="settingStore.sysPublic.passkeyEnabled && settingStore.isPlus" key="passkey" :tab="t('authentication.passkeyTab')">
|
|
||||||
<template v-if="formState.loginType === 'passkey'">
|
|
||||||
<div v-if="!passkeySupported" class="text-red-500 text-sm mt-2 text-center mb-10">
|
|
||||||
{{ t("authentication.passkeyNotSupported") }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button v-if="formState.loginType !== 'passkey'" type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
|
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
|
||||||
{{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }}
|
{{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-else type="primary" size="large" html-type="button" :loading="loading" class="login-button" :disabled="!passkeySupported" @click="handlePasskeyLogin">
|
|
||||||
{{ t("authentication.passkeyLogin") }}
|
|
||||||
</a-button>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<div class="mt-2 flex justify-between items-center">
|
<div class="mt-2 flex justify-between items-center">
|
||||||
@@ -81,7 +71,7 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="!queryBindCode && settingStore.sysPublic.oauthEnabled && settingStore.isPlus" class="w-full">
|
<div v-if="!queryBindCode && (settingStore.sysPublic.oauthEnabled || settingStore.sysPublic.passkeyEnabled) && settingStore.isPlus" class="w-full">
|
||||||
<oauth-footer :oauth-only="isOauthOnly"></oauth-footer>
|
<oauth-footer :oauth-only="isOauthOnly"></oauth-footer>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
@@ -195,70 +185,6 @@ const twoFactor = reactive({
|
|||||||
verifyCode: "",
|
verifyCode: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const passkeySupported = ref(false);
|
|
||||||
const passkeyEnabled = ref(false);
|
|
||||||
|
|
||||||
const checkPasskeySupport = () => {
|
|
||||||
passkeySupported.value = false;
|
|
||||||
if (typeof window !== "undefined" && "credentials" in navigator && "PublicKeyCredential" in window) {
|
|
||||||
passkeySupported.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasskeyLogin = async () => {
|
|
||||||
if (!passkeySupported.value) {
|
|
||||||
notification.error({ message: t("authentication.passkeyNotSupported") });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const optionsResponse: any = await request({
|
|
||||||
url: "/passkey/generateAuthentication",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
const options = optionsResponse;
|
|
||||||
|
|
||||||
console.log("passkey authentication options:", options, JSON.stringify(options));
|
|
||||||
const credential = await (navigator.credentials as any).get({
|
|
||||||
publicKey: {
|
|
||||||
challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)),
|
|
||||||
rpId: options.rpId,
|
|
||||||
allowCredentials: options.allowCredentials || [],
|
|
||||||
timeout: options.timeout || 60000,
|
|
||||||
// attestation: options.attestation,
|
|
||||||
// excludeCredentials: excludeCredentials,
|
|
||||||
// extensions: options.extensions,
|
|
||||||
// authenticatorSelection: options.authenticatorSelection,
|
|
||||||
// hints: options.hints,
|
|
||||||
// 关键配置在这里 👇
|
|
||||||
authenticatorSelection: {
|
|
||||||
residentKey: "required", // 或 "preferred",请求创建可发现凭证
|
|
||||||
requireResidentKey: true, // 为兼容旧浏览器,设置与 residentKey 相同的值
|
|
||||||
userVerification: "preferred", // 用户验证策略
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("passkey authentication credential:", credential, JSON.stringify(credential));
|
|
||||||
if (!credential) {
|
|
||||||
throw new Error("Passkey认证失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginRes: any = await UserApi.loginByPasskey({
|
|
||||||
credential,
|
|
||||||
challenge: options.challenge,
|
|
||||||
});
|
|
||||||
|
|
||||||
await userStore.onLoginSuccess(loginRes);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Passkey登录失败:", e);
|
|
||||||
notification.error({ message: e.message || "Passkey登录失败" });
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async () => {
|
const handleFinish = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -307,9 +233,7 @@ const isOauthOnly = computed(() => {
|
|||||||
return sysPublicSettings.oauthOnly && settingStore.isPlus && sysPublicSettings.oauthEnabled;
|
return sysPublicSettings.oauthOnly && settingStore.isPlus && sysPublicSettings.oauthEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {});
|
||||||
checkPasskeySupport();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="passkeyEnabled && isPlus" class="oauth-icon-button" :class="{ pointer: passkeySupported }" @click="handlePasskeyLogin">
|
||||||
|
<div><fs-icon icon="ion:finger-print-outline" :class="{ 'text-blue-600': passkeySupported, 'text-gray-400': !passkeySupported }" class="text-40" /></div>
|
||||||
|
<div class="ellipsis title" :title="t('authentication.passkeyLogin')" :class="{ 'text-gray-400': !passkeySupported }">{{ t("authentication.passkeyLogin") }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useI18n } from "/@/locales";
|
||||||
|
import { useSettingStore } from "/@/store/settings";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import { request } from "/src/api/service";
|
||||||
|
import * as UserApi from "/src/store/user/api.user";
|
||||||
|
import { useUserStore } from "/src/store/user";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const passkeySupported = ref(false);
|
||||||
|
|
||||||
|
const passkeyEnabled = computed(() => settingStore.sysPublic.passkeyEnabled);
|
||||||
|
const isPlus = computed(() => settingStore.isPlus);
|
||||||
|
|
||||||
|
const checkPasskeySupport = () => {
|
||||||
|
passkeySupported.value = false;
|
||||||
|
if (typeof window !== "undefined" && "credentials" in navigator && "PublicKeyCredential" in window) {
|
||||||
|
passkeySupported.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasskeyLogin = async () => {
|
||||||
|
if (!passkeySupported.value) {
|
||||||
|
notification.error({ message: t("authentication.passkeyNotSupported") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const optionsResponse: any = await request({
|
||||||
|
url: "/passkey/generateAuthentication",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
const options = optionsResponse;
|
||||||
|
|
||||||
|
console.log("passkey authentication options:", options, JSON.stringify(options));
|
||||||
|
const credential = await (navigator.credentials as any).get({
|
||||||
|
publicKey: {
|
||||||
|
challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)),
|
||||||
|
rpId: options.rpId,
|
||||||
|
allowCredentials: options.allowCredentials || [],
|
||||||
|
timeout: options.timeout || 60000,
|
||||||
|
authenticatorSelection: {
|
||||||
|
residentKey: "required",
|
||||||
|
requireResidentKey: true,
|
||||||
|
userVerification: "preferred",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("passkey authentication credential:", credential, JSON.stringify(credential));
|
||||||
|
if (!credential) {
|
||||||
|
throw new Error("Passkey认证失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginRes: any = await UserApi.loginByPasskey({
|
||||||
|
credential,
|
||||||
|
challenge: options.challenge,
|
||||||
|
});
|
||||||
|
|
||||||
|
await userStore.onLoginSuccess(loginRes);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Passkey登录失败:", e);
|
||||||
|
notification.error({ message: e.message || "Passkey登录失败" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkPasskeySupport();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="oauth-title-text">{{ computedTitle }}</div>
|
<div class="oauth-title-text">{{ computedTitle }}</div>
|
||||||
</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>
|
||||||
<template v-for="item in oauthProviderList" :key="item.type">
|
<template v-for="item in oauthProviderList" :key="item.type">
|
||||||
<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.name)">
|
||||||
<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>
|
||||||
@@ -19,6 +20,7 @@ import * as api from "./api";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
import PasskeyLogin from "../login/passkey-login.vue";
|
||||||
|
|
||||||
const oauthProviderList = ref([]);
|
const oauthProviderList = ref([]);
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -190,8 +190,10 @@ export class PipelineController extends CrudController<PipelineService> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('/update', { description: Constants.per.authOnly })
|
@Post('/update', { description: Constants.per.authOnly })
|
||||||
async update(@Body(ALL) bean) {
|
async update(@Body(ALL) bean:PipelineEntity) {
|
||||||
return await this.save(bean);
|
await this.checkOwner(this.getService(), bean.id,"write",true);
|
||||||
|
await this.service.update(bean as any);
|
||||||
|
return this.ok({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/save', { description: Constants.per.authOnly, summary: '新增/更新流水线' })
|
@Post('/save', { description: Constants.per.authOnly, summary: '新增/更新流水线' })
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
bean.order = old.order;
|
bean.order = old.order;
|
||||||
bean.userId = old.userId;
|
bean.userId = old.userId;
|
||||||
bean.projectId = old.projectId;
|
bean.projectId = old.projectId;
|
||||||
|
if (bean.content == null) {
|
||||||
|
bean.content = old.content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!old || !old.webhookKey) {
|
if (!old || !old.webhookKey) {
|
||||||
bean.webhookKey = await this.genWebhookKey();
|
bean.webhookKey = await this.genWebhookKey();
|
||||||
|
|||||||
Reference in New Issue
Block a user