Skip to content

feat(auth): WeChat Official Account login & account binding - #9

Merged
DeaglePC merged 16 commits into
mainfrom
feat/wechat-login
Jun 25, 2026
Merged

feat(auth): WeChat Official Account login & account binding#9
DeaglePC merged 16 commits into
mainfrom
feat/wechat-login

Conversation

@DeaglePC

Copy link
Copy Markdown
Owner

Overview

Adds WeChat Official Account login and account binding to CatHeadTab, plus a handful of UI/dev fixes made alongside it. The login flow works with personal/unverified subscription accounts: the user follows the Official Account and sends a one-time verification code in chat; the backend confirms it and issues a JWT, mirroring the existing OAuth success path.

What changed

Backend

  • internal/handler/wechat.go — WeChat login/link/poll endpoints and the Official Account message callback.
  • internal/service/wechat_crypto.go (+ tests) — message signature verification and AES decryption for the callback.
  • internal/service/wechat_session.go / wechat.go — verification-code session lifecycle (issue → pending → confirmed/expired).
  • internal/config/config.go, internal/router/router.go, internal/repository/user.go, internal/handler/auth.go — config plumbing, routes, and user-record support for a bound WeChat identity.

Frontend

  • components/WeChatQR.tsx — new "follow + verification code" UI: shows the account QR (bundled asset, overridable at runtime via WECHAT_QR_IMAGE_URL) and a one-time code, polls until confirmed.
  • components/AuthModal.tsx — WeChat option in the sign-in modal; also fixes the modal so it resizes to its content when switching views (no more blank gap going register → login).
  • components/ProfileModal.tsx — bind/unbind WeChat for a logged-in user.
  • i18n strings added to zh.ts / en.ts, including a hint that the verification code is case-insensitive.

Misc fixes bundled in this branch

  • Brand-logo SVG favicon + restored favicon link in index.html.
  • Folder collapsed-preview icons now resolve identically to in-folder rendering (custom icons / colored-letter fallback).
  • .claude/launch.json: autoPort so the dev server falls back when 5173 is taken.
  • Config docs / .env.example / docker-compose.yml updated for the new WeChat env vars.

Reviewer notes

  • WeChat callback crypto is covered by wechat_crypto_test.go.
  • The live login flow needs real Official-Account credentials, so it wasn't exercised end-to-end in CI; the auth-modal resize fix was verified in the browser preview.
  • Login is case-insensitive on the verification code, surfaced to users via a hint in the QR view.

🤖 Generated with Claude Code

DeaglePC and others added 16 commits June 13, 2026 20:50
Add "follow + verification code" login via the WeChat Official Account
(公众号), plus WeChat binding for signed-in users.

Backend:
- WeChatService, WeChatSessionStore and message-crypto helpers
- Endpoints: POST /auth/wechat/login, GET /auth/wechat/poll,
  GET/POST /auth/wechat/callback (server URL verification + message push)
- POST /user/link/wechat to bind WeChat to an existing account
- Expose wechat_enabled in the OAuth config response
- Rate-limit verification-code issuance (1 per 3s per IP/user); polling
  is intentionally unlimited

Frontend:
- WeChatQR component and AuthModal / ProfileModal integration
- en/zh i18n strings

Docs:
- .env.example, docs/configuration.md and README updates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add <link rel="icon" type="image/svg+xml" href="/favicon.svg">,
removed in f693817. Without it the site only advertised 16/32px PNG
icons, so the bookmark icon picker could no longer detect a hi-res
(scalable) icon for catheadtab.cn. The PNG links are kept as fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enabling 服务器配置 (dev mode) routes every message to our callback, which
disables the Official Account's built-in AI auto-reply. Add an opt-in that
hands non-login messages back to WeChat's official AI via the passive reply
MsgType=transfer_biz_ai_ivr, so verification-code login and AI auto-reply
can coexist on a single account.

- config: add WeChatAIReply (WECHAT_AI_REPLY env)
- callback: route code-shaped text to login/link, everything else to the AI
  transfer when enabled (or a silent ack); normal messages no longer get a
  spurious "验证码无效" reply
- wire WECHAT_AI_REPLY through .env.example and docker-compose
- docs: document the transfer_biz_ai_ivr behavior and its prerequisites

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ship the login QR as a bundled asset instead of requiring WECHAT_QR_IMAGE_URL:
WeChatQR now imports frontend/src/assets/wechat-qr.png and uses it as the
default, while a configured WECHAT_QR_IMAGE_URL still overrides it at runtime.

NOTE: frontend/src/assets/wechat-qr.png is currently a PLACEHOLDER image —
replace it with your Official Account's real QR (same path/filename).

- WeChatQR: import the bundled asset, qrSrc = qrImageUrl || bundled
- docs/.env.example: WECHAT_QR_IMAGE_URL is now an optional runtime override

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SVG favicon (preferred by browsers over the PNG links) was a simple
hand-drawn cat that didn't match the brand logo. Embed the real logo
(store_icon.png, downscaled to 256px) inside favicon.svg so the tab icon
matches icon-*.png, while keeping the .svg filename and link intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-resize effect measured the `flex-1 overflow-y-auto` scroll
container, whose scrollHeight can never report below its stretched
clientHeight. Switching register -> login kept the window at the taller
register height, leaving a blank gap. Measure an inner content wrapper
(natural height) instead so the window shrinks to fit each view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mini-icon thumbnail only resolved favicons from URLs, so app tiles,
custom emoji/text icons, custom http images, and the colored-letter
fallback all rendered differently (or blank) in the collapsed folder
preview. Capture icon/url/title/iconColor per child and render each tile
through a MiniPreviewIcon that mirrors DesktopIconContent's resolution
precedence, so previews look identical to the folder's contents. Extend
the memo comparator to include title/iconColor so previews update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Let the frontend dev server fall back to another port when 5173 is
already in use instead of failing to start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a muted hint under the verification code in the WeChat scan-login
flow so users know the code they send to the Official Account is not
case-sensitive. Strings added to both zh and en locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The modal's auto-resize keyed off a fixed dependency list, so switching to
the WeChat view sized the window while WeChatQR was still in its async
loading state. When the QR + verification code arrived afterward the content
grew but no dependency changed, leaving the window too short and requiring a
scroll. Drive the resize off a ResizeObserver on the measured content node so
any reflow (including async WeChat content) re-fits the window, while keeping
the synchronous re-measure and content deps for immediate sizing and as a
fallback when ResizeObserver is unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Programmatic setWindowSize kept the top-left fixed and only clamped, so a
window that auto-grows to fit its content (e.g. the auth modal expanding for
the OAuth buttons or the register view) anchored its top and drifted below
center. Resize around the window's current center instead, so a centered
window stays centered as its height changes. Pointer-driven resizing keeps
its own top-left anchored path, so dragging the resize handle is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single-login account (e.g. WeChat-only) had no exit: it can't unlink its
sole login method (would lock the user out) and there was no way to delete
the account. Add DELETE /api/v1/user that removes the user and cascades to
all related data (oauth links, layout, bookmarks, background, ai usage).

This also enables re-homing an identity: delete the old account to free the
WeChat openid / email, then log in with another account and bind it there.

- backend: UserHandler.DeleteAccount + route (auth required, no email verify)
- frontend: ProfileModal "danger zone" with typed username confirmation,
  signs out locally on success
- i18n: zh/en strings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verification-code login returned the JWT to anyone polling
/wechat/poll with a confirmed code. The short code was both the human
input and the poll bearer credential, polling is unlimited, and a
confirmed session re-served the token for its whole ~7min TTL — so the
token was theoretically harvestable by guessing/replaying the code.

Issue a separate 32-byte random session id at login; the browser polls
with it while the human code is only matched against the WeChat callback
message. Pending sessions are now indexed by both code and session id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-fit windows play a scaleIn transform on open. getBoundingClientRect()
returns the transform-scaled visual box, reporting the header a few px shorter
than its laid-out height, so the window was sized too small and left a stray
vertical scrollbar until the next resize. Use offsetHeight (transform-independent
layout border-box) for the header measurement in both AuthModal and AddWidgetModal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New followers no longer receive the "send the code shown on the page"
passive reply. The web login flow already instructs the user, so the
auto-reply was redundant; events are now acknowledged silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DeaglePC
DeaglePC merged commit ac77faf into main Jun 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant