diff --git a/server/.golangci.yml b/server/.golangci.yml new file mode 100644 index 0000000..f9aff66 --- /dev/null +++ b/server/.golangci.yml @@ -0,0 +1,22 @@ +version: "2" + +linters: + enable: + - govet + - errcheck + - staticcheck + - revive + - gofmt + +linters-settings: + revive: + rules: + - name: exported + - name: var-naming + +issues: + exclude-rules: + # tools.go is build-tag-only, ignore unused import warnings + - path: tools\.go + linters: + - revive diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..0f31ee1 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,30 @@ +.PHONY: build test vet lint generate migrate-up migrate-down + +# ── build ────────────────────────────────────────────────────────────────────── +build: + go build ./... + +# ── test ─────────────────────────────────────────────────────────────────────── +test: + go test ./... + +# ── vet ──────────────────────────────────────────────────────────────────────── +vet: + go vet ./... + +# ── lint ─────────────────────────────────────────────────────────────────────── +lint: + golangci-lint run ./... + +# ── generate ─────────────────────────────────────────────────────────────────── +# Populated in task 1d: runs oapi-codegen against openapi.yaml +generate: + @echo "generate: not yet configured (see task 1d)" + +# ── migrate-up / migrate-down ────────────────────────────────────────────────── +# Populated in task 1e: runs golang-migrate against the Postgres DSN +migrate-up: + @echo "migrate-up: not yet configured (see task 1e)" + +migrate-down: + @echo "migrate-down: not yet configured (see task 1e)" diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..47d4009 --- /dev/null +++ b/server/README.md @@ -0,0 +1,47 @@ +# Pangolin Server + +Go 后端控制面,负责账户、套餐、激活码、设备与节点目录管理。架构详见 `../design/server/ARCHITECTURE.md`。 + +## 目录结构 + +``` +server/ +├── cmd/ +│ ├── server/ # HTTP 服务入口(chi router,/healthz,/v1 挂载归 1d) +│ └── migrate/ # 数据库迁移 CLI 入口(实现归 1e) +├── internal/ +│ ├── config/ # 配置加载(env + 文件,归 1e) +│ ├── store/ # 数据库访问层 Postgres(归 1e) +│ ├── apierr/ # 统一错误体 {code, message_zh, message_en}(归 1f) +│ ├── idgen/ # UUID + Crockford Base32 激活码(归 1f) +│ ├── auth/ # 验证码、argon2id、JWT RS256(归 1c/1d) +│ ├── codes/ # 激活码批次、兑换、审计(归 1d) +│ ├── devices/ # 设备管理(归 1d) +│ ├── nodes/ # 节点目录、connect/disconnect(归 1d) +│ ├── usage/ # 用量统计(归 1d) +│ └── admin/ # 内部管理端(归 1d) +├── Makefile +├── go.mod +├── go.sum +├── tools.go # //go:build tools — 锁定 oapi-codegen / migrate 版本 +└── .golangci.yml +``` + +## 常用命令 + +```bash +make build # go build ./... +make test # go test ./... +make vet # go vet ./... +make lint # golangci-lint run ./... +make generate # oapi-codegen 代码生成(1d 填充) +make migrate-up # 执行迁移(1e 填充) +make migrate-down # 回滚迁移(1e 填充) + +# 快速启动(默认监听 :8080) +go run ./cmd/server +# 指定地址 +go run ./cmd/server -addr :9090 +# 或通过环境变量 +ADDR=:9090 go run ./cmd/server +``` diff --git a/server/cmd/migrate/main.go b/server/cmd/migrate/main.go new file mode 100644 index 0000000..743a015 --- /dev/null +++ b/server/cmd/migrate/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stdout, "pangolin-migrate: database migration tool") + fmt.Fprintln(os.Stdout, "Usage: migrate [-up|-down] [-steps N]") + fmt.Fprintln(os.Stdout, "") + fmt.Fprintln(os.Stdout, " (implementation in task 1e)") + os.Exit(0) +} diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go new file mode 100644 index 0000000..0af43dc --- /dev/null +++ b/server/cmd/server/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "flag" + "log" + "net/http" + "os" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func main() { + addr := flag.String("addr", "", "listen address (default :8080, overridden by ADDR env)") + flag.Parse() + + if *addr == "" { + if v := os.Getenv("ADDR"); v != "" { + *addr = v + } else { + *addr = ":8080" + } + } + + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + }) + + log.Printf("pangolin server listening on %s", *addr) + if err := http.ListenAndServe(*addr, r); err != nil { + log.Fatalf("server error: %v", err) + } +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..4f5e23a --- /dev/null +++ b/server/go.mod @@ -0,0 +1,8 @@ +module github.com/wangjia/pangolin/server + +go 1.22 + +require ( + github.com/go-chi/chi/v5 v5.2.1 + github.com/google/uuid v1.6.0 +) diff --git a/server/internal/admin/doc.go b/server/internal/admin/doc.go new file mode 100644 index 0000000..0f7369e --- /dev/null +++ b/server/internal/admin/doc.go @@ -0,0 +1,5 @@ +// Package admin exposes internal management endpoints: code-batch generation, +// node CRUD, user lookup/ban, and audit-log queries. The admin router is +// mounted on a separate listener (or sub-path) protected by IP allowlist +// and two-factor authentication — it is never exposed on the public API port. +package admin diff --git a/server/internal/apierr/doc.go b/server/internal/apierr/doc.go new file mode 100644 index 0000000..e32a16d --- /dev/null +++ b/server/internal/apierr/doc.go @@ -0,0 +1,5 @@ +// Package apierr defines the canonical error response shape used across all +// v1 API handlers: {code, message_zh, message_en}. It provides constructor +// helpers for common HTTP error categories (400/401/403/404/409/429/500) +// and a middleware that serialises *APIError values to JSON automatically. +package apierr diff --git a/server/internal/auth/doc.go b/server/internal/auth/doc.go new file mode 100644 index 0000000..25e0734 --- /dev/null +++ b/server/internal/auth/doc.go @@ -0,0 +1,5 @@ +// Package auth handles all authentication concerns: email verification codes +// (Redis-backed, rate-limited), password hashing (argon2id), JWT issuance +// (RS256, access 15 min + refresh 30 d), and the HTTP middleware that +// validates bearer tokens and injects claims into request context. +package auth diff --git a/server/internal/codes/doc.go b/server/internal/codes/doc.go new file mode 100644 index 0000000..4317dc6 --- /dev/null +++ b/server/internal/codes/doc.go @@ -0,0 +1,5 @@ +// Package codes manages activation code lifecycle: batch generation, +// webhook ingestion from card-selling stores, idempotent redemption +// (subscription extension), and fraud controls (per-user failure +// rate-limiting). All redemptions are recorded in the audit log. +package codes diff --git a/server/internal/config/doc.go b/server/internal/config/doc.go new file mode 100644 index 0000000..4bb85a5 --- /dev/null +++ b/server/internal/config/doc.go @@ -0,0 +1,4 @@ +// Package config loads and validates server configuration from environment +// variables and optional config files. It provides a single Config struct +// consumed by all other packages at startup. +package config diff --git a/server/internal/devices/doc.go b/server/internal/devices/doc.go new file mode 100644 index 0000000..c510035 --- /dev/null +++ b/server/internal/devices/doc.go @@ -0,0 +1,5 @@ +// Package devices manages user devices (name, platform, WireGuard public key). +// It enforces per-plan device-count limits and coordinates with the nodes +// package to revoke WireGuard peers when a device is removed or a +// subscription expires. +package devices diff --git a/server/internal/idgen/doc.go b/server/internal/idgen/doc.go new file mode 100644 index 0000000..9004797 --- /dev/null +++ b/server/internal/idgen/doc.go @@ -0,0 +1,5 @@ +// Package idgen generates application-level identifiers. +// For most entities it wraps github.com/google/uuid (v7 time-ordered UUIDs). +// For activation codes it produces 16-character Crockford Base32 strings +// with a check digit, suitable for display and human entry. +package idgen diff --git a/server/internal/nodes/doc.go b/server/internal/nodes/doc.go new file mode 100644 index 0000000..50107a6 --- /dev/null +++ b/server/internal/nodes/doc.go @@ -0,0 +1,5 @@ +// Package nodes owns the node catalogue (region, tier, status, weight) and +// the connect/disconnect flow. It communicates with node agents over mTLS +// gRPC to add and remove WireGuard peers, and exposes a versioned catalogue +// endpoint with 304 support so clients can cache node lists efficiently. +package nodes diff --git a/server/internal/store/doc.go b/server/internal/store/doc.go new file mode 100644 index 0000000..445ced2 --- /dev/null +++ b/server/internal/store/doc.go @@ -0,0 +1,6 @@ +// Package store provides the database access layer for Pangolin. +// It wraps Postgres (via database/sql) and exposes typed repository +// interfaces for each domain entity: users, devices, plans, subscriptions, +// codes, nodes, usage, and audit log. Migrations are managed separately +// by the migrate command. +package store diff --git a/server/internal/usage/doc.go b/server/internal/usage/doc.go new file mode 100644 index 0000000..7c6f7a6 --- /dev/null +++ b/server/internal/usage/doc.go @@ -0,0 +1,5 @@ +// Package usage records per-user daily traffic and session minutes in +// usage_daily. It enforces free-plan daily caps and tracks ad-unlock +// timestamps. No destination addresses or DNS queries are stored — +// only aggregate byte counts and minute counts, per the no-log policy. +package usage diff --git a/server/setup.sh b/server/setup.sh new file mode 100644 index 0000000..2bc525a --- /dev/null +++ b/server/setup.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run once after cloning to download dependencies and generate go.sum. +set -euo pipefail +cd "$(dirname "$0")" +go mod tidy +echo "setup complete — run 'make build' to verify" diff --git a/server/tools.go b/server/tools.go new file mode 100644 index 0000000..6632df1 --- /dev/null +++ b/server/tools.go @@ -0,0 +1,8 @@ +//go:build tools + +package tools + +import ( + _ "github.com/golang-migrate/migrate/v4/cmd/migrate" + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +)