refactor: restructure passkey with dedicated package

- Create internal/passkey/ package with challenge session management
- Add HTTP handler layer in internal/api/passkey.go
- Simplify Passkey model to use blob credential storage
- Register passkey routes in router
- Update frontend to match new API endpoints
- Add .env.example template
This commit is contained in:
Sakurasan
2026-09-01 22:34:44 +08:00
parent d28fca8ee6
commit 9733b3c20b
11 changed files with 546 additions and 268 deletions
+21 -23
View File
@@ -16,26 +16,21 @@ export const useWebAuthStore = defineStore("webauth", () => {
const loading = ref(false);
const error = ref<string | null>(null);
const addPasskey = async () => {
const addPasskey = async (name?: string) => {
error.value = "";
loading.value = true;
try {
// 1. 从后端获取注册选项 (Creation Options)
const res = await request.get("/profile/passkey");
// console.log("begin:", res.data.data.publicKey);
const options = res.data.data.publicKey;
const res = await request.post("/webauthn/register/begin", {});
const { creation, challenge } = res.data.data;
// 调用 Web Authentication API 进行注册
// const credential = await navigator.credentials.create(options);
// console.log("credential:", credential);
let attestation;
try {
// Pass 'undefined' as the second argument if you are not using an AbortSignal
attestation = await startRegistration({ optionsJSON: options });
// console.log("WebAuthn 注册结果 (Attestation):", JSON.stringify(attestation));
attestation = await startRegistration({ optionsJSON: creation });
error.value = null;
} catch (regError: any) {
// console.log("WebAuthn 注册失败或取消:", regError);
if (regError.name === "NotAllowedError") {
error.value = "Passkey 操作被取消或不允许。";
} else {
@@ -45,8 +40,11 @@ export const useWebAuthStore = defineStore("webauth", () => {
}
// 3. 将注册结果 (Attestation) 发送到后端进行验证和保存
const res2: AxiosResponse = await request.post("/profile/passkey", attestation);
// console.log("end:", res2);
const res2: AxiosResponse = await request.post("/webauthn/register/complete", {
challenge,
name: name || "passkey",
credential: attestation,
});
return res2;
} catch (err: any) {
error.value = err.response?.data?.error || "添加 Passkey 失败,请稍后重试。";
@@ -56,20 +54,18 @@ export const useWebAuthStore = defineStore("webauth", () => {
}
};
const loginPasskey = async () => {
const loginPasskey = async (username?: string) => {
error.value = null;
loading.value = true;
try {
// 1. 从后端获取登录选项 (Assertion Options)
const res = await request.get("/auth/passkey/begin");
// console.log("login begin:", res.data);
const options = res.data.data.publicKey;
const res = await request.post("/auth/passkey/begin", { username });
const { assertion, challenge, user_id } = res.data.data;
// 2. 调用 Web Authentication API 进行认证
let assertion;
let credential;
try {
assertion = await startAuthentication({ optionsJSON: options });
// console.log("WebAuthn 认证结果 (Assertion):", JSON.stringify(assertion));
credential = await startAuthentication({ optionsJSON: assertion });
} catch (loginError: any) {
if (loginError.name === "NotAllowedError") {
error.value = "Passkey 登录被取消或不允许。";
@@ -80,8 +76,11 @@ export const useWebAuthStore = defineStore("webauth", () => {
}
// 3. 将认证结果 (Assertion) 发送到后端进行验证并获取 Token
const challenge = options.challenge; // 从 begin 接口返回的 options 中获取 challenge
const res2: AxiosResponse = await request.post(`/auth/passkey/finish?challenge=${challenge}`, assertion);
const res2: AxiosResponse = await request.post("/auth/passkey/finish", {
challenge,
credential,
user_id,
});
// 4. 处理登录成功的响应,通常包含 Token
if (res2.status === 200 && !!res2.data.data?.token) {
@@ -103,8 +102,7 @@ export const useWebAuthStore = defineStore("webauth", () => {
loading.value = true;
error.value = null;
try {
const response = await request.get('/profile/passkeys')
// console.log('getPasskeys',response.data.data)
const response = await request.get('/webauthn/passkeys')
passkeys.value = response.data.data
} catch (err: any) {
error.value = err.response?.data?.error || '获取token列表失败';
@@ -118,7 +116,7 @@ export const useWebAuthStore = defineStore("webauth", () => {
loading.value = true;
error.value = null;
try {
const response: AxiosResponse = await request.delete(`/profile/passkeys/${id}`)
const response: AxiosResponse = await request.delete(`/webauthn/passkeys/${id}`)
return response
} catch (err: any) {
error.value = err.response?.data?.error || `删除passkey ${id} 失败`;
-4
View File
@@ -170,8 +170,6 @@
<tr class="text-xs uppercase tracking-wider text-base-content/50">
<th class="pl-4">Name</th>
<th>Create Time</th>
<th>Sign Count</th>
<th>Device</th>
<th class="pr-4 text-right"><span class="sr-only">Actions</span></th>
</tr>
</thead>
@@ -179,8 +177,6 @@
<tr v-for="passkey in passkeys" :key="passkey.id" class="border-base-300/40 hover:bg-base-200/50">
<td class="pl-4 font-medium">{{ passkey.name }}</td>
<td class="tabular-nums text-base-content/70">{{ formatDateTime(passkey.created_at) }}</td>
<td class="tabular-nums">{{ passkey.sign_count }}</td>
<td class="text-base-content/70">{{ passkey.device_type }}</td>
<td class="pr-4 text-right">
<button class="btn btn-ghost btn-xs btn-square text-error"
@click="confirmRmovePasskey(passkey)" aria-label="Delete passkey">