Skip to content

[add] Signature model, page & back-end API#55

Merged
TechQuery merged 3 commits intomainfrom
TechQuery/issue54
Feb 15, 2026
Merged

[add] Signature model, page & back-end API#55
TechQuery merged 3 commits intomainfrom
TechQuery/issue54

Conversation

@TechQuery
Copy link
Member

@TechQuery TechQuery commented Feb 15, 2026

PR-55 PR-55 PR-55 Powered by Pull Request Badge

  1. [add] Signature model, page & back-end API (resolve 开发通用【数字签名】服务 #54)
  2. [optimize] upgrade to ESLint 10 & other latest Upstream packages

Summary by CodeRabbit

  • 新功能
    • 客户端新增数字签名能力:在浏览器中生成并管理密钥对,对任意文本进行签名,签名以十六进制形式缓存并复用。
    • 新增服务端签名验证接口:接收公钥/算法/原文/签名并返回验证结果,用于远程核验签名有效性。
    • 新增签名管理页面:支持生成密钥、触发签名流程并通过嵌入链接/iframe 预览签名结果和验证流程。

[optimize] upgrade to ESLint 10 & other latest Upstream packages
@dosubot
Copy link

dosubot bot commented Feb 15, 2026

Related Documentation

Checked 10 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

在 Web Crypto 基础上新增通用数字签名功能:包含客户端 MobX 签名模型(ECDSA P-384)、服务端签名验证 API 路由,以及支持 iframe 嵌入的签名页面与服务端渲染接入。(不含主观评价)

Changes

Cohort / File(s) Summary
签名模型与加密工具
models/Signature.ts
新增 SignatureModel(继承 BaseModel),使用 MobX + mobx-restful 持久化 privateKeypublicKeysignatureMap,实现 makeKeyPair()sign(value)(均由 @toggle('uploading') 装饰)。新增 buffer2hex() 工具。注意检查 CryptoKey 序列化/恢复、MobX 装饰器(@persist/@observable/@computed/@action)与类型声明、以及与 web-utility 的 base64/JWK 约定。
服务端验证路由
pages/api/signature/[...slug].ts
新增 Next.js API 路由,禁用 bodyParser,使用 Koa 路由包装(next-ssr-middleware)。实现 POST /verification:解码 base64/JWK 公钥与 hex 签名,调用 Web Crypto importKey/verify,按验证结果返回 200 或 400。注意 Content-Type、错误处理与解码对称性、以及生产环境下的安全与日志策略。
签名页面与客户端集成
pages/signature.tsx
新增页面 pages/signature.tsx,基于 MobX(ObservedComponent)管理 signatureStore,导出 getServerSideProps = compose(router)。页面在挂载时根据路由参数触发密钥生成或签名,并通过计算的 linkData 构建 iframe src 并渲染 iframe 与可点击链接。注意:确保所有 UI 文本使用 t()、统一使用 React Bootstrap 组件、MobX observer/observable/action/computed 使用正确、避免不必要渲染与确保 TypeScript 强类型。

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

密钥生于浏览器里,公钥随 iframe 行,
签名藏于 map 中,后端一验真与轻。
MobX 管理态,t() 将文案呈,
ECDSA 在手,匿名投票更安心 ✨

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题清晰准确地总结了主要变更:添加签名模型、页面和后端API,与代码变更完全对应。
Linked Issues check ✅ Passed 代码变更基本满足Issue #54要求:实现了密钥对生成与签名页(支持iframe嵌入),包含签名验证API(返回HTTP 200/400)。
Out of Scope Changes check ✅ Passed 所有变更直接与Issue #54的数字签名服务实现相关,无明显的超出范围的更改。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch TechQuery/issue54

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@TechQuery TechQuery added the feature New feature or request label Feb 15, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 23 to 35
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,
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

当查询参数缺失时,linkData 会生成无意义的键名

valueNamealgorithmNamepublicKeyNamesignatureName 均可能为 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-utilityencodeBase64() 替换 btoa(),保持编码一致性。

项目中已在 pages/policy/[...slug].tsxpages/recipe/[...slug].tsx 使用 decodeBase64 from web-utility 进行解码。为保持代码一致性和规范统一,此处编码也应采用 web-utility 提供的 Base64 工具函数,而非原生 btoa()。这样可避免 Node.js/SSR 环境下的兼容性风险,并强化项目的依赖统一。

Comment on lines +44 to +72
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}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

iframeLink 缺失时,页面渲染出无效的 URL 和 iframe。

如果用户直接访问 /signature 而不带 iframeLink 查询参数(例如仅生成密钥对的场景),Line 49 会拼出 "undefined?..." 作为 link,导致:

  1. <a href="undefined?..."> 指向无效地址
  2. <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).

@TechQuery TechQuery merged commit 63fa0ff into main Feb 15, 2026
4 of 5 checks passed
@TechQuery TechQuery deleted the TechQuery/issue54 branch February 15, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

开发通用【数字签名】服务

1 participant