Commit Graph

9 Commits

Author SHA1 Message Date
wangjia 787151245e merge: maestro/tsk_tFMU7-hKzfOf [tsk_tFMU7-hKzfOf] codes 激活码模块
解决与 1A 骨架的冲突:module 统一为 github.com/wangjia/pangolin/server
(codes 分支原用 pangolin/server,7 个源文件 import 已改写);
go.mod require 并集(redis 取 9.20.1);Makefile 以骨架为基底并入
build-codegen/test-unit/test-integration target。
go build/vet 通过,codes 单测通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:03:11 +08:00
wangjia 8c516d4f2a merge: maestro/tsk_FUQws_DMIcXa [tsk_FUQws_DMIcXa] mTLS/CA + 引导 token 框架
解决与 Go 骨架(1A)的 add/add 冲突:go.mod 统一 module 路径为
github.com/wangjia/pangolin/server,require 取并集;setup.sh 合并两边意图。
go build ./... 与 go test ./internal/mtls/... 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:01:31 +08:00
wangjia 2112230d57 merge: maestro/tsk_A1HkEdRbXLGY [tsk_A1HkEdRbXLGY] openapi.yaml 契约(13 操作 + 全量 schema) 2026-06-13 02:57:25 +08:00
wangjia 3f0f82eca0 merge: maestro/tsk_L2k2VrEujof8 [tsk_L2k2VrEujof8] Go 模块骨架 + 工具链(go.mod/tools.go/Makefile) 2026-06-13 02:57:25 +08:00
wangjia afcd7b325c feat(codes): implement activation-code module (tsk_tFMU7-hKzfOf)
Implements server/internal/codes/ with all required functionality:

## Generator (generator.go)
- Crockford Base32 16-char codes (15 data + 1 Crockford mod-37 check char)
- crypto/rand for unbiased random generation with rejection sampling
- Canonicalize(): I/L→1, O→0 folding, hyphen/space stripping
- Hash(): SHA-256 of canonical plaintext (only value stored in DB)
- Algorithm hard-coded; check char detects all single-char substitution errors

## Store (store.go)
- MySQL-backed via database/sql
- CreateBatch / CreateCode with ErrDuplicate on UNIQUE conflict
- FindCodeByHashForUpdate: SELECT … FOR UPDATE for row-level concurrency control
- MarkRedeemed, ExtendSubscription, CreateSubscription, GetActiveSubscriptions
- WriteAuditLog, CodeExistsByHash
- BeginTx at READ COMMITTED (FOR UPDATE provides row exclusivity)

## Service (service.go)
- Redeem(): 9-step flow with full idempotency and concurrency safety
  - isLocked / recordFail / clearFail via Redis key redeem:fail:{user_id}
  - SELECT … FOR UPDATE → single winner under N-concurrent redemptions
  - Same-plan: extends existing subscription (max(expires_at,now)+days)
  - Cross-plan: creates new subscription (max(now,latest)+days)
  - Idempotent: same user re-submits → 200 without re-applying
  - 5 failures → ACCOUNT_LOCKED for 1 hour (sliding window via Redis)
- CreateBatch(): generates N codes, stores hashes, returns plaintext once
  - Automatic retry on hash collision (birthday probability ≈10⁻⁸)

## Webhook (webhook.go)
- POST /webhook/store/codes — outside /v1, no JWT required
- HMAC-SHA256 with hmac.Equal constant-time comparison
- ±5 min timestamp window
- Redis SetNX nonce deduplication (15-min TTL)
- Idempotent: duplicate nonce → 200; duplicate code_hash → 200

## HTTP Handler (handler.go)
- POST /v1/redeem endpoint wired to Service.Redeem
- Reads userID from context key (set by JWT middleware from auth module)
- Bilingual error responses {code, message_zh, message_en}

## Export (export.go)
- ExportCSV(): streams plaintext codes to io.Writer as CSV
- Plaintext NEVER stored in DB; only SHA-256 hash persists

## CLI (cmd/codegen/main.go)
- codegen -plan -days -count -channel -note -dsn [-out]
- Transition tool until admin panel (#8) is ready
- Outputs CSV to stdout or file; warns operator about plaintext sensitivity

## Infrastructure (skeleton)
- internal/config/config.go: env-var configuration
- internal/db/db.go: MySQL connection pool helper
- internal/redisutil/redis.go: Redis client constructor
- internal/apierr/apierr.go: bilingual error types
- migrations/001_init.sql: DDL for codes module tables
- go.mod with all dependencies

## Tests
- generator_test.go (unit, no deps):
  - Format, uniqueness (10k codes, zero collisions), normalization,
    check-char detection of all single-char errors, hash consistency
- webhook_test.go (unit, no deps):
  - Signature rejection, missing signature, stale/future timestamp,
    missing nonce, constant-time HMAC comparison
- service_test.go (//go:build integration, testcontainers):
  - N=20 concurrent redeemers → exactly 1 winner
  - Idempotent redeem returns success for same user
  - Other-user redemption → CODE_REDEEMED + failure counted
  - 5 failures → ACCOUNT_LOCKED on 6th attempt
  - Same-plan extension: expires_at precision assertion
  - Cross-plan creation: new subscription row
  - Audit log written on every successful redemption
  - CSV export: no plaintext in DB, hash present
  - Webhook nonce replay: idempotent 200
  - Webhook same-hash: idempotent 200, single DB row

Run unit tests: make test
Run integration tests: make test-integration (requires Docker)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 02:16:16 +08:00
wangjia 73fd281bb5 feat(mtls): implement mTLS/CA + bootstrap token framework [tsk_FUQws_DMIcXa]
ECDSA P-256 self-signed CA with disk persistence (load-or-generate),
CSR signing (CN=nodeUUID, 90d validity, EKU=ClientAuth), one-time
bootstrap tokens via Redis GETDEL (15min TTL), CRL revocation with
Redis SET + DB interface, gRPC unary+stream interceptors that extract
CN from verified TLS chains (Enroll whitelisted, others require cert),
and NewServerTLSConfig (VerifyClientCertIfGiven + TLS 1.3 + CRL hook).

Frozen API: SignCSR / CAPEM / IssueToken / ConsumeToken / Revoke /
            NewServerTLSConfig / UnaryServerInterceptor / NodeUUIDFromContext

Tests cover: CA sign+verify, token one-time guarantee, TTL expiry,
             revocation rejection, interceptor whitelist (5 categories).
             Redis layer backed by miniredis in tests.

Run setup.sh from server/ to fetch deps and verify tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:44:21 +08:00
wangjia 58c6eb721e feat: 新增 server/api/openapi.yaml — v1 API 完整契约(tsk_A1HkEdRbXLGY)
OpenAPI 3.0.3,覆盖 doc/02 §2 全部 15 个操作:
  - 认证 4(sendVerificationCode/register/login/refreshToken,security:[])
  - 账户 3(getMe/listDevices/deleteDevice)
  - 商业 3(redeemCode/adsUnlock/listPlans)
  - 节点 3(listNodes 含 304/connectNode/disconnectNode)
  - 用量 1(getUsage)、公告 1(listNotices)

全量 schema:Error·TokenPair·Me·UserInfo·SubscriptionInfo·TodayUsageSummary·
Device·Plan·RedeemResult·NodeDirectory·Node·ConnectCredential·
RealityParams·Hysteria2Params·UsagePoint·Notice

安全约定:bearerAuth(JWT RS256) 全局;auth 四操作 security:[];
components/responses 统一 Error 引用;429 TooManyRequests 带 Retry-After;
节点目录不含密钥,密钥仅由 connect 端点下发;对外 ID 全部 uuid 格式。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:29:31 +08:00
wangjia 1629385c70 feat(server): Go 模块骨架 + 工具链 [tsk_L2k2VrEujof8]
新建 server/ Go 模块:
- go.mod: module github.com/wangjia/pangolin/server, Go 1.22
  依赖: go-chi/chi/v5, google/uuid
- tools.go: //go:build tools 锁定 oapi-codegen / golang-migrate 版本
- cmd/server/main.go: chi router + GET /healthz → {"status":"ok"},监听 :8080
- cmd/migrate/main.go: 占位入口(实逻辑归 1e)
- internal/{config,store,apierr,idgen,auth,codes,devices,nodes,usage,admin}/doc.go
  各包职责说明
- Makefile: build/test/vet/lint/generate/migrate-up/migrate-down
- .golangci.yml: govet/errcheck/staticcheck/revive/gofmt 基线
- README.md: 定位 + 目录树 + make 入口说明
- setup.sh: 首次 go mod tidy 引导脚本(go.sum 需运行后生成)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:28:37 +08:00
wangjia b4114c93ee feat(db): add MySQL migrations 000001-000007 covering all 12 tables + seed [tsk_paiqu21eZIuw]
- 000001: users + devices(账户 / 设备,含 argon2id pw_hash、dp_uuid)
- 000002: plans + subscriptions(套餐口径来自 design/CLAUDE.md §7)
- 000003: code_batches + codes(SHA-256 code_hash,明文不入库)
- 000004: usage_daily + audit_log(最小数据原则 + JSON meta)
- 000005: providers + nodes(providers 先建,nodes FK 引用)
- 000006: node_events(9 值 ENUM)+ directory_version(CHECK id=1,MySQL ≥ 8.0.16)
- 000007: plans 三行 seed + directory_version(1,1),ON DUPLICATE KEY UPDATE 保可重入

所有表 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,时间列 DATETIME(6),
主键 BIGINT UNSIGNED AUTO_INCREMENT + 对外 uuid CHAR(36) UNIQUE。
每对 down 文件逆序 DROP(子表先于父表)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:28:01 +08:00