mirror of
https://github.com/certd/certd.git
synced 2026-04-27 23:37:29 +08:00
perf: passkey 支持Bitwarden
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<div class="fs-user-info">{{ t("user.greeting") }},{{ userStore.getUserInfo?.nickName || userStore.getUserInfo?.username }}</div>
|
<div class="fs-user-info" @click="goUserProfile">{{ t("user.greeting") }},{{ userStore.getUserInfo?.nickName || userStore.getUserInfo?.username }}</div>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
<a-menu-item>
|
<a-menu-item>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ const avatar = computed(() => {
|
|||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await userStore.logout(true);
|
await userStore.logout(true);
|
||||||
}
|
}
|
||||||
|
function goUserProfile() {
|
||||||
|
router.push("/certd/mine/user-profile");
|
||||||
|
}
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ const projectStore = useProjectStore();
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #user-dropdown>
|
<template #user-dropdown>
|
||||||
<UserDropdown :avatar="avatar" :menus="menus" :text="userStore.userInfo?.nickName || userStore.userInfo?.username" description="" tag-text="" @logout="handleLogout" />
|
<UserDropdown :avatar="avatar" :menus="menus" :text="userStore.userInfo?.nickName || userStore.userInfo?.username" description="" tag-text="" @logout="handleLogout" @user-profile="goUserProfile" />
|
||||||
</template>
|
</template>
|
||||||
<template #lock-screen>
|
<template #lock-screen>
|
||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
|||||||
@@ -103,4 +103,6 @@ export default {
|
|||||||
deviceNameHelper: "Please enter the device name, used to identify the device",
|
deviceNameHelper: "Please enter the device name, used to identify the device",
|
||||||
passkeyRegisterHelper: "Site domain change will invalidate passkey",
|
passkeyRegisterHelper: "Site domain change will invalidate passkey",
|
||||||
userInfo: "User Info",
|
userInfo: "User Info",
|
||||||
|
securitySettingTip: "2FA Setting",
|
||||||
|
securitySetting: "2FA Setting",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,4 +105,6 @@ export default {
|
|||||||
deviceNameHelper: "请输入当前设备名称,绑定多个时好做区分",
|
deviceNameHelper: "请输入当前设备名称,绑定多个时好做区分",
|
||||||
passkeyRegisterHelper: "1、站点域名变更会导致passkey失效;\n2、同一设备同一个用户绑定多次只有最后一次的有效,之前绑定的会失效,需要手动删除",
|
passkeyRegisterHelper: "1、站点域名变更会导致passkey失效;\n2、同一设备同一个用户绑定多次只有最后一次的有效,之前绑定的会失效,需要手动删除",
|
||||||
userInfo: "账号信息",
|
userInfo: "账号信息",
|
||||||
|
securitySettingTip: "2FA设置",
|
||||||
|
securitySetting: "2FA设置",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
hoverDelay: 500,
|
hoverDelay: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{ logout: [] }>();
|
const emit = defineEmits<{ logout: []; userProfile: [] }>();
|
||||||
|
|
||||||
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } = usePreferences();
|
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } = usePreferences();
|
||||||
const lockStore = useLockStore();
|
const lockStore = useLockStore();
|
||||||
@@ -132,6 +132,11 @@ function handleSubmitLogout() {
|
|||||||
logoutModalApi.close();
|
logoutModalApi.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUserProfile() {
|
||||||
|
emit("userProfile");
|
||||||
|
openPopover.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (enableShortcutKey.value) {
|
if (enableShortcutKey.value) {
|
||||||
const keys = useMagicKeys();
|
const keys = useMagicKeys();
|
||||||
whenever(keys["Alt+KeyQ"]!, () => {
|
whenever(keys["Alt+KeyQ"]!, () => {
|
||||||
@@ -173,7 +178,7 @@ if (enableShortcutKey.value) {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
|
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
|
||||||
<div ref="refContent">
|
<div ref="refContent">
|
||||||
<DropdownMenuLabel class="flex items-center p-3">
|
<DropdownMenuLabel class="flex items-center p-3 pointer" @click="handleUserProfile">
|
||||||
<VbenAvatar :alt="text" :src="avatar" class="size-12" dot dot-class="bottom-0 right-1 border-2 size-4 bg-green-500" />
|
<VbenAvatar :alt="text" :src="avatar" class="size-12" dot dot-class="bottom-0 right-1 border-2 size-4 bg-green-500" />
|
||||||
<div class="ml-2 w-full">
|
<div class="ml-2 w-full">
|
||||||
<div v-if="tagText || text || $slots.tagText" class="text-foreground mb-1 flex items-center text-sm font-medium">
|
<div v-if="tagText || text || $slots.tagText" class="text-foreground mb-1 flex items-center text-sm font-medium">
|
||||||
|
|||||||
@@ -35,11 +35,15 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons gap-2">
|
||||||
<a-button type="primary" class="action-btn" @click="doUpdate">
|
<a-button type="primary" class="action-btn" @click="doUpdate">
|
||||||
{{ t("authentication.updateProfile") }}
|
{{ t("authentication.updateProfile") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<change-password-button class="ml-10" :show-button="true" />
|
<change-password-button :show-button="true" />
|
||||||
|
|
||||||
|
<a-button type="primary" class="action-btn" @click="goSecuritySetting">
|
||||||
|
{{ t("authentication.securitySettingTip") }}
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,6 +146,7 @@ import { useSettingStore } from "/@/store/settings";
|
|||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import { dict } from "@fast-crud/fast-crud";
|
import { dict } from "@fast-crud/fast-crud";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -175,6 +180,11 @@ function doUpdate() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
function goSecuritySetting() {
|
||||||
|
router.push("/certd/mine/security");
|
||||||
|
}
|
||||||
|
|
||||||
const oauthBounds = ref([]);
|
const oauthBounds = ref([]);
|
||||||
const oauthProviders = ref([]);
|
const oauthProviders = ref([]);
|
||||||
|
|
||||||
@@ -294,6 +304,12 @@ async function doRegisterPasskey(deviceName: string) {
|
|||||||
name: userInfo.value.username + "@" + deviceName,
|
name: userInfo.value.username + "@" + deviceName,
|
||||||
displayName: deviceName,
|
displayName: deviceName,
|
||||||
},
|
},
|
||||||
|
// 关键配置在这里 👇
|
||||||
|
authenticatorSelection: {
|
||||||
|
residentKey: "required", // 或 "preferred",请求创建可发现凭证
|
||||||
|
requireResidentKey: true, // 为兼容旧浏览器,设置与 residentKey 相同的值
|
||||||
|
userVerification: "preferred", // 用户验证策略
|
||||||
|
},
|
||||||
};
|
};
|
||||||
console.log("passkey register publicKey:", publicKey, JSON.stringify(publicKey));
|
console.log("passkey register publicKey:", publicKey, JSON.stringify(publicKey));
|
||||||
const credential = await (navigator.credentials as any).create({
|
const credential = await (navigator.credentials as any).create({
|
||||||
|
|||||||
@@ -231,6 +231,12 @@ const handlePasskeyLogin = async () => {
|
|||||||
// extensions: options.extensions,
|
// extensions: options.extensions,
|
||||||
// authenticatorSelection: options.authenticatorSelection,
|
// authenticatorSelection: options.authenticatorSelection,
|
||||||
// hints: options.hints,
|
// hints: options.hints,
|
||||||
|
// 关键配置在这里 👇
|
||||||
|
authenticatorSelection: {
|
||||||
|
residentKey: "required", // 或 "preferred",请求创建可发现凭证
|
||||||
|
requireResidentKey: true, // 为兼容旧浏览器,设置与 residentKey 相同的值
|
||||||
|
userVerification: "preferred", // 用户验证策略
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
|
|||||||
throw new AuthException("Passkey不存在");
|
throw new AuthException("Passkey不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verification.counter <= passkey.counter) {
|
if (verification.counter <= passkey.counter && passkey.counter > 0) {
|
||||||
throw new AuthException("认证失败:计数器异常");
|
throw new AuthException("认证失败:计数器异常");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user