feat: 档2 接真后端 — mac app 真注册/登录/拉节点
dev 本机置备(新增 dev/):
- docker-compose.yml: MySQL :13306 + Redis :16379(避让本机已占的 3306/6379)
- run-local.sh 一键: 起容器→openssl 生成密钥(dev/.local gitignore)→migrate→
seed→启动 server :18080(避让 8080);LogMailer 把验证码打日志
- seed.sql(1 provider + 1 HK 节点)、run-local.md 手册、.gitignore
后端两处修复(本地无 gRPC 场景,注释意图与代码不符的 bug):
- main.go: 无 gRPC 时真正构造 Hub-only nodeSvc(原只建 hub 没赋值),/v1/nodes 才能挂
- main.go: /me 改 Route 子路由根 Get,修 Get("/me")+Route("/me") 冲突致 404
客户端接真后端:
- token_store: MacOsOptions(useDataProtectionKeyChain:false) 修 keychain -34018
- auth_screen: 删 dev 旁路(test 账户内存登录),所有登录走真 API
验收: 后端 curl 端到端全通(验证码→注册→登录→/nodes→/me);
server go build/vet/test 通过, client analyze 0 + test 84
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class TokenStore {
|
||||
const TokenStore({FlutterSecureStorage? storage})
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
: _storage = storage ?? const FlutterSecureStorage(
|
||||
// macOS 用文件式 keychain,避免未签名 app 缺 keychain-access-groups
|
||||
// entitlement 时 write 报 -34018。
|
||||
mOptions: MacOsOptions(useDataProtectionKeyChain: false),
|
||||
);
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// auth_screen.dart — 登录 / 注册页(邮箱 + 验证码流程)
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -41,22 +40,6 @@ class _AuthScreenState extends ConsumerState<AuthScreen> {
|
||||
|
||||
late final AuthApi _api = AuthApi(baseUrl: _kApiUrl);
|
||||
|
||||
// ── Dev-only 测试账户旁路(仅 debug build)─────────────────────
|
||||
// 后端 /v1/auth 未就绪时,用此账户直接进入主界面浏览 UI。
|
||||
// 登录后节点列表回退 kDemoNodes,连接键走 mock 动画。
|
||||
static const _devEmail = 'test@pangolin.dev';
|
||||
static const _devPassword = 'test1234';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// debug 下预填测试账户,方便直接点登录。
|
||||
if (kDebugMode) {
|
||||
_email.text = _devEmail;
|
||||
_pw.text = _devPassword;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _emailValid => RegExp(r'\S+@\S+\.\S+').hasMatch(_email.text);
|
||||
|
||||
@override
|
||||
@@ -97,13 +80,6 @@ class _AuthScreenState extends ConsumerState<AuthScreen> {
|
||||
|
||||
Future<void> _doLogin() async {
|
||||
setState(() { _loading = true; _errorZh = null; });
|
||||
// Dev 旁路:debug build 下用测试账户跳过后端直接登录。
|
||||
// 用 devLogin 只设内存态,不写 keychain(规避 -34018 entitlement 问题)。
|
||||
if (kDebugMode && _email.text.trim() == _devEmail && _pw.text == _devPassword) {
|
||||
ref.read(authProvider.notifier).devLogin('dev-access-token');
|
||||
if (mounted) widget.onDone();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final tokens = await _api.login(
|
||||
email: _email.text.trim(),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# 本地 dev 密钥(JWT 私钥/公钥、webhook secret、derive key),绝不进 git。
|
||||
.local/
|
||||
@@ -0,0 +1,27 @@
|
||||
# 本地开发用 MySQL + Redis(与生产隔离)。
|
||||
# 用法:docker compose -f dev/docker-compose.yml up -d
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: pangolin-dev-mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: pangolin
|
||||
ports:
|
||||
- "13306:3306" # 避让本机已占用的 3306
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-proot"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
volumes:
|
||||
- pangolin-dev-mysql:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: pangolin-dev-redis
|
||||
ports:
|
||||
- "16379:6379" # 避让本机已占用的 6379
|
||||
|
||||
volumes:
|
||||
pangolin-dev-mysql:
|
||||
@@ -0,0 +1,61 @@
|
||||
# 本地开发:一键起后端
|
||||
|
||||
让后端在本机真跑起来(auth 注册/登录 + 节点列表),供 mac app 接真后端调试。
|
||||
|
||||
## 前置
|
||||
|
||||
- Docker(起 MySQL + Redis)
|
||||
- Go(`/opt/homebrew/bin` 在 PATH,脚本已内置)
|
||||
- openssl(生成 JWT 密钥)
|
||||
|
||||
## 一键启动
|
||||
|
||||
```bash
|
||||
cd <项目根> # pangolin/
|
||||
bash dev/run-local.sh
|
||||
```
|
||||
|
||||
脚本做的事:
|
||||
1. `docker compose` 起 `pangolin-dev-mysql`(:13306)+ `pangolin-dev-redis`(:16379,避让本机已占用的 3306/6379),等 MySQL 就绪
|
||||
2. 首次生成密钥到 `dev/.local/`(gitignore):RS256 JWT 私钥/公钥、`webhook_secret`、`derive_key`
|
||||
3. 组装 env(`DB_DSN`/`WEBHOOK_SECRET`/`NODE_DERIVE_KEY`/`JWT_*`),不配 SMTP → 验证码打日志
|
||||
4. `go run ./cmd/migrate up`(建表 + plans seed),`dev/seed.sql` 插一个 HK 测试节点
|
||||
5. 前台启动 `go run ./cmd/server`(:18080)—— **注册验证码会打印在终端日志**
|
||||
|
||||
停止:`Ctrl+C`(停 server);`docker compose -f dev/docker-compose.yml down` 停容器(加 `-v` 清库)。
|
||||
|
||||
## 端到端验证(curl)
|
||||
|
||||
```bash
|
||||
# 1) 发验证码(看 server 终端日志读 6 位码)
|
||||
curl -XPOST localhost:18080/v1/auth/code -H 'Content-Type: application/json' \
|
||||
-d '{"email":"me@x.com"}'
|
||||
|
||||
# 2) 注册(填上一步日志里的码)
|
||||
curl -XPOST localhost:18080/v1/auth/register -H 'Content-Type: application/json' \
|
||||
-d '{"email":"me@x.com","code":"<码>","password":"pass1234"}'
|
||||
# → {"access_token":"...","refresh_token":"..."}
|
||||
|
||||
# 3) 拉节点列表(带 access token)
|
||||
curl localhost:18080/v1/nodes -H 'Authorization: Bearer <access>'
|
||||
# → 含 HK 测试节点
|
||||
|
||||
# 4) 当前用户
|
||||
curl localhost:18080/v1/me -H 'Authorization: Bearer <access>'
|
||||
```
|
||||
|
||||
## mac app 接它
|
||||
|
||||
```bash
|
||||
cd client
|
||||
# 客户端默认连 :8080;本机 8080 被占用,dev 后端在 18080,故显式指定:
|
||||
flutter run -d macos --dart-define=PANGOLIN_API_URL=http://localhost:18080
|
||||
```
|
||||
|
||||
注册页输真邮箱 → 发验证码(后端日志读码)→ 填码+密码 → 进主界面 → 节点页显示真节点(HK·测试)。
|
||||
|
||||
## 说明
|
||||
|
||||
- **密钥纯本地玩具**:`dev/.local/` 不进 git;换机重跑脚本会重新生成。
|
||||
- **节点 endpoint 占位**(`127.0.0.1:11443`):档 2 只验证列表 + connect 配置渲染,**不真连**。真连接出网见档 3。
|
||||
- 不连 EC2 生产 MySQL(本地隔离,避免污染线上)。
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-local.sh — 一键起本地后端(DB/Redis + 密钥 + 迁移 + seed + server)。
|
||||
# 从项目根运行: bash dev/run-local.sh
|
||||
# 验证码:未配 SMTP,注册验证码会打到本终端的 server 日志里。
|
||||
# 密钥:首次生成到 dev/.local/(gitignore);标量密钥用固定/随机 dev 值,纯本地玩具。
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
ROOT="$PWD"
|
||||
LOCAL="$ROOT/dev/.local"
|
||||
mkdir -p "$LOCAL"
|
||||
|
||||
# ── 1. 起 MySQL + Redis ──────────────────────────────────────────────
|
||||
echo "[run-local] 起 MySQL + Redis ..."
|
||||
docker compose -f dev/docker-compose.yml up -d
|
||||
echo "[run-local] 等待 MySQL 就绪 ..."
|
||||
until docker exec pangolin-dev-mysql mysqladmin ping -proot --silent >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
echo "[run-local] MySQL 就绪。"
|
||||
|
||||
# ── 2. 生成密钥(仅当缺失)────────────────────────────────────────────
|
||||
if [ ! -f "$LOCAL/jwt_private.pem" ]; then
|
||||
echo "[run-local] 生成 RS256 JWT 密钥对 ..."
|
||||
openssl genrsa -out "$LOCAL/jwt_private.pem" 2048
|
||||
openssl rsa -in "$LOCAL/jwt_private.pem" -pubout -out "$LOCAL/jwt_public.pem"
|
||||
fi
|
||||
[ -f "$LOCAL/webhook_secret" ] || openssl rand -hex 32 > "$LOCAL/webhook_secret"
|
||||
[ -f "$LOCAL/derive_key" ] || openssl rand -hex 32 > "$LOCAL/derive_key"
|
||||
|
||||
# ── 3. 组装环境变量 ──────────────────────────────────────────────────
|
||||
read -r WEBHOOK_SECRET < "$LOCAL/webhook_secret"
|
||||
read -r NODE_DERIVE_KEY < "$LOCAL/derive_key"
|
||||
export WEBHOOK_SECRET NODE_DERIVE_KEY
|
||||
export DB_DSN="root:root@tcp(127.0.0.1:13306)/pangolin?parseTime=true&loc=UTC&multiStatements=true"
|
||||
export REDIS_ADDR="127.0.0.1:16379"
|
||||
export JWT_PRIVATE_KEY_PATH="$LOCAL/jwt_private.pem"
|
||||
export JWT_KEY_ID="dev-key-1"
|
||||
export JWT_PUBLIC_KEYS="dev-key-1:$LOCAL/jwt_public.pem"
|
||||
export ADDR=":18080" # 避让本机已占用的 8080
|
||||
# 不设 SMTP_* → LogMailer(验证码打日志);不设 GRPC_* → 不启 agent server。
|
||||
|
||||
# ── 4. 迁移 + seed ───────────────────────────────────────────────────
|
||||
echo "[run-local] 数据库迁移 ..."
|
||||
( cd server && go run ./cmd/migrate up )
|
||||
echo "[run-local] seed 节点 ..."
|
||||
docker exec -i pangolin-dev-mysql mysql -uroot -proot pangolin < dev/seed.sql
|
||||
|
||||
# ── 5. 启动后端(前台,验证码看本终端日志)──────────────────────────
|
||||
echo "[run-local] 启动后端 :18080 —— 注册验证码会打印在下方日志。Ctrl+C 停止。"
|
||||
( cd server && go run ./cmd/server )
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 本地开发 seed:1 provider + 1 个 status='up' 节点,供 GET /v1/nodes 返回。
|
||||
-- 幂等:固定 uuid,重跑先删后插。endpoint/reality_* 为占位(档 2 不真连,档 3 再指真实数据面)。
|
||||
|
||||
DELETE FROM nodes WHERE uuid = '11111111-1111-1111-1111-111111111111';
|
||||
DELETE FROM providers WHERE name = 'dev-local';
|
||||
|
||||
INSERT INTO providers (name, api_kind, regions, pool, enabled)
|
||||
VALUES ('dev-local', 'manual', '["HK"]', 'consumable', TRUE);
|
||||
|
||||
INSERT INTO nodes
|
||||
(uuid, region, name_zh, name_en, role, tier, endpoint, hy2_port,
|
||||
reality_pbk, reality_prk, reality_short_id, reality_sni, provider_id, status, weight)
|
||||
VALUES (
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'HK', '香港 · 测试', 'Hong Kong (dev)',
|
||||
'exit', 'free',
|
||||
'127.0.0.1:11443', 443,
|
||||
'devPlaceholderRealityPublicKey00000000000000', '', '6eb28f1a',
|
||||
'www.apple.com',
|
||||
(SELECT id FROM providers WHERE name = 'dev-local' LIMIT 1),
|
||||
'up', 100
|
||||
);
|
||||
@@ -102,11 +102,12 @@ func main() {
|
||||
"GRPC_CERT_PATH_set", grpcCertPath != "",
|
||||
"GRPC_KEY_PATH_set", grpcKeyPath != "")
|
||||
}
|
||||
// Even without gRPC, build a Hub-only nodeSvc for local/test use.
|
||||
// nil CA means Enroll will panic if called — acceptable when gRPC is off.
|
||||
hub := nodes.NewHub(rdb)
|
||||
hub.Start(context.Background())
|
||||
log.Printf("nodes.Service: gRPC not configured; Hub active for command queueing")
|
||||
// Even without gRPC, build a Hub-only nodeSvc so the HTTP /v1/nodes
|
||||
// routes (ListNodes/Connect) work locally. nil CA/tokens means
|
||||
// agent Enroll would panic if called — acceptable when gRPC is off.
|
||||
nodeSvc = nodes.NewService(nil, nil, rdb, nodeStore)
|
||||
nodeSvc.Hub().Start(context.Background())
|
||||
log.Printf("nodes.Service: gRPC not configured; Hub active for command queueing (HTTP /v1/nodes enabled)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +238,8 @@ func mountV1(r chi.Router, sqlDB *sql.DB, rdb *redis.Client, nodeSvc *nodes.Serv
|
||||
v1.Group(func(protected chi.Router) {
|
||||
protected.Use(auth.RequireAuth(tm))
|
||||
|
||||
protected.Get("/me", accountAPI.GetMe)
|
||||
protected.Route("/me", func(me chi.Router) {
|
||||
me.Get("/", accountAPI.GetMe) // 子路由根,避免与 Route("/me") 冲突致 404
|
||||
devicesHandler.RegisterRoutes(me)
|
||||
})
|
||||
protected.Post("/redeem", redeemHandler.ServeHTTP)
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user