diff --git a/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts b/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts index 397c061fb..ce6c2bde9 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts @@ -101,4 +101,5 @@ export default { registerPasskey: "Register Passkey", deviceName: "Device Name", deviceNameHelper: "Please enter the device name, used to identify the device", + passkeyRegisterHelper: "Site domain change will invalidate passkey", }; diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts index 0c2ac6f71..ff24a1e3c 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts @@ -103,4 +103,5 @@ export default { registerPasskey: "注册Passkey", deviceName: "设备名称", deviceNameHelper: "请输入当前设备名称,绑定多个时好做区分", + passkeyRegisterHelper: "1、站点域名变更会导致passkey失效;\n2、同一设备同一个用户绑定多次只有最后一次的有效,之前绑定的会失效,需要手动删除", }; diff --git a/packages/ui/certd-client/src/store/user/api.user.ts b/packages/ui/certd-client/src/store/user/api.user.ts index 7de04cc65..e358c760c 100644 --- a/packages/ui/certd-client/src/store/user/api.user.ts +++ b/packages/ui/certd-client/src/store/user/api.user.ts @@ -115,23 +115,22 @@ export async function generatePasskeyRegistrationOptions() { }); } -export async function verifyPasskeyRegistration(userId: number, response: any, challenge: string) { +export async function verifyPasskeyRegistration(response: any, challenge: string) { return await request({ url: "/passkey/verifyRegistration", method: "post", - data: { userId, response, challenge }, + data: { response, challenge }, }); } -export async function generatePasskeyAuthenticationOptions(userId: number) { +export async function generatePasskeyAuthenticationOptions() { return await request({ url: "/passkey/generateAuthentication", method: "post", - data: { userId }, }); } -export async function loginByPasskey(form: { userId: number; credential: any; challenge: string }) { +export async function loginByPasskey(form: { credential: any; challenge: string }) { return await request({ url: "/loginByPasskey", method: "post", @@ -139,7 +138,7 @@ export async function loginByPasskey(form: { userId: number; credential: any; ch }); } -export async function registerPasskey(form: { userId: number; response: any; challenge: string }) { +export async function registerPasskey(form: { response: any; challenge: string }) { return await request({ url: "/passkey/register", method: "post", diff --git a/packages/ui/certd-client/src/style/common.less b/packages/ui/certd-client/src/style/common.less index a784bea43..fe0cc9c53 100644 --- a/packages/ui/certd-client/src/style/common.less +++ b/packages/ui/certd-client/src/style/common.less @@ -295,7 +295,7 @@ h6 { } .helper { - color: #aeaeae; + color: #8f8f8f; font-size: 12px; margin-top: 3px; margin-bottom: 3px; diff --git a/packages/ui/certd-client/src/views/certd/mine/api.ts b/packages/ui/certd-client/src/views/certd/mine/api.ts index 8798b5881..9ce8648ad 100644 --- a/packages/ui/certd-client/src/views/certd/mine/api.ts +++ b/packages/ui/certd-client/src/views/certd/mine/api.ts @@ -58,14 +58,14 @@ export async function OauthBoundUrl(type: string) { export async function GetPasskeys() { return await request({ - url: "/mine/passkeys", + url: "/mine/passkey/list", method: "POST", }); } export async function UnbindPasskey(id: number) { return await request({ - url: "/mine/unbindPasskey", + url: "/mine/passkey/unbind", method: "POST", data: { id }, }); @@ -110,39 +110,23 @@ export interface PasskeyCredential { export async function generatePasskeyRegistrationOptions() { return await request({ - url: "/passkey/generateRegistration", + url: "/mine/passkey/generateRegistration", method: "post", }); } -export async function verifyPasskeyRegistration(userId: number, response: any, challenge: string, deviceName: string) { +export async function verifyPasskeyRegistration(response: any, challenge: string, deviceName: string) { return await request({ - url: "/passkey/verifyRegistration", + url: "/mine/passkey/verifyRegistration", method: "post", - data: { userId, response, challenge, deviceName }, + data: { response, challenge, deviceName }, }); } -export async function generatePasskeyAuthenticationOptions(userId: number) { +export async function registerPasskey(response: any, challenge: string, deviceName: string) { return await request({ - url: "/passkey/generateAuthentication", + url: "/mine/passkey/register", method: "post", - data: { userId }, - }); -} - -export async function loginByPasskey(userId: number, credential: any, challenge: string) { - return await request({ - url: "/passkey/login", - method: "post", - data: { userId, credential, challenge }, - }); -} - -export async function registerPasskey(userId: number, response: any, challenge: string) { - return await request({ - url: "/passkey/register", - method: "post", - data: { userId, response, challenge }, + data: { response, challenge, deviceName }, }); } diff --git a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue index 807ac49cc..378249a17 100644 --- a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue +++ b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue @@ -33,7 +33,11 @@
{{ passkey.deviceName }} - {{ formatDate(passkey.registeredAt) }} + +
注册时间:{{ formatDate(passkey.registeredAt) }}
+
最后使用:{{ formatDate(passkey.updateTime) }}
+
+ 解绑
@@ -42,6 +46,9 @@
{{ t("authentication.passkeyNotSupported") }}
+
{{ t("authentication.passkeyRegisterHelper") }}
+          
{{ t("authentication.updateProfile") }} @@ -59,10 +66,11 @@ import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue import { useI18n } from "/src/locales"; import { useUserProfile } from "./use"; import { usePasskeyRegister } from "./use"; -import { message, Modal } from "ant-design-vue"; +import { message, Modal, notification } from "ant-design-vue"; import { useSettingStore } from "/@/store/settings"; import { isEmpty } from "lodash-es"; import { dict } from "@fast-crud/fast-crud"; +import dayjs from "dayjs"; const { t } = useI18n(); @@ -190,9 +198,15 @@ async function doRegisterPasskey(deviceName: string) { const res: any = await api.generatePasskeyRegistrationOptions(); const options = res; - navigator.credentials.query({ - publicKey: options, - }); + // navigator.credentials.query({ + // publicKey: options, + // }); + + // const excludeCredentials = passkeys.value.map(item => ({ + // id: new TextEncoder().encode(item.passkeyId), + // type: "public-key", + // })); + const credential = await (navigator.credentials as any).create({ publicKey: { challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)), @@ -200,9 +214,9 @@ async function doRegisterPasskey(deviceName: string) { pubKeyCredParams: options.pubKeyCredParams, timeout: options.timeout || 60000, attestation: options.attestation, - excludeCredentials: options.excludeCredentials || [], + // excludeCredentials: excludeCredentials, user: { - id: Uint8Array.from([res.userId]), + id: new TextEncoder().encode(options.userId + ""), name: userInfo.value.username, displayName: deviceName, }, @@ -222,18 +236,20 @@ async function doRegisterPasskey(deviceName: string) { clientDataJSON: toBase64Url(credential.response.clientDataJSON), }, }; + console.log("credential", credential, response); + debugger; - const verifyRes: any = await api.verifyPasskeyRegistration(userInfo.value.id, response, options.challenge, deviceName); + const verifyRes: any = await api.verifyPasskeyRegistration(response, options.challenge, deviceName); await loadPasskeys(); } catch (e: any) { console.error("Passkey注册失败:", e); - Modal.error({ title: "错误", content: e.message || "Passkey注册失败" }); + notification.error({ message: "错误", description: e.message || "Passkey注册失败" }); } } const formatDate = (dateString: string) => { if (!dateString) return ""; - return new Date(dateString).toLocaleString("zh-CN"); + return dayjs(dateString).format("YYYY-MM-DD HH:mm:ss"); }; const checkPasskeySupport = () => { diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue index f12aea711..89b4918e0 100644 --- a/packages/ui/certd-client/src/views/framework/login/index.vue +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -233,7 +233,6 @@ const handlePasskeyLogin = async () => { } const loginRes: any = await UserApi.loginByPasskey({ - userId: optionsResponse.userId, credential, challenge: options.challenge, }); diff --git a/packages/ui/certd-server/src/controller/basic/login/login-controller.ts b/packages/ui/certd-server/src/controller/basic/login/login-controller.ts index 13ffb56b2..4bf11b43d 100644 --- a/packages/ui/certd-server/src/controller/basic/login/login-controller.ts +++ b/packages/ui/certd-server/src/controller/basic/login/login-controller.ts @@ -4,6 +4,7 @@ import { AddonService, BaseController, Constants, SysPublicSettings, SysSettings import { CodeService } from "../../../modules/basic/service/code-service.js"; import { checkComm } from "@certd/plus-core"; import { CaptchaService } from "../../../modules/basic/service/captcha-service.js"; +import { PasskeyService } from "../../../modules/login/service/passkey-service.js"; /** */ @@ -23,6 +24,10 @@ export class LoginController extends BaseController { @Inject() captchaService: CaptchaService; + @Inject() + passkeyService: PasskeyService; + + @Post('/login', { summary: Constants.per.guest }) public async login( @Body(ALL) @@ -81,22 +86,36 @@ export class LoginController extends BaseController { return this.ok(token); } - @Post('/loginByPasskey', { summary: Constants.per.guest }) - public async loginByPasskey( - @Body(ALL) - body: any - ) { - const credential = body.credential; - const challenge = body.challenge; + + - const token = await this.loginService.loginByPasskey({ - credential, - challenge, - }, this.ctx); - - // this.writeTokenCookie(token); - return this.ok(token); - } + @Post('/passkey/generateAuthentication', { summary: Constants.per.guest }) + public async generateAuthentication() { + const options = await this.passkeyService.generateAuthenticationOptions( + this.ctx + ); + + return this.ok({ + ...options, + }); + } + + @Post('/loginByPasskey', { summary: Constants.per.guest }) + public async loginByPasskey( + @Body(ALL) + body: any + ) { + const credential = body.credential; + const challenge = body.challenge; + + const token = await this.loginService.loginByPasskey({ + credential, + challenge, + }, this.ctx); + + // this.writeTokenCookie(token); + return this.ok(token); + } @Post('/logout', { summary: Constants.per.authOnly }) public logout() { diff --git a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts index 9a2c86eed..35cf3d7ef 100644 --- a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts +++ b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts @@ -50,30 +50,4 @@ export class MineController extends BaseController { }); return this.ok({}); } - - @Post('/passkeys', { summary: Constants.per.authOnly }) - public async getPasskeys() { - const userId = this.getUserId(); - const passkeys = await this.passkeyService.find({ - select: ['id', 'deviceName', 'registeredAt'], - where: { userId }}); - return this.ok(passkeys); - } - - @Post('/unbindPasskey', { summary: Constants.per.authOnly }) - public async unbindPasskey(@Body(ALL) body: any) { - const userId = this.getUserId(); - const passkeyId = body.id; - - const passkey = await this.passkeyService.findOne({ - where: { id: passkeyId, userId }, - }); - - if (!passkey) { - throw new Error('Passkey不存在'); - } - - await this.passkeyService.delete([passkey.id]); - return this.ok({}); - } } diff --git a/packages/ui/certd-server/src/controller/basic/login/passkey-controller.ts b/packages/ui/certd-server/src/controller/user/mine/passkey-controller.ts similarity index 61% rename from packages/ui/certd-server/src/controller/basic/login/passkey-controller.ts rename to packages/ui/certd-server/src/controller/user/mine/passkey-controller.ts index eef51f7eb..7ff49ab55 100644 --- a/packages/ui/certd-server/src/controller/basic/login/passkey-controller.ts +++ b/packages/ui/certd-server/src/controller/user/mine/passkey-controller.ts @@ -4,8 +4,8 @@ import { BaseController, Constants } from "@certd/lib-server"; import { UserService } from "../../../modules/sys/authority/service/user-service.js"; @Provide() -@Controller('/api/passkey') -export class PasskeyController extends BaseController { +@Controller('/api/mine/passkey') +export class MinePasskeyController extends BaseController { @Inject() passkeyService: PasskeyService; @@ -39,12 +39,12 @@ export class PasskeyController extends BaseController { }); } - @Post('/verifyRegistration', { summary: Constants.per.guest }) + @Post('/verifyRegistration', { summary: Constants.per.authOnly }) public async verifyRegistration( @Body(ALL) body: any ) { - const userId = body.userId; + const userId = this.getUserId() const response = body.response; const challenge = body.challenge; const deviceName = body.deviceName; @@ -60,28 +60,14 @@ export class PasskeyController extends BaseController { return this.ok(result); } - @Post('/generateAuthentication', { summary: Constants.per.guest }) - public async generateAuthentication( - @Body(ALL) - body: any - ) { - const options = await this.passkeyService.generateAuthenticationOptions( - this.ctx - ); + - return this.ok({ - ...options, - }); - } - - - - @Post('/register', { summary: Constants.per.guest }) + @Post('/register', { summary: Constants.per.authOnly }) public async registerPasskey( @Body(ALL) body: any ) { - const userId = body.userId; + const userId = this.getUserId(); const response = body.response; const deviceName = body.deviceName; const challenge = body.challenge; @@ -96,4 +82,34 @@ export class PasskeyController extends BaseController { return this.ok(result); } + + + @Post('/list', { summary: Constants.per.authOnly }) + public async getPasskeys() { + const userId = this.getUserId(); + const passkeys = await this.passkeyService.find({ + select: ['id', 'deviceName', 'registeredAt', 'transports', 'passkeyId' ,'updateTime'], + where: { userId }, + order: { registeredAt: 'DESC' }, + }); + return this.ok(passkeys); + } + + @Post('/unbind', { summary: Constants.per.authOnly }) + public async unbindPasskey(@Body(ALL) body: any) { + const userId = this.getUserId(); + const passkeyId = body.id; + + const passkey = await this.passkeyService.findOne({ + where: { id: passkeyId, userId }, + }); + + if (!passkey) { + throw new Error('Passkey不存在'); + } + + await this.passkeyService.delete([passkey.id]); + return this.ok({}); + } + } diff --git a/packages/ui/certd-server/src/modules/login/service/passkey-service.ts b/packages/ui/certd-server/src/modules/login/service/passkey-service.ts index 3a8992bfc..e949f3ffb 100644 --- a/packages/ui/certd-server/src/modules/login/service/passkey-service.ts +++ b/packages/ui/certd-server/src/modules/login/service/passkey-service.ts @@ -1,10 +1,11 @@ import { cache } from "@certd/basic"; -import { AuthException, BaseService } from "@certd/lib-server"; +import { AuthException, BaseService, SysSettingsService, SysSiteInfo } from "@certd/lib-server"; +import { isComm } from "@certd/plus-core"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; import { UserService } from "../../sys/authority/service/user-service.js"; import { PasskeyEntity } from "../entity/passkey.js"; -import { Repository } from "typeorm"; -import { InjectEntityModel } from "@midwayjs/typeorm"; @Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) @@ -16,15 +17,31 @@ export class PasskeyService extends BaseService { @InjectEntityModel(PasskeyEntity) repository: Repository; + @Inject() + sysSettingsService: SysSettingsService; + getRepository(): Repository { return this.repository; } + + async getRpInfo(){ + let rpName = "Certd" + if(isComm()){ + const siteInfo = await this.sysSettingsService.getSetting(SysSiteInfo); + rpName = siteInfo.title || rpName; + } + return { + rpName, + } + } async generateRegistrationOptions(userId: number, username: string, remoteIp: string, ctx: any) { const { generateRegistrationOptions } = await import("@simplewebauthn/server"); const user = await this.userService.info(userId); + const {rpName} = await this.getRpInfo(); + const options = await generateRegistrationOptions({ - rpName: "Certd", + rpName: rpName, rpID: this.getRpId(ctx), userID: new Uint8Array([userId]), userName: username, @@ -184,6 +201,7 @@ export class PasskeyService extends BaseService { } passkey.counter = verification.counter; + passkey.updateTime = new Date(); await this.repository.save(passkey); const user = await this.userService.info(passkey.userId);