[add] Signature model, page & back-end API#55
Conversation
[optimize] upgrade to ESLint 10 & other latest Upstream packages
📝 WalkthroughWalkthrough在 Web Crypto 基础上新增通用数字签名功能:包含客户端 MobX 签名模型(ECDSA P-384)、服务端签名验证 API 路由,以及支持 iframe 嵌入的签名页面与服务端渲染接入。(不含主观评价) Changes
Sequence Diagram(s)sequenceDiagram
participant User as 用户
participant Page as SignaturePage
participant Model as SignatureModel
participant WebCrypto as Web Crypto API
participant API as /api/signature/... (服务端)
User->>Page: 访问签名页或提交 value
Page->>Model: 初始化或读取 signatureStore
alt 无公钥
Page->>Model: makeKeyPair()
Model->>WebCrypto: generateKey(ECDSA P-384)
WebCrypto-->>Model: privateKey, publicKey
Model->>Model: export publicKey (JWK → base64)
Model-->>Page: 返回 publicKey
Page->>Page: 构建 iframe linkData(包含 publicKey)
end
alt 需要签名
Page->>Model: sign(value)
Model->>Model: 检查 signatureMap 缓存
alt 缓存未命中
Model->>Model: 确保有 keyPair(调用 makeKeyPair)
Model->>WebCrypto: sign(privateKey, encoded value)
WebCrypto-->>Model: signature (ArrayBuffer)
Model->>Model: buffer2hex → 存入 signatureMap
end
Model-->>Page: 返回 signature (hex)
Page->>Page: 更新 iframe linkData(publicKey + signature)
end
Page->>API: POST /verification { algorithm, publicKey, value, signature }
API->>WebCrypto: importKey(publicKey), verify(signature, value)
WebCrypto-->>API: boolean
alt 验证成功
API-->>Page: 200 OK
else 验证失败
API-->>Page: 400 Bad Request
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@models/Signature.ts`:
- Around line 43-63: The nested `@toggle`('uploading') decorators on sign and
makeKeyPair cause the uploading flag to be toggled twice; update the
implementation to avoid this by either giving the two methods distinct toggle
keys (e.g., change one decorator to `@toggle`('keygen') on makeKeyPair) or have
sign call an undecorated internal key-generation helper (createKeyPairInternal)
instead of await this.makeKeyPair(); modify the methods sign and makeKeyPair
(and add the internal helper if choosing the second option) so the uploading
state is not unintentionally cleared by the inner call.
- Around line 12-14: The privateKey property is non-serializable (CryptoKey) so
persist current JWK instead of the CryptoKey: change persistence to store a
string JWK (exported via crypto.subtle.exportKey('jwk', this.privateKey)) and on
load/import rehydrate into a CryptoKey using crypto.subtle.importKey; update the
Signature model to stop persisting CryptoKey directly (replace `@persist`() on
privateKey with a persisted string field like privateKeyJwk or switch the
`@persist` target) and ensure makeKeyPair() and sign() use the re-imported
CryptoKey (call importKey after load and before sign) so sign() no longer
assumes this.privateKey is valid after page refresh.
In `@pages/api/signature/`[...slug].ts:
- Around line 7-20: Wrap the verification handler registered in
router.post('/verification') in a try/catch (or invoke the existing safeAPI
wrapper used in pages/api/core.ts) to prevent unhandled exceptions from
JSON.parse(atob(...)), Buffer.from(..., 'hex'), crypto.subtle.importKey or
crypto.subtle.verify from leaking internals; on catch return a sanitized error
response (e.g., 400 for invalid inputs or 500 for internal error) and log the
error via the same logger used elsewhere, validating presence of
algorithm/publicKey/signature before calling JSON.parse/atob and returning a
clear client error when inputs are malformed.
- Line 18: The response status uses context.status = verified ? 200 : 400 but
the API must return 204 on successful verification per Issue `#54`; update the
assignment so that when verified is truthy it sets context.status to 204 instead
of 200 (leave the failure branch as 400) — change the line that sets
context.status (the one referencing verified) accordingly.
In `@pages/signature.tsx`:
- Around line 23-35: linkData currently uses valueName, algorithmName,
publicKeyName, signatureName directly from this.observedProps.route.query and
concatenates '' which produces keys like "undefined" when those query names are
missing; update the linkData getter to validate each query key (valueName,
algorithmName, publicKeyName, signatureName) before adding to the object passed
to buildURLData — either skip entries whose names are falsy or map them to an
empty string, and ensure the signature lookup
this.signatureStore.signatureMap[value + ''] is only used if signatureName is
defined; this change should be made inside the linkData getter and affects the
object passed to buildURLData so no invalid "undefined" parameter names are
produced.
- Line 27: The fallback for signatureStore.signatureMap is currently an object
({}), but signatureMap stores hex-encoded signature strings so using {} causes
buildURLData to serialize to "[object Object]"; update the expression in
pages/signature.tsx (the line assigning signature from
this.signatureStore.signatureMap[value + '']) to use an empty string '' as the
fallback so signature is always a string and won't pollute the URL.
- Line 56: The iframe src is set directly from the user-controlled iframeLink
(and linkData); validate and constrain it before use: parse iframeLink with the
URL constructor, enforce it matches an allowlist of domains or a specific
prefix, and reject or fall back to a safe URL if validation fails (update the
logic that reads route.query into iframeLink/linkData). Also harden the iframe
element (where it renders the src) by adding a restrictive sandbox attribute
(e.g., disallow top-level navigation and remove unneeded privileges) and other
safe attributes (like referrerPolicy) to minimize capabilities even for allowed
hosts.
- Around line 37-42: componentDidMount currently reads this.props.route.query
while other lifecycle code uses this.observedProps.route, causing inconsistent
MobX reactivity; change the access in componentDidMount to use
this.observedProps.route.query instead and keep all route usage consistent (e.g.
in componentDidMount replace this.props.route.query with
this.observedProps.route.query) so signatureStore.makeKeyPair and
signatureStore.sign are triggered reactively.
| get linkData() { | ||
| const { route } = this.observedProps; | ||
| const { valueName, algorithmName, publicKeyName, signatureName, value } = route.query, | ||
| { algorithm, publicKey } = this.signatureStore; | ||
| const signature = this.signatureStore.signatureMap[value + ''] || {}; | ||
|
|
||
| return buildURLData({ | ||
| [valueName + '']: value, | ||
| [algorithmName + '']: btoa(JSON.stringify(algorithm)), | ||
| [publicKeyName + '']: publicKey, | ||
| [signatureName + '']: signature, | ||
| }); | ||
| } |
There was a problem hiding this comment.
当查询参数缺失时,linkData 会生成无意义的键名
valueName、algorithmName、publicKeyName、signatureName 均可能为 undefined,+ '' 强转后得到 "undefined" 作为 URL 参数名。建议对 iframeLink 及这些映射名参数做存在性检查,在参数缺失时返回空字符串或跳过构建。
🤖 Prompt for AI Agents
In `@pages/signature.tsx` around lines 23 - 35, linkData currently uses valueName,
algorithmName, publicKeyName, signatureName directly from
this.observedProps.route.query and concatenates '' which produces keys like
"undefined" when those query names are missing; update the linkData getter to
validate each query key (valueName, algorithmName, publicKeyName, signatureName)
before adding to the object passed to buildURLData — either skip entries whose
names are falsy or map them to an empty string, and ensure the signature lookup
this.signatureStore.signatureMap[value + ''] is only used if signatureName is
defined; this change should be made inside the linkData getter and affects the
object passed to buildURLData so no invalid "undefined" parameter names are
produced.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@pages/signature.tsx`:
- Around line 44-72: The render builds link = `${iframeLink}?${this.linkData}`
and always renders the anchor and iframe even when iframeLink is undefined;
change render() (pages/signature.tsx) to only compute/link and render the <a>
and <iframe> when iframeLink is truthy: move the link construction (using
iframeLink and this.linkData) inside a conditional and conditionally render the
<pre>...<a> and the <iframe> elements; otherwise render just the title/markdown
info (so functions/props to edit: render, the iframeLink variable from
this.props.route.query, and the link variable).
🧹 Nitpick comments (2)
pages/signature.tsx (2)
68-72: iframe 缺少title属性(无障碍访问)。
<iframe>元素应提供title属性以满足 WCAG 可访问性要求,帮助屏幕阅读器用户理解 iframe 内容的用途。♻️ 建议补充
<iframe className="border-0 w-100 vh-100" sandbox="allow-scripts allow-same-origin allow-forms" src={link} + title={title} />
31-31: 用web-utility的encodeBase64()替换btoa(),保持编码一致性。项目中已在
pages/policy/[...slug].tsx和pages/recipe/[...slug].tsx使用decodeBase64fromweb-utility进行解码。为保持代码一致性和规范统一,此处编码也应采用 web-utility 提供的 Base64 工具函数,而非原生btoa()。这样可避免 Node.js/SSR 环境下的兼容性风险,并强化项目的依赖统一。
| render() { | ||
| const { t } = this.observedContext, | ||
| { value, iframeLink } = this.props.route.query; | ||
|
|
||
| const title = value ? textJoin(t('sign'), value + '') : t('generate_key_pair'), | ||
| link = `${iframeLink}?${this.linkData}`; | ||
|
|
||
| return ( | ||
| <Container> | ||
| <PageHead title={title} /> | ||
|
|
||
| <h1 className="my-5 text-truncate">{title}</h1> | ||
|
|
||
| <section className="markdown-body bg-white py-4"> | ||
| <blockquote>{t('signature_disclaimer')}</blockquote> | ||
| <pre> | ||
| <code> | ||
| <a href={link} target="_blank" rel="noopener noreferrer"> | ||
| {link} | ||
| </a> | ||
| </code> | ||
| </pre> | ||
| </section> | ||
|
|
||
| <iframe | ||
| className="border-0 w-100 vh-100" | ||
| sandbox="allow-scripts allow-same-origin allow-forms" | ||
| src={link} | ||
| /> |
There was a problem hiding this comment.
当 iframeLink 缺失时,页面渲染出无效的 URL 和 iframe。
如果用户直接访问 /signature 而不带 iframeLink 查询参数(例如仅生成密钥对的场景),Line 49 会拼出 "undefined?..." 作为 link,导致:
<a href="undefined?...">指向无效地址<iframe src="undefined?...">加载失败
应在 iframeLink 存在时才渲染链接和 iframe 部分,否则可只展示签名/密钥对的结果信息。
🐛 建议修复:条件渲染 iframe 区域
render() {
const { t } = this.observedContext,
{ value, iframeLink } = this.props.route.query;
const title = value ? textJoin(t('sign'), value + '') : t('generate_key_pair'),
- link = `${iframeLink}?${this.linkData}`;
+ link = iframeLink ? `${iframeLink}?${this.linkData}` : '';
return (
<Container>
<PageHead title={title} />
<h1 className="my-5 text-truncate">{title}</h1>
<section className="markdown-body bg-white py-4">
<blockquote>{t('signature_disclaimer')}</blockquote>
- <pre>
- <code>
- <a href={link} target="_blank" rel="noopener noreferrer">
- {link}
- </a>
- </code>
- </pre>
+ {link && (
+ <pre>
+ <code>
+ <a href={link} target="_blank" rel="noopener noreferrer">
+ {link}
+ </a>
+ </code>
+ </pre>
+ )}
</section>
- <iframe
- className="border-0 w-100 vh-100"
- sandbox="allow-scripts allow-same-origin allow-forms"
- src={link}
- />
+ {link && (
+ <iframe
+ className="border-0 w-100 vh-100"
+ sandbox="allow-scripts allow-same-origin allow-forms"
+ src={link}
+ />
+ )}
</Container>
);
}🤖 Prompt for AI Agents
In `@pages/signature.tsx` around lines 44 - 72, The render builds link =
`${iframeLink}?${this.linkData}` and always renders the anchor and iframe even
when iframeLink is undefined; change render() (pages/signature.tsx) to only
compute/link and render the <a> and <iframe> when iframeLink is truthy: move the
link construction (using iframeLink and this.linkData) inside a conditional and
conditionally render the <pre>...<a> and the <iframe> elements; otherwise render
just the title/markdown info (so functions/props to edit: render, the iframeLink
variable from this.props.route.query, and the link variable).
Summary by CodeRabbit