chore: prepare online wechat deployment

This commit is contained in:
vpnops 2026-05-12 18:32:53 +08:00
parent 2e2a903a0b
commit e638d245b7
6 changed files with 92 additions and 0 deletions

View file

@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add WeChat Open Platform QR-code authentication for web login and registration, including OAuth callback handling, user binding/creation, i18n error messaging, and environment variable configuration.
### Changed
- Respect `ALLOW_PUBLIC_REGISTRATION` when auto-creating first-time WeChat users.
## [1.8.4] - 2026-04-09
### Security

View file

@ -52,6 +52,15 @@ def _wechat_redirect_uri() -> Optional[str]:
return os.getenv("WECHAT_OPEN_REDIRECT_URI") or os.getenv("WECHAT_REDIRECT_URI")
def _allow_wechat_user_creation() -> bool:
return os.getenv("ALLOW_PUBLIC_REGISTRATION", "false").lower() in {
"true",
"1",
"yes",
"on",
}
async def build_wechat_authorize_url(state: str) -> WeChatAuthorizeUrlResponse:
app_id = _wechat_app_id()
redirect_uri = _wechat_redirect_uri()
@ -167,6 +176,9 @@ async def handle_wechat_callback(request: WeChatCallbackRequest) -> LoginRespons
updated = await UserRepository.update_user(user_id, updates)
user = updated[0] if updated else {**user, **updates}
else:
if not _allow_wechat_user_creation():
raise HTTPException(status_code=403, detail="Public registration is disabled")
user = await UserRepository.create_wechat_user(
{
"username": _username_from_wechat(user_info),

View file

@ -10,6 +10,13 @@ OPEN_NOTEBOOK_CORS_ORIGINS=https://lumina.yinhour.com
OPEN_NOTEBOOK_AUTH_MODE=jwt
ALLOW_PUBLIC_REGISTRATION=false
# Optional WeChat Open Platform web login.
# The redirect URI must be registered in WeChat exactly as shown here.
# If ALLOW_PUBLIC_REGISTRATION=false, WeChat can only sign in existing bound users.
WECHAT_OPEN_APP_ID=
WECHAT_OPEN_APP_SECRET=
WECHAT_OPEN_REDIRECT_URI=https://lumina.yinhour.com/login/wechat/callback
# Required for saving AI provider credentials.
OPEN_NOTEBOOK_ENCRYPTION_KEY=CHANGE_ME_generate_with_openssl_rand_base64_48

View file

@ -88,6 +88,16 @@ openssl rand -base64 32
Keep `OPEN_NOTEBOOK_AUTH_MODE=jwt` for any Internet-facing site. Keep `ALLOW_PUBLIC_REGISTRATION=false` unless you intentionally want public signups.
Optional WeChat QR-code login uses a WeChat Open Platform website application:
```bash
WECHAT_OPEN_APP_ID=...
WECHAT_OPEN_APP_SECRET=...
WECHAT_OPEN_REDIRECT_URI=https://lumina.yinhour.com/login/wechat/callback
```
Register the same redirect URI in WeChat Open Platform. With `ALLOW_PUBLIC_REGISTRATION=false`, WeChat sign-in is limited to existing bound users; set it to `true` only if first-time WeChat users should be able to create accounts.
## 6. Install App Dependencies and Build Frontend
```bash

View file

@ -22,6 +22,20 @@ Comprehensive list of all environment variables available in Open Notebook.
---
## WeChat Web Login
WeChat web login uses a WeChat Open Platform website application and the frontend callback page at `/login/wechat/callback`.
| Variable | Required? | Default | Description |
|----------|-----------|---------|-------------|
| `WECHAT_OPEN_APP_ID` | Required for WeChat login | None | WeChat Open Platform website application AppID. Legacy alias: `WECHAT_APP_ID` |
| `WECHAT_OPEN_APP_SECRET` | Required for callback token exchange | None | WeChat Open Platform website application AppSecret. Supports `_FILE` secret loading via `WECHAT_OPEN_APP_SECRET_FILE`; legacy alias: `WECHAT_APP_SECRET` |
| `WECHAT_OPEN_REDIRECT_URI` | Required for WeChat login | None | Full frontend callback URL registered in WeChat, for example `https://lumina.yinhour.com/login/wechat/callback`. Legacy alias: `WECHAT_REDIRECT_URI` |
If `ALLOW_PUBLIC_REGISTRATION=false`, WeChat can sign in existing bound users but will not auto-create accounts for new WeChat identities.
---
## Email Verification
Open Notebook uses email verification for public registration and password reset.

View file

@ -83,6 +83,7 @@ async def test_handle_wechat_callback_logs_in_existing_bound_user(monkeypatch):
@pytest.mark.asyncio
async def test_handle_wechat_callback_creates_user_for_new_wechat_identity(monkeypatch):
monkeypatch.setenv("ALLOW_PUBLIC_REGISTRATION", "true")
created_user = {
"id": "app_user:wx_openid_2",
"username": "wx_openid_2",
@ -138,3 +139,48 @@ async def test_handle_wechat_callback_creates_user_for_new_wechat_identity(monke
assert create_payload["display_name"] == "New WeChat User"
assert create_payload["wechat_openid"] == "openid-2"
assert create_payload["avatar_url"] == "https://example.com/new-avatar.jpg"
@pytest.mark.asyncio
async def test_handle_wechat_callback_blocks_new_user_when_public_registration_disabled(monkeypatch):
monkeypatch.setenv("ALLOW_PUBLIC_REGISTRATION", "false")
monkeypatch.setattr(
wechat_auth_service,
"_exchange_code_for_tokens",
AsyncMock(
return_value=WeChatOAuthTokens(
access_token="wx-token",
openid="openid-3",
unionid=None,
)
),
)
monkeypatch.setattr(
wechat_auth_service,
"_fetch_user_info",
AsyncMock(
return_value=WeChatUserInfo(
openid="openid-3",
unionid=None,
nickname="Blocked WeChat User",
headimgurl=None,
)
),
)
monkeypatch.setattr(
wechat_auth_service.UserRepository,
"get_user_by_wechat_identity",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
wechat_auth_service.UserRepository,
"create_wechat_user",
AsyncMock(),
)
with pytest.raises(wechat_auth_service.HTTPException) as exc_info:
await handle_wechat_callback(WeChatCallbackRequest(code="auth-code", state=None))
assert exc_info.value.status_code == 403
assert exc_info.value.detail == "Public registration is disabled"
wechat_auth_service.UserRepository.create_wechat_user.assert_not_awaited()