feat(auth): WeChat Official Account login & account binding - #9
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 viaWECHAT_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.zh.ts/en.ts, including a hint that the verification code is case-insensitive.Misc fixes bundled in this branch
index.html..claude/launch.json:autoPortso the dev server falls back when 5173 is taken..env.example/docker-compose.ymlupdated for the new WeChat env vars.Reviewer notes
wechat_crypto_test.go.🤖 Generated with Claude Code