无订阅 支付后出现两个订阅
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m37s

This commit is contained in:
shanshanzhong 2026-03-05 21:53:36 -08:00
parent a1ab0fefa4
commit 7308aa9191
51 changed files with 2041 additions and 556 deletions

7
.claude-flow/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Claude Flow runtime files
data/
logs/
sessions/
neural/
*.log
*.tmp

View File

@ -0,0 +1,403 @@
# RuFlo V3 - Complete Capabilities Reference
> Generated: 2026-03-06T01:38:48.235Z
> Full documentation: https://github.com/ruvnet/claude-flow
## 📋 Table of Contents
1. [Overview](#overview)
2. [Swarm Orchestration](#swarm-orchestration)
3. [Available Agents (60+)](#available-agents)
4. [CLI Commands (26 Commands, 140+ Subcommands)](#cli-commands)
5. [Hooks System (27 Hooks + 12 Workers)](#hooks-system)
6. [Memory & Intelligence (RuVector)](#memory--intelligence)
7. [Hive-Mind Consensus](#hive-mind-consensus)
8. [Performance Targets](#performance-targets)
9. [Integration Ecosystem](#integration-ecosystem)
---
## Overview
RuFlo V3 is a domain-driven design architecture for multi-agent AI coordination with:
- **15-Agent Swarm Coordination** with hierarchical and mesh topologies
- **HNSW Vector Search** - 150x-12,500x faster pattern retrieval
- **SONA Neural Learning** - Self-optimizing with <0.05ms adaptation
- **Byzantine Fault Tolerance** - Queen-led consensus mechanisms
- **MCP Server Integration** - Model Context Protocol support
### Current Configuration
| Setting | Value |
|---------|-------|
| Topology | hierarchical-mesh |
| Max Agents | 15 |
| Memory Backend | hybrid |
| HNSW Indexing | Enabled |
| Neural Learning | Enabled |
| LearningBridge | Enabled (SONA + ReasoningBank) |
| Knowledge Graph | Enabled (PageRank + Communities) |
| Agent Scopes | Enabled (project/local/user) |
---
## Swarm Orchestration
### Topologies
| Topology | Description | Best For |
|----------|-------------|----------|
| `hierarchical` | Queen controls workers directly | Anti-drift, tight control |
| `mesh` | Fully connected peer network | Distributed tasks |
| `hierarchical-mesh` | V3 hybrid (recommended) | 10+ agents |
| `ring` | Circular communication | Sequential workflows |
| `star` | Central coordinator | Simple coordination |
| `adaptive` | Dynamic based on load | Variable workloads |
### Strategies
- `balanced` - Even distribution across agents
- `specialized` - Clear roles, no overlap (anti-drift)
- `adaptive` - Dynamic task routing
### Quick Commands
```bash
# Initialize swarm
npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized
# Check status
npx @claude-flow/cli@latest swarm status
# Monitor activity
npx @claude-flow/cli@latest swarm monitor
```
---
## Available Agents
### Core Development (5)
`coder`, `reviewer`, `tester`, `planner`, `researcher`
### V3 Specialized (4)
`security-architect`, `security-auditor`, `memory-specialist`, `performance-engineer`
### Swarm Coordination (5)
`hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator`, `collective-intelligence-coordinator`, `swarm-memory-manager`
### Consensus & Distributed (7)
`byzantine-coordinator`, `raft-manager`, `gossip-coordinator`, `consensus-builder`, `crdt-synchronizer`, `quorum-manager`, `security-manager`
### Performance & Optimization (5)
`perf-analyzer`, `performance-benchmarker`, `task-orchestrator`, `memory-coordinator`, `smart-agent`
### GitHub & Repository (9)
`github-modes`, `pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager`, `workflow-automation`, `project-board-sync`, `repo-architect`, `multi-repo-swarm`
### SPARC Methodology (6)
`sparc-coord`, `sparc-coder`, `specification`, `pseudocode`, `architecture`, `refinement`
### Specialized Development (8)
`backend-dev`, `mobile-dev`, `ml-developer`, `cicd-engineer`, `api-docs`, `system-architect`, `code-analyzer`, `base-template-generator`
### Testing & Validation (2)
`tdd-london-swarm`, `production-validator`
### Agent Routing by Task
| Task Type | Recommended Agents | Topology |
|-----------|-------------------|----------|
| Bug Fix | researcher, coder, tester | mesh |
| New Feature | coordinator, architect, coder, tester, reviewer | hierarchical |
| Refactoring | architect, coder, reviewer | mesh |
| Performance | researcher, perf-engineer, coder | hierarchical |
| Security | security-architect, auditor, reviewer | hierarchical |
| Docs | researcher, api-docs | mesh |
---
## CLI Commands
### Core Commands (12)
| Command | Subcommands | Description |
|---------|-------------|-------------|
| `init` | 4 | Project initialization |
| `agent` | 8 | Agent lifecycle management |
| `swarm` | 6 | Multi-agent coordination |
| `memory` | 11 | AgentDB with HNSW search |
| `mcp` | 9 | MCP server management |
| `task` | 6 | Task assignment |
| `session` | 7 | Session persistence |
| `config` | 7 | Configuration |
| `status` | 3 | System monitoring |
| `workflow` | 6 | Workflow templates |
| `hooks` | 17 | Self-learning hooks |
| `hive-mind` | 6 | Consensus coordination |
### Advanced Commands (14)
| Command | Subcommands | Description |
|---------|-------------|-------------|
| `daemon` | 5 | Background workers |
| `neural` | 5 | Pattern training |
| `security` | 6 | Security scanning |
| `performance` | 5 | Profiling & benchmarks |
| `providers` | 5 | AI provider config |
| `plugins` | 5 | Plugin management |
| `deployment` | 5 | Deploy management |
| `embeddings` | 4 | Vector embeddings |
| `claims` | 4 | Authorization |
| `migrate` | 5 | V2→V3 migration |
| `process` | 4 | Process management |
| `doctor` | 1 | Health diagnostics |
| `completions` | 4 | Shell completions |
### Example Commands
```bash
# Initialize
npx @claude-flow/cli@latest init --wizard
# Spawn agent
npx @claude-flow/cli@latest agent spawn -t coder --name my-coder
# Memory operations
npx @claude-flow/cli@latest memory store --key "pattern" --value "data" --namespace patterns
npx @claude-flow/cli@latest memory search --query "authentication"
# Diagnostics
npx @claude-flow/cli@latest doctor --fix
```
---
## Hooks System
### 27 Available Hooks
#### Core Hooks (6)
| Hook | Description |
|------|-------------|
| `pre-edit` | Context before file edits |
| `post-edit` | Record edit outcomes |
| `pre-command` | Risk assessment |
| `post-command` | Command metrics |
| `pre-task` | Task start + agent suggestions |
| `post-task` | Task completion learning |
#### Session Hooks (4)
| Hook | Description |
|------|-------------|
| `session-start` | Start/restore session |
| `session-end` | Persist state |
| `session-restore` | Restore previous |
| `notify` | Cross-agent notifications |
#### Intelligence Hooks (5)
| Hook | Description |
|------|-------------|
| `route` | Optimal agent routing |
| `explain` | Routing decisions |
| `pretrain` | Bootstrap intelligence |
| `build-agents` | Generate configs |
| `transfer` | Pattern transfer |
#### Coverage Hooks (3)
| Hook | Description |
|------|-------------|
| `coverage-route` | Coverage-based routing |
| `coverage-suggest` | Improvement suggestions |
| `coverage-gaps` | Gap analysis |
### 12 Background Workers
| Worker | Priority | Purpose |
|--------|----------|---------|
| `ultralearn` | normal | Deep knowledge |
| `optimize` | high | Performance |
| `consolidate` | low | Memory consolidation |
| `predict` | normal | Predictive preload |
| `audit` | critical | Security |
| `map` | normal | Codebase mapping |
| `preload` | low | Resource preload |
| `deepdive` | normal | Deep analysis |
| `document` | normal | Auto-docs |
| `refactor` | normal | Suggestions |
| `benchmark` | normal | Benchmarking |
| `testgaps` | normal | Coverage gaps |
---
## Memory & Intelligence
### RuVector Intelligence System
- **SONA**: Self-Optimizing Neural Architecture (<0.05ms)
- **MoE**: Mixture of Experts routing
- **HNSW**: 150x-12,500x faster search
- **EWC++**: Prevents catastrophic forgetting
- **Flash Attention**: 2.49x-7.47x speedup
- **Int8 Quantization**: 3.92x memory reduction
### 4-Step Intelligence Pipeline
1. **RETRIEVE** - HNSW pattern search
2. **JUDGE** - Success/failure verdicts
3. **DISTILL** - LoRA learning extraction
4. **CONSOLIDATE** - EWC++ preservation
### Self-Learning Memory (ADR-049)
| Component | Status | Description |
|-----------|--------|-------------|
| **LearningBridge** | ✅ Enabled | Connects insights to SONA/ReasoningBank neural pipeline |
| **MemoryGraph** | ✅ Enabled | PageRank knowledge graph + community detection |
| **AgentMemoryScope** | ✅ Enabled | 3-scope agent memory (project/local/user) |
**LearningBridge** - Insights trigger learning trajectories. Confidence evolves: +0.03 on access, -0.005/hour decay. Consolidation runs the JUDGE/DISTILL/CONSOLIDATE pipeline.
**MemoryGraph** - Builds a knowledge graph from entry references. PageRank identifies influential insights. Communities group related knowledge. Graph-aware ranking blends vector + structural scores.
**AgentMemoryScope** - Maps Claude Code 3-scope directories:
- `project`: `<gitRoot>/.claude/agent-memory/<agent>/`
- `local`: `<gitRoot>/.claude/agent-memory-local/<agent>/`
- `user`: `~/.claude/agent-memory/<agent>/`
High-confidence insights (>0.8) can transfer between agents.
### Memory Commands
```bash
# Store pattern
npx @claude-flow/cli@latest memory store --key "name" --value "data" --namespace patterns
# Semantic search
npx @claude-flow/cli@latest memory search --query "authentication"
# List entries
npx @claude-flow/cli@latest memory list --namespace patterns
# Initialize database
npx @claude-flow/cli@latest memory init --force
```
---
## Hive-Mind Consensus
### Queen Types
| Type | Role |
|------|------|
| Strategic Queen | Long-term planning |
| Tactical Queen | Execution coordination |
| Adaptive Queen | Dynamic optimization |
### Worker Types (8)
`researcher`, `coder`, `analyst`, `tester`, `architect`, `reviewer`, `optimizer`, `documenter`
### Consensus Mechanisms
| Mechanism | Fault Tolerance | Use Case |
|-----------|-----------------|----------|
| `byzantine` | f < n/3 faulty | Adversarial |
| `raft` | f < n/2 failed | Leader-based |
| `gossip` | Eventually consistent | Large scale |
| `crdt` | Conflict-free | Distributed |
| `quorum` | Configurable | Flexible |
### Hive-Mind Commands
```bash
# Initialize
npx @claude-flow/cli@latest hive-mind init --queen-type strategic
# Status
npx @claude-flow/cli@latest hive-mind status
# Spawn workers
npx @claude-flow/cli@latest hive-mind spawn --count 5 --type worker
# Consensus
npx @claude-flow/cli@latest hive-mind consensus --propose "task"
```
---
## Performance Targets
| Metric | Target | Status |
|--------|--------|--------|
| HNSW Search | 150x-12,500x faster | ✅ Implemented |
| Memory Reduction | 50-75% | ✅ Implemented (3.92x) |
| SONA Integration | Pattern learning | ✅ Implemented |
| Flash Attention | 2.49x-7.47x | 🔄 In Progress |
| MCP Response | <100ms | Achieved |
| CLI Startup | <500ms | Achieved |
| SONA Adaptation | <0.05ms | 🔄 In Progress |
| Graph Build (1k) | <200ms | 2.78ms (71.9x headroom) |
| PageRank (1k) | <100ms | 12.21ms (8.2x headroom) |
| Insight Recording | <5ms/each | 0.12ms (41x headroom) |
| Consolidation | <500ms | 0.26ms (1,955x headroom) |
| Knowledge Transfer | <100ms | 1.25ms (80x headroom) |
---
## Integration Ecosystem
### Integrated Packages
| Package | Version | Purpose |
|---------|---------|---------|
| agentic-flow | 3.0.0-alpha.1 | Core coordination + ReasoningBank + Router |
| agentdb | 3.0.0-alpha.10 | Vector database + 8 controllers |
| @ruvector/attention | 0.1.3 | Flash attention |
| @ruvector/sona | 0.1.5 | Neural learning |
### Optional Integrations
| Package | Command |
|---------|---------|
| ruv-swarm | `npx ruv-swarm mcp start` |
| flow-nexus | `npx flow-nexus@latest mcp start` |
| agentic-jujutsu | `npx agentic-jujutsu@latest` |
### MCP Server Setup
```bash
# Add Claude Flow MCP
claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
# Optional servers
claude mcp add ruv-swarm -- npx -y ruv-swarm mcp start
claude mcp add flow-nexus -- npx -y flow-nexus@latest mcp start
```
---
## Quick Reference
### Essential Commands
```bash
# Setup
npx @claude-flow/cli@latest init --wizard
npx @claude-flow/cli@latest daemon start
npx @claude-flow/cli@latest doctor --fix
# Swarm
npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8
npx @claude-flow/cli@latest swarm status
# Agents
npx @claude-flow/cli@latest agent spawn -t coder
npx @claude-flow/cli@latest agent list
# Memory
npx @claude-flow/cli@latest memory search --query "patterns"
# Hooks
npx @claude-flow/cli@latest hooks pre-task --description "task"
npx @claude-flow/cli@latest hooks worker dispatch --trigger optimize
```
### File Structure
```
.claude-flow/
├── config.yaml # Runtime configuration
├── CAPABILITIES.md # This file
├── data/ # Memory storage
├── logs/ # Operation logs
├── sessions/ # Session state
├── hooks/ # Custom hooks
├── agents/ # Agent configs
└── workflows/ # Workflow templates
```
---
**Full Documentation**: https://github.com/ruvnet/claude-flow
**Issues**: https://github.com/ruvnet/claude-flow/issues

43
.claude-flow/config.yaml Normal file
View File

@ -0,0 +1,43 @@
# RuFlo V3 Runtime Configuration
# Generated: 2026-03-06T01:38:48.235Z
version: "3.0.0"
swarm:
topology: hierarchical-mesh
maxAgents: 15
autoScale: true
coordinationStrategy: consensus
memory:
backend: hybrid
enableHNSW: true
persistPath: .claude-flow/data
cacheSize: 100
# ADR-049: Self-Learning Memory
learningBridge:
enabled: true
sonaMode: balanced
confidenceDecayRate: 0.005
accessBoostAmount: 0.03
consolidationThreshold: 10
memoryGraph:
enabled: true
pageRankDamping: 0.85
maxNodes: 5000
similarityThreshold: 0.8
agentScopes:
enabled: true
defaultScope: project
neural:
enabled: true
modelPath: .claude-flow/neural
hooks:
enabled: true
autoExecute: true
mcp:
autoStart: false
port: 3000

View File

@ -0,0 +1,17 @@
{
"initialized": "2026-03-06T01:38:48.235Z",
"routing": {
"accuracy": 0,
"decisions": 0
},
"patterns": {
"shortTerm": 0,
"longTerm": 0,
"quality": 0
},
"sessions": {
"total": 0,
"current": null
},
"_note": "Intelligence grows as you use Claude Flow"
}

View File

@ -0,0 +1,18 @@
{
"timestamp": "2026-03-06T01:38:48.235Z",
"processes": {
"agentic_flow": 0,
"mcp_server": 0,
"estimated_agents": 0
},
"swarm": {
"active": false,
"agent_count": 0,
"coordination_active": false
},
"integration": {
"agentic_flow_active": false,
"mcp_active": false
},
"_initialized": true
}

View File

@ -0,0 +1,26 @@
{
"version": "3.0.0",
"initialized": "2026-03-06T01:38:48.235Z",
"domains": {
"completed": 0,
"total": 5,
"status": "INITIALIZING"
},
"ddd": {
"progress": 0,
"modules": 0,
"totalFiles": 0,
"totalLines": 0
},
"swarm": {
"activeAgents": 0,
"maxAgents": 15,
"topology": "hierarchical-mesh"
},
"learning": {
"status": "READY",
"patternsLearned": 0,
"sessionsCompleted": 0
},
"_note": "Metrics will update as you use Claude Flow. Run: npx @claude-flow/cli@latest daemon start"
}

View File

@ -0,0 +1,8 @@
{
"initialized": "2026-03-06T01:38:48.236Z",
"status": "PENDING",
"cvesFixed": 0,
"totalCves": 3,
"lastScan": null,
"_note": "Run: npx @claude-flow/cli@latest security scan"
}

22
.mcp.json Normal file
View File

@ -0,0 +1,22 @@
{
"mcpServers": {
"claude-flow": {
"command": "npx",
"args": [
"-y",
"@claude-flow/cli@latest",
"mcp",
"start"
],
"env": {
"npm_config_update_notifier": "false",
"CLAUDE_FLOW_MODE": "v3",
"CLAUDE_FLOW_HOOKS_ENABLED": "true",
"CLAUDE_FLOW_TOPOLOGY": "hierarchical-mesh",
"CLAUDE_FLOW_MAX_AGENTS": "15",
"CLAUDE_FLOW_MEMORY_BACKEND": "hybrid"
},
"autoStart": false
}
}
}

188
CLAUDE.md Normal file
View File

@ -0,0 +1,188 @@
# Claude Code Configuration - RuFlo V3
## Behavioral Rules (Always Enforced)
- Do what has been asked; nothing more, nothing less
- NEVER create files unless they're absolutely necessary for achieving your goal
- ALWAYS prefer editing an existing file to creating a new one
- NEVER proactively create documentation files (*.md) or README files unless explicitly requested
- NEVER save working files, text/mds, or tests to the root folder
- Never continuously check status after spawning a swarm — wait for results
- ALWAYS read a file before editing it
- NEVER commit secrets, credentials, or .env files
## File Organization
- NEVER save to root folder — use the directories below
- Use `/src` for source code files
- Use `/tests` for test files
- Use `/docs` for documentation and markdown files
- Use `/config` for configuration files
- Use `/scripts` for utility scripts
- Use `/examples` for example code
## Project Architecture
- Follow Domain-Driven Design with bounded contexts
- Keep files under 500 lines
- Use typed interfaces for all public APIs
- Prefer TDD London School (mock-first) for new code
- Use event sourcing for state changes
- Ensure input validation at system boundaries
### Project Config
- **Topology**: hierarchical-mesh
- **Max Agents**: 15
- **Memory**: hybrid
- **HNSW**: Enabled
- **Neural**: Enabled
## Build & Test
```bash
# Build
npm run build
# Test
npm test
# Lint
npm run lint
```
- ALWAYS run tests after making code changes
- ALWAYS verify build succeeds before committing
## Security Rules
- NEVER hardcode API keys, secrets, or credentials in source files
- NEVER commit .env files or any file containing secrets
- Always validate user input at system boundaries
- Always sanitize file paths to prevent directory traversal
- Run `npx @claude-flow/cli@latest security scan` after security-related changes
## Concurrency: 1 MESSAGE = ALL RELATED OPERATIONS
- All operations MUST be concurrent/parallel in a single message
- Use Claude Code's Task tool for spawning agents, not just MCP
- ALWAYS batch ALL todos in ONE TodoWrite call (5-10+ minimum)
- ALWAYS spawn ALL agents in ONE message with full instructions via Task tool
- ALWAYS batch ALL file reads/writes/edits in ONE message
- ALWAYS batch ALL Bash commands in ONE message
## Swarm Orchestration
- MUST initialize the swarm using CLI tools when starting complex tasks
- MUST spawn concurrent agents using Claude Code's Task tool
- Never use CLI tools alone for execution — Task tool agents do the actual work
- MUST call CLI tools AND Task tool in ONE message for complex work
### 3-Tier Model Routing (ADR-026)
| Tier | Handler | Latency | Cost | Use Cases |
|------|---------|---------|------|-----------|
| **1** | Agent Booster (WASM) | <1ms | $0 | Simple transforms (varconst, add types) Skip LLM |
| **2** | Haiku | ~500ms | $0.0002 | Simple tasks, low complexity (<30%) |
| **3** | Sonnet/Opus | 2-5s | $0.003-0.015 | Complex reasoning, architecture, security (>30%) |
- Always check for `[AGENT_BOOSTER_AVAILABLE]` or `[TASK_MODEL_RECOMMENDATION]` before spawning agents
- Use Edit tool directly when `[AGENT_BOOSTER_AVAILABLE]`
## Swarm Configuration & Anti-Drift
- ALWAYS use hierarchical topology for coding swarms
- Keep maxAgents at 6-8 for tight coordination
- Use specialized strategy for clear role boundaries
- Use `raft` consensus for hive-mind (leader maintains authoritative state)
- Run frequent checkpoints via `post-task` hooks
- Keep shared memory namespace for all agents
```bash
npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized
```
## Swarm Execution Rules
- ALWAYS use `run_in_background: true` for all agent Task calls
- ALWAYS put ALL agent Task calls in ONE message for parallel execution
- After spawning, STOP — do NOT add more tool calls or check status
- Never poll TaskOutput or check swarm status — trust agents to return
- When agent results arrive, review ALL results before proceeding
## V3 CLI Commands
### Core Commands
| Command | Subcommands | Description |
|---------|-------------|-------------|
| `init` | 4 | Project initialization |
| `agent` | 8 | Agent lifecycle management |
| `swarm` | 6 | Multi-agent swarm coordination |
| `memory` | 11 | AgentDB memory with HNSW search |
| `task` | 6 | Task creation and lifecycle |
| `session` | 7 | Session state management |
| `hooks` | 17 | Self-learning hooks + 12 workers |
| `hive-mind` | 6 | Byzantine fault-tolerant consensus |
### Quick CLI Examples
```bash
npx @claude-flow/cli@latest init --wizard
npx @claude-flow/cli@latest agent spawn -t coder --name my-coder
npx @claude-flow/cli@latest swarm init --v3-mode
npx @claude-flow/cli@latest memory search --query "authentication patterns"
npx @claude-flow/cli@latest doctor --fix
```
## Available Agents (60+ Types)
### Core Development
`coder`, `reviewer`, `tester`, `planner`, `researcher`
### Specialized
`security-architect`, `security-auditor`, `memory-specialist`, `performance-engineer`
### Swarm Coordination
`hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator`
### GitHub & Repository
`pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager`
### SPARC Methodology
`sparc-coord`, `sparc-coder`, `specification`, `pseudocode`, `architecture`
## Memory Commands Reference
```bash
# Store (REQUIRED: --key, --value; OPTIONAL: --namespace, --ttl, --tags)
npx @claude-flow/cli@latest memory store --key "pattern-auth" --value "JWT with refresh" --namespace patterns
# Search (REQUIRED: --query; OPTIONAL: --namespace, --limit, --threshold)
npx @claude-flow/cli@latest memory search --query "authentication patterns"
# List (OPTIONAL: --namespace, --limit)
npx @claude-flow/cli@latest memory list --namespace patterns --limit 10
# Retrieve (REQUIRED: --key; OPTIONAL: --namespace)
npx @claude-flow/cli@latest memory retrieve --key "pattern-auth" --namespace patterns
```
## Quick Setup
```bash
claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
npx @claude-flow/cli@latest daemon start
npx @claude-flow/cli@latest doctor --fix
```
## Claude Code vs CLI Tools
- Claude Code's Task tool handles ALL execution: agents, file ops, code generation, git
- CLI tools handle coordination via Bash: swarm init, memory, hooks, routing
- NEVER use CLI tools as a substitute for Task tool agents
## Support
- Documentation: https://github.com/ruvnet/claude-flow
- Issues: https://github.com/ruvnet/claude-flow/issues

View File

@ -27,8 +27,8 @@ type (
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"` Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
} }
ToggleRedemptionCodeStatusRequest { ToggleRedemptionCodeStatusRequest {
Id int64 `json:"id" validate:"required"` Id int64 `json:"id" validate:"required"`
Status int64 `json:"status" validate:"oneof=0 1"` Status int64 `json:"status" validate:"oneof=0 1"`
} }
DeleteRedemptionCodeRequest { DeleteRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"` Id int64 `json:"id" validate:"required"`
@ -93,3 +93,4 @@ service ppanel {
@handler GetRedemptionRecordList @handler GetRedemptionRecordList
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse) get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
} }

View File

@ -127,6 +127,14 @@ service ppanel {
@handler UpdateVerifyCodeConfig @handler UpdateVerifyCodeConfig
put /verify_code_config (VerifyCodeConfig) put /verify_code_config (VerifyCodeConfig)
@doc "Get Signature Config"
@handler GetSignatureConfig
get /signature_config returns (SignatureConfig)
@doc "Update Signature Config"
@handler UpdateSignatureConfig
put /signature_config (SignatureConfig)
@doc "PreView Node Multiplier" @doc "PreView Node Multiplier"
@handler PreViewNodeMultiplier @handler PreViewNodeMultiplier
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)

View File

@ -15,18 +15,18 @@ import (
type ( type (
// GetUserListRequest // GetUserListRequest
GetUserListRequest { GetUserListRequest {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
Search string `form:"search,omitempty"` Search string `form:"search,omitempty"`
UserId *int64 `form:"user_id,omitempty"` UserId *int64 `form:"user_id,omitempty"`
Unscoped bool `form:"unscoped,omitempty"` Unscoped bool `form:"unscoped,omitempty"`
SubscribeId *int64 `form:"subscribe_id,omitempty"` SubscribeId *int64 `form:"subscribe_id,omitempty"`
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"` UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
ShortCode string `form:"short_code,omitempty"` ShortCode string `form:"short_code,omitempty"`
FamilyJoined *bool `form:"family_joined,omitempty"` FamilyJoined *bool `form:"family_joined,omitempty"`
FamilyStatus string `form:"family_status,omitempty"` FamilyStatus string `form:"family_status,omitempty"`
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"` FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
FamilyId *int64 `form:"family_id,omitempty"` FamilyId *int64 `form:"family_id,omitempty"`
} }
// GetUserListResponse // GetUserListResponse
GetUserListResponse { GetUserListResponse {
@ -366,3 +366,4 @@ service ppanel {
@handler DissolveFamily @handler DissolveFamily
put /family/dissolve (DissolveFamilyRequest) put /family/dissolve (DissolveFamilyRequest)
} }

View File

@ -24,6 +24,7 @@ type (
Invite InviteConfig `json:"invite"` Invite InviteConfig `json:"invite"`
Currency Currency `json:"currency"` Currency Currency `json:"currency"`
Subscribe SubscribeConfig `json:"subscribe"` Subscribe SubscribeConfig `json:"subscribe"`
Signature SignatureConfig `json:"signature"`
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"` VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
OAuthMethods []string `json:"oauth_methods"` OAuthMethods []string `json:"oauth_methods"`
WebAd bool `json:"web_ad"` WebAd bool `json:"web_ad"`

View File

@ -30,3 +30,4 @@ service ppanel {
@handler RedeemCode @handler RedeemCode
post / (RedeemCodeRequest) returns (RedeemCodeResponse) post / (RedeemCodeRequest) returns (RedeemCodeResponse)
} }

View File

@ -14,11 +14,9 @@ type (
QuerySubscribeListRequest { QuerySubscribeListRequest {
Language string `form:"language"` Language string `form:"language"`
} }
QueryUserSubscribeNodeListResponse { QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"` List []UserSubscribeInfo `json:"list"`
} }
UserSubscribeInfo { UserSubscribeInfo {
Id int64 `json:"id"` Id int64 `json:"id"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
@ -41,7 +39,6 @@ type (
IsTryOut bool `json:"is_try_out"` IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"` Nodes []*UserSubscribeNodeInfo `json:"nodes"`
} }
UserSubscribeNodeInfo { UserSubscribeNodeInfo {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -75,3 +72,4 @@ service ppanel {
@handler QueryUserSubscribeNodeList @handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse) get /node/list returns (QueryUserSubscribeNodeListResponse)
} }

View File

@ -66,7 +66,6 @@ type (
UnbindOAuthRequest { UnbindOAuthRequest {
Method string `json:"method"` Method string `json:"method"`
} }
GetLoginLogRequest { GetLoginLogRequest {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
@ -95,21 +94,17 @@ type (
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"` Code string `json:"code" validate:"required"`
} }
GetDeviceListResponse {
GetDeviceListResponse { List []UserDevice `json:"list"`
List []UserDevice `json:"list"` Total int64 `json:"total"`
Total int64 `json:"total"` }
} UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
UnbindDeviceRequest { }
Id int64 `json:"id" validate:"required"`
}
UpdateUserSubscribeNoteRequest { UpdateUserSubscribeNoteRequest {
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
Note string `json:"note" validate:"max=500"` Note string `json:"note" validate:"max=500"`
} }
UpdateUserRulesRequest { UpdateUserRulesRequest {
Rules []string `json:"rules" validate:"required"` Rules []string `json:"rules" validate:"required"`
} }
@ -135,23 +130,20 @@ type (
List []WithdrawalLog `json:"list"` List []WithdrawalLog `json:"list"`
Total int64 `json:"total"` Total int64 `json:"total"`
} }
GetDeviceOnlineStatsResponse {
WeeklyStats []WeeklyStat `json:"weekly_stats"`
GetDeviceOnlineStatsResponse { ConnectionRecords ConnectionRecords `json:"connection_records"`
WeeklyStats []WeeklyStat `json:"weekly_stats"` }
ConnectionRecords ConnectionRecords `json:"connection_records"` WeeklyStat {
} Day int `json:"day"`
DayName string `json:"day_name"`
WeeklyStat { Hours float64 `json:"hours"`
Day int `json:"day"` }
DayName string `json:"day_name"` ConnectionRecords {
Hours float64 `json:"hours"` CurrentContinuousDays int64 `json:"current_continuous_days"`
} HistoryContinuousDays int64 `json:"history_continuous_days"`
ConnectionRecords { LongestSingleConnection int64 `json:"longest_single_connection"`
CurrentContinuousDays int64 `json:"current_continuous_days"` }
HistoryContinuousDays int64 `json:"history_continuous_days"`
LongestSingleConnection int64 `json:"longest_single_connection"`
}
) )
@server ( @server (
@ -248,9 +240,9 @@ service ppanel {
@handler UpdateBindEmail @handler UpdateBindEmail
put /bind_email (UpdateBindEmailRequest) put /bind_email (UpdateBindEmailRequest)
@doc "Get Device List" @doc "Get Device List"
@handler GetDeviceList @handler GetDeviceList
get /devices returns (GetDeviceListResponse) get /devices returns (GetDeviceListResponse)
@doc "Unbind Device" @doc "Unbind Device"
@handler UnbindDevice @handler UnbindDevice
@ -272,23 +264,23 @@ service ppanel {
@handler QueryWithdrawalLog @handler QueryWithdrawalLog
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
@doc "Device Online Statistics" @doc "Device Online Statistics"
@handler DeviceOnlineStatistics @handler DeviceOnlineStatistics
get /device_online_statistics returns (GetDeviceOnlineStatsResponse) get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
} }
@server(
prefix: v1/public/user @server (
group: public/user/ws prefix: v1/public/user
middleware: AuthMiddleware group: public/user/ws
middleware: AuthMiddleware
) )
service ppanel { service ppanel {
@doc "Webosocket Device Connect" @doc "Webosocket Device Connect"
@handler DeviceWsConnect @handler DeviceWsConnect
get /device_ws_connect get /device_ws_connect
} }

View File

@ -84,6 +84,9 @@ type (
VerifyCodeLimit int64 `json:"verify_code_limit"` VerifyCodeLimit int64 `json:"verify_code_limit"`
VerifyCodeInterval int64 `json:"verify_code_interval"` VerifyCodeInterval int64 `json:"verify_code_interval"`
} }
SignatureConfig {
EnableSignature bool `json:"enable_signature"`
}
PubilcVerifyCodeConfig { PubilcVerifyCodeConfig {
VerifyCodeInterval int64 `json:"verify_code_interval"` VerifyCodeInterval int64 `json:"verify_code_interval"`
} }
@ -217,7 +220,7 @@ type (
CurrencySymbol string `json:"currency_symbol"` CurrencySymbol string `json:"currency_symbol"`
} }
SubscribeDiscount { SubscribeDiscount {
Quantity int64 `json:"quantity"` Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
} }
Subscribe { Subscribe {
@ -493,26 +496,26 @@ type (
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
UserSubscribe { UserSubscribe {
Id int64 `json:"id"` Id int64 `json:"id"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"` OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"` Subscribe Subscribe `json:"subscribe"`
StartTime int64 `json:"start_time"` StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"` ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"` FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"` ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"` Traffic int64 `json:"traffic"`
Download int64 `json:"download"` Download int64 `json:"download"`
Upload int64 `json:"upload"` Upload int64 `json:"upload"`
Token string `json:"token"` Token string `json:"token"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
EntitlementSource string `json:"entitlement_source"` EntitlementSource string `json:"entitlement_source"`
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"` EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
ReadOnly bool `json:"read_only"` ReadOnly bool `json:"read_only"`
Short string `json:"short"` Short string `json:"short"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
UserAffiliate { UserAffiliate {
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
@ -555,15 +558,15 @@ type (
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
FamilyMemberItem { FamilyMemberItem {
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
Role uint8 `json:"role"` Role uint8 `json:"role"`
RoleName string `json:"role_name"` RoleName string `json:"role_name"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
StatusName string `json:"status_name"` StatusName string `json:"status_name"`
JoinSource string `json:"join_source"` JoinSource string `json:"join_source"`
JoinedAt int64 `json:"joined_at"` JoinedAt int64 `json:"joined_at"`
LeftAt int64 `json:"left_at,omitempty"` LeftAt int64 `json:"left_at,omitempty"`
} }
FamilyDetail { FamilyDetail {
Summary FamilySummary `json:"summary"` Summary FamilySummary `json:"summary"`
@ -737,7 +740,6 @@ type (
List []SubscribeGroup `json:"list"` List []SubscribeGroup `json:"list"`
Total int64 `json:"total"` Total int64 `json:"total"`
} }
GetUserSubscribeTrafficLogsRequest { GetUserSubscribeTrafficLogsRequest {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
@ -915,3 +917,4 @@ type (
UserSubscribeId int64 `json:"user_subscribe_id"` UserSubscribeId int64 `json:"user_subscribe_id"`
} }
) )

View File

@ -40,6 +40,21 @@ Redis:
ReadTimeout: 3 # 读操作超时时间建议2-3秒 ReadTimeout: 3 # 读操作超时时间建议2-3秒
WriteTimeout: 3 # 写操作超时时间建议2-3秒 WriteTimeout: 3 # 写操作超时时间建议2-3秒
AppSignature:
AppSecrets:
android-client: uB4G,XxL2{7b # Android 客户端签名密钥
ios-client: uB4G,XxL2{7b # iOS 客户端签名密钥
web-client: uB4G,XxL2{7b # Web 客户端签名密钥
ValidWindowSeconds: 300 # 签名时间窗口(秒)
SkipPrefixes:
- /v1/notify/ # 支付回调不验签
- /v1/iap/notifications # Apple IAP 回调不验签
- /v1/telegram/webhook # Telegram 回调不验签
- /v1/subscribe/config # 订阅导出不验签
Signature:
EnableSignature: false # 系统签名开关(实际运行会以数据库 system.signature.EnableSignature 为准)
Administrator: Administrator:
Email: admin@ppanel.dev # 后台登录邮箱,请修改 Email: admin@ppanel.dev # 后台登录邮箱,请修改
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码 Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码

View File

@ -13,6 +13,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
Invite(svc) Invite(svc)
Verify(svc) Verify(svc)
Subscribe(svc) Subscribe(svc)
Signature(svc)
Register(svc) Register(svc)
Mobile(svc) Mobile(svc)
Currency(svc) Currency(svc)

View File

@ -0,0 +1,4 @@
DELETE
FROM `system`
WHERE `category` = 'signature'
AND `key` = 'EnableSignature';

View File

@ -0,0 +1,14 @@
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
SELECT 'signature',
'EnableSignature',
'false',
'bool',
'Enable signature verification for public APIs',
NOW(3),
NOW(3)
WHERE NOT EXISTS (
SELECT 1
FROM `system`
WHERE `category` = 'signature'
AND `key` = 'EnableSignature'
);

22
initialize/signature.go Normal file
View File

@ -0,0 +1,22 @@
package initialize
import (
"context"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
)
func Signature(svc *svc.ServiceContext) {
logger.Debug("Signature config initialization")
configs, err := svc.SystemModel.GetSignatureConfig(context.Background())
if err != nil {
logger.Error("[Init Signature Config] Get Signature Config Error: ", logger.Field("error", err.Error()))
return
}
var signatureConfig config.Signature
tool.SystemConfigSliceReflectToStruct(configs, &signatureConfig)
svc.Config.Signature = signatureConfig
}

View File

@ -36,6 +36,9 @@ const TosConfigKey = "system:tos_config"
// VerifyCodeConfigKey Verify Code Config Key // VerifyCodeConfigKey Verify Code Config Key
const VerifyCodeConfigKey = "system:verify_code_config" const VerifyCodeConfigKey = "system:verify_code_config"
// SignatureConfigKey Signature Config Key
const SignatureConfigKey = "system:signature_config"
// SessionIdKey cache session key // SessionIdKey cache session key
const SessionIdKey = "auth:session_id" const SessionIdKey = "auth:session_id"

View File

@ -5,36 +5,39 @@ import (
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/orm"
"github.com/perfect-panel/server/pkg/signature"
"github.com/perfect-panel/server/pkg/trace" "github.com/perfect-panel/server/pkg/trace"
) )
type Config struct { type Config struct {
Model string `yaml:"Model" default:"prod"` Model string `yaml:"Model" default:"prod"`
Host string `yaml:"Host" default:"0.0.0.0"` Host string `yaml:"Host" default:"0.0.0.0"`
Port int `yaml:"Port" default:"8080"` Port int `yaml:"Port" default:"8080"`
Debug bool `yaml:"Debug" default:"false"` Debug bool `yaml:"Debug" default:"false"`
TLS TLS `yaml:"TLS"` TLS TLS `yaml:"TLS"`
JwtAuth JwtAuth `yaml:"JwtAuth"` JwtAuth JwtAuth `yaml:"JwtAuth"`
Logger logger.LogConf `yaml:"Logger"` Logger logger.LogConf `yaml:"Logger"`
MySQL orm.Config `yaml:"MySQL"` MySQL orm.Config `yaml:"MySQL"`
Redis RedisConfig `yaml:"Redis"` Redis RedisConfig `yaml:"Redis"`
Site SiteConfig `yaml:"Site"` Site SiteConfig `yaml:"Site"`
Node NodeConfig `yaml:"Node"` Node NodeConfig `yaml:"Node"`
Mobile MobileConfig `yaml:"Mobile"` Mobile MobileConfig `yaml:"Mobile"`
Email EmailConfig `yaml:"Email"` Email EmailConfig `yaml:"Email"`
Device DeviceConfig `yaml:"device"` Device DeviceConfig `yaml:"device"`
Verify Verify `yaml:"Verify"` AppSignature signature.SignatureConf `yaml:"AppSignature"`
VerifyCode VerifyCode `yaml:"VerifyCode"` Signature Signature `yaml:"Signature"`
Register RegisterConfig `yaml:"Register"` Verify Verify `yaml:"Verify"`
Subscribe SubscribeConfig `yaml:"Subscribe"` VerifyCode VerifyCode `yaml:"VerifyCode"`
Invite InviteConfig `yaml:"Invite"` Register RegisterConfig `yaml:"Register"`
Kutt KuttConfig `yaml:"Kutt"` Subscribe SubscribeConfig `yaml:"Subscribe"`
OpenInstall OpenInstallConfig `yaml:"OpenInstall"` Invite InviteConfig `yaml:"Invite"`
Loki LokiConfig `yaml:"Loki"` Kutt KuttConfig `yaml:"Kutt"`
Telegram Telegram `yaml:"Telegram"` OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
Log Log `yaml:"Log"` Loki LokiConfig `yaml:"Loki"`
Currency Currency `yaml:"Currency"` Telegram Telegram `yaml:"Telegram"`
Trace trace.Config `yaml:"Trace"` Log Log `yaml:"Log"`
Currency Currency `yaml:"Currency"`
Trace trace.Config `yaml:"Trace"`
Administrator struct { Administrator struct {
Email string `yaml:"Email" default:"admin@ppanel.dev"` Email string `yaml:"Email" default:"admin@ppanel.dev"`
Password string `yaml:"Password" default:"password"` Password string `yaml:"Password" default:"password"`
@ -122,6 +125,10 @@ type DeviceConfig struct {
SecuritySecret string `yaml:"security_secret"` SecuritySecret string `yaml:"security_secret"`
} }
type Signature struct {
EnableSignature bool `yaml:"EnableSignature" default:"false"`
}
type SiteConfig struct { type SiteConfig struct {
Host string `yaml:"Host" default:""` Host string `yaml:"Host" default:""`
SiteName string `yaml:"SiteName" default:""` SiteName string `yaml:"SiteName" default:""`

View File

@ -0,0 +1,18 @@
package system
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/system"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Get Signature Config
func GetSignatureConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := system.NewGetSignatureConfigLogic(c.Request.Context(), svcCtx)
resp, err := l.GetSignatureConfig()
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package system
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/system"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Update Signature Config
func UpdateSignatureConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.SignatureConfig
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := system.NewUpdateSignatureConfigLogic(c.Request.Context(), svcCtx)
err := l.UpdateSignatureConfig(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -28,7 +28,6 @@ import (
common "github.com/perfect-panel/server/internal/handler/common" common "github.com/perfect-panel/server/internal/handler/common"
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement" publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
publicDocument "github.com/perfect-panel/server/internal/handler/public/document" publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple"
publicOrder "github.com/perfect-panel/server/internal/handler/public/order" publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment" publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal" publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
@ -237,12 +236,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Filter traffic log details // Filter traffic log details
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx)) adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
// Error message log list
adminLogGroupRouter.GET("/message/error/list", adminLog.GetErrorLogMessageListHandler(serverCtx))
// Error message log detail
adminLogGroupRouter.GET("/message/error/detail", adminLog.GetErrorLogMessageDetailHandler(serverCtx))
} }
adminMarketingGroupRouter := router.Group("/v1/admin/marketing") adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
@ -471,6 +464,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// setting telegram bot // setting telegram bot
adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx)) adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx))
// Get Signature Config
adminSystemGroupRouter.GET("/signature_config", adminSystem.GetSignatureConfigHandler(serverCtx))
// Update Signature Config
adminSystemGroupRouter.PUT("/signature_config", adminSystem.UpdateSignatureConfigHandler(serverCtx))
// Get site config // Get site config
adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx)) adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx))
@ -579,6 +578,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// kick offline user device // kick offline user device
adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx)) adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx))
// Get family detail
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
// Dissolve family
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
// Get family list
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
// Update family max members
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
// Remove family member
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
// Get user list // Get user list
adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx)) adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx))
@ -623,21 +637,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get user subcribe traffic logs // Get user subcribe traffic logs
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
// Get family list
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
// Get family detail
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
// Update family max members
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
// Remove family member
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
// Dissolve family
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
} }
authGroupRouter := router.Group("/v1/auth") authGroupRouter := router.Group("/v1/auth")
@ -650,18 +649,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Check user telephone is exist // Check user telephone is exist
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx)) authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
// Check legacy verification code
authGroupRouter.POST("/check-code", middleware.ApiVersionSwitchHandler(
auth.CheckCodeLegacyV1Handler(serverCtx),
auth.CheckCodeLegacyV2Handler(serverCtx),
))
// User login // User login
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
// Email login
authGroupRouter.POST("/login/email", auth.EmailLoginHandler(serverCtx))
// Device Login // Device Login
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx)) authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
@ -702,17 +692,11 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx)) commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
// Check verification code // Check verification code
commonGroupRouter.POST("/check_verification_code", middleware.ApiVersionSwitchHandler( commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx))
common.CheckVerificationCodeV1Handler(serverCtx),
common.CheckVerificationCodeV2Handler(serverCtx),
))
// Get Client // Get Client
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
// Get Download Link
commonGroupRouter.GET("/client/download", common.GetDownloadLinkHandler(serverCtx))
// Heartbeat // Heartbeat
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx)) commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
@ -733,12 +717,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get Tos Content // Get Tos Content
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx)) commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
// Submit contact info
commonGroupRouter.POST("/contact", common.SubmitContactHandler(serverCtx))
// Report client error log
commonGroupRouter.POST("/log/message/report", common.ReportLogMessageHandler(serverCtx))
} }
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
@ -828,15 +806,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx)) publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
} }
iapAppleGroupRouter := router.Group("/v1/public/iap/apple")
iapAppleGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{
iapAppleGroupRouter.GET("/status", publicIapApple.GetAppleStatusHandler(serverCtx))
iapAppleGroupRouter.POST("/transactions/attach", publicIapApple.AttachAppleTransactionHandler(serverCtx))
iapAppleGroupRouter.POST("/transactions/attach_by_id", publicIapApple.AttachAppleTransactionByIdHandler(serverCtx))
iapAppleGroupRouter.POST("/restore", publicIapApple.RestoreAppleTransactionsHandler(serverCtx))
}
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
@ -961,30 +930,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Query Withdrawal Log // Query Withdrawal Log
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
// Bind Email With Verification
publicUserGroupRouter.POST("/bind_email_with_verification", publicUser.BindEmailWithVerificationHandler(serverCtx))
// Bind Invite Code
publicUserGroupRouter.POST("/bind_invite_code", publicUser.BindInviteCodeHandler(serverCtx))
// Get Subscribe Status
publicUserGroupRouter.POST("/subscribe_status", publicUser.GetSubscribeStatusHandler(serverCtx))
// Delete Account
publicUserGroupRouter.POST("/delete_account", publicUser.DeleteAccountHandler(serverCtx))
// Get agent realtime data
publicUserGroupRouter.GET("/agent/realtime", publicUser.GetAgentRealtimeHandler(serverCtx))
// Get agent downloads data
publicUserGroupRouter.GET("/agent/downloads", publicUser.GetAgentDownloadsHandler(serverCtx))
// Get user invite statistics
publicUserGroupRouter.GET("/invite/stats", publicUser.GetUserInviteStatsHandler(serverCtx))
// Get invite sales data
publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx))
} }
publicUserWsGroupRouter := router.Group("/v1/public/user") publicUserWsGroupRouter := router.Group("/v1/public/user")
@ -1015,10 +960,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx)) serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
} }
serverV2GroupRouter := router.Group("/v2/server") serverGroupRouter := router.Group("/v2/server")
{ {
// Get Server Protocol Config // Get Server Protocol Config
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx)) serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
} }
} }

View File

@ -0,0 +1,38 @@
package system
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetSignatureConfigLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get Signature Config
func NewGetSignatureConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSignatureConfigLogic {
return &GetSignatureConfigLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetSignatureConfigLogic) GetSignatureConfig() (resp *types.SignatureConfig, err error) {
resp = &types.SignatureConfig{}
configs, err := l.svcCtx.SystemModel.GetSignatureConfig(l.ctx)
if err != nil {
l.Errorw("[GetSignatureConfig] Database query error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get signature config error: %v", err.Error())
}
tool.SystemConfigSliceReflectToStruct(configs, resp)
return resp, nil
}

View File

@ -0,0 +1,53 @@
package system
import (
"context"
"reflect"
"github.com/perfect-panel/server/initialize"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/system"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type UpdateSignatureConfigLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Update Signature Config
func NewUpdateSignatureConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSignatureConfigLogic {
return &UpdateSignatureConfigLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateSignatureConfigLogic) UpdateSignatureConfig(req *types.SignatureConfig) error {
v := reflect.ValueOf(*req)
t := v.Type()
err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error {
for i := 0; i < v.NumField(); i++ {
fieldName := t.Field(i).Name
fieldValue := tool.ConvertValueToString(v.Field(i))
if err := db.Model(&system.System{}).Where("`category` = 'signature' and `key` = ?", fieldName).Update("value", fieldValue).Error; err != nil {
return err
}
}
return l.svcCtx.Redis.Del(l.ctx, config.SignatureConfigKey, config.GlobalConfigKey).Err()
})
if err != nil {
l.Errorw("[UpdateSignatureConfig] update signature config error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update signature config error: %v", err.Error())
}
initialize.Signature(l.svcCtx)
return nil
}

View File

@ -47,6 +47,7 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
tool.DeepCopy(&resp.Auth.Email, l.svcCtx.Config.Email) tool.DeepCopy(&resp.Auth.Email, l.svcCtx.Config.Email)
tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile) tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile)
tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register) tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register)
tool.DeepCopy(&resp.Signature, l.svcCtx.Config.Signature)
tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify) tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify)
tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite) tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite)
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency) tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)

View File

@ -113,16 +113,17 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
} }
nodeIds := tool.StringToInt64Slice(subDetails.Nodes) nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
tags := strings.Split(subDetails.NodeTags, ",") tags := normalizeSubscribeNodeTags(subDetails.NodeTags)
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
enable := true enable := true
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
Page: 0, Page: 1,
Size: 1000, Size: 1000,
NodeId: nodeIds, NodeId: nodeIds,
Tag: tags,
Enabled: &enable, // Only get enabled nodes Enabled: &enable, // Only get enabled nodes
}) })
@ -213,3 +214,21 @@ func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.
return userSub, nil return userSub, nil
} }
func normalizeSubscribeNodeTags(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
cleaned := make([]string, 0, len(parts))
for _, tag := range parts {
trimmed := strings.TrimSpace(tag)
if trimmed == "" {
continue
}
cleaned = append(cleaned, trimmed)
}
return tool.RemoveDuplicateElements(cleaned...)
}

View File

@ -23,3 +23,11 @@ func TestFillUserSubscribeInfoEntitlementFields(t *testing.T) {
require.Equal(t, int64(3001), sub.EntitlementOwnerUserId) require.Equal(t, int64(3001), sub.EntitlementOwnerUserId)
require.True(t, sub.ReadOnly) require.True(t, sub.ReadOnly)
} }
func TestNormalizeSubscribeNodeTags(t *testing.T) {
tags := normalizeSubscribeNodeTags("美国, 日本, , 美国, ,日本")
require.Equal(t, []string{"美国", "日本"}, tags)
empty := normalizeSubscribeNodeTags("")
require.Nil(t, empty)
}

View File

@ -15,7 +15,10 @@ func CorsMiddleware(c *gin.Context) {
} }
// c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host) // c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host)
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header") c.Writer.Header().Set(
"Access-Control-Allow-Headers",
"Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header, X-Signature-Enabled, X-App-Id, X-Timestamp, X-Nonce, X-Signature",
)
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Max-Age", "172800") c.Writer.Header().Set("Access-Control-Max-Age", "172800")

View File

@ -0,0 +1,137 @@
package middleware
import (
"bytes"
"io"
"strings"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
"github.com/perfect-panel/server/pkg/signature"
"github.com/perfect-panel/server/pkg/xerr"
)
var (
publicSignaturePrefixes = []string{
"/v1/auth",
"/v1/auth/oauth",
"/v1/common",
"/v1/public",
}
defaultSignatureSkipPrefixes = []string{
"/v1/notify/",
"/v1/iap/notifications",
"/v1/telegram/webhook",
"/v1/subscribe/config",
}
)
func SignatureMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
skipPrefixes := collectSignatureSkipPrefixes(srvCtx)
return func(c *gin.Context) {
path := c.Request.URL.Path
if !isPublicSignaturePath(path) {
c.Next()
return
}
for _, prefix := range skipPrefixes {
if strings.HasPrefix(path, prefix) {
c.Next()
return
}
}
if !srvCtx.Config.Signature.EnableSignature {
c.Next()
return
}
if c.GetHeader("X-Signature-Enabled") != "1" {
c.Next()
return
}
appId := c.GetHeader("X-App-Id")
if appId == "" {
result.HttpResult(c, nil, xerr.NewErrCode(xerr.InvalidAccess))
c.Abort()
return
}
timestamp := c.GetHeader("X-Timestamp")
nonce := c.GetHeader("X-Nonce")
sig := c.GetHeader("X-Signature")
if timestamp == "" || nonce == "" || sig == "" {
result.HttpResult(c, nil, xerr.NewErrCode(xerr.SignatureMissing))
c.Abort()
return
}
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
sts := signature.BuildStringToSign(
c.Request.Method,
path,
c.Request.URL.RawQuery,
bodyBytes,
appId,
timestamp,
nonce,
)
if err := srvCtx.SignatureValidator.Validate(c.Request.Context(), appId, timestamp, nonce, sig, sts); err != nil {
result.HttpResult(c, nil, xerr.NewErrCode(mapSignatureErr(err)))
c.Abort()
return
}
c.Next()
}
}
func isPublicSignaturePath(path string) bool {
for _, prefix := range publicSignaturePrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
func collectSignatureSkipPrefixes(srvCtx *svc.ServiceContext) []string {
prefixSet := make(map[string]struct{}, len(defaultSignatureSkipPrefixes)+len(srvCtx.Config.AppSignature.SkipPrefixes)+1)
for _, prefix := range defaultSignatureSkipPrefixes {
prefixSet[prefix] = struct{}{}
}
for _, prefix := range srvCtx.Config.AppSignature.SkipPrefixes {
if strings.TrimSpace(prefix) == "" {
continue
}
prefixSet[prefix] = struct{}{}
}
if path := strings.TrimSpace(srvCtx.Config.Subscribe.SubscribePath); path != "" {
prefixSet[path] = struct{}{}
}
prefixes := make([]string, 0, len(prefixSet))
for prefix := range prefixSet {
prefixes = append(prefixes, prefix)
}
return prefixes
}
func mapSignatureErr(err error) uint32 {
switch err {
case signature.ErrSignatureMissing:
return xerr.SignatureMissing
case signature.ErrSignatureExpired:
return xerr.SignatureExpired
case signature.ErrSignatureReplay:
return xerr.SignatureReplay
default:
return xerr.SignatureInvalid
}
}

View File

@ -0,0 +1,239 @@
package middleware
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/signature"
"github.com/perfect-panel/server/pkg/xerr"
)
type testNonceStore struct {
seen map[string]bool
}
func newTestNonceStore() *testNonceStore {
return &testNonceStore{seen: map[string]bool{}}
}
func (s *testNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, _ int64) (bool, error) {
key := appId + ":" + nonce
if s.seen[key] {
return true, nil
}
s.seen[key] = true
return false, nil
}
func makeTestSignature(secret, sts string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(sts))
return hex.EncodeToString(mac.Sum(nil))
}
func newTestServiceContext() *svc.ServiceContext {
conf := config.Config{}
conf.Signature.EnableSignature = true
conf.AppSignature = signature.SignatureConf{
AppSecrets: map[string]string{
"web-client": "test-secret",
},
ValidWindowSeconds: 300,
SkipPrefixes: []string{
"/v1/public/health",
},
}
return &svc.ServiceContext{
Config: conf,
SignatureValidator: signature.NewValidator(conf.AppSignature, newTestNonceStore()),
}
}
func newTestServiceContextWithSwitch(enabled bool) *svc.ServiceContext {
svcCtx := newTestServiceContext()
svcCtx.Config.Signature.EnableSignature = enabled
return svcCtx
}
func decodeCode(t *testing.T, body []byte) uint32 {
t.Helper()
var resp struct {
Code uint32 `json:"code"`
}
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("unmarshal response failed: %v", err)
}
return resp.Code
}
func TestSignatureMiddlewareMissingAppID(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
req.Header.Set("X-Signature-Enabled", "1")
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if code := decodeCode(t, resp.Body.Bytes()); code != xerr.InvalidAccess {
t.Fatalf("expected InvalidAccess(%d), got %d", xerr.InvalidAccess, code)
}
}
func TestSignatureMiddlewareMissingSignatureHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
req.Header.Set("X-Signature-Enabled", "1")
req.Header.Set("X-App-Id", "web-client")
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if code := decodeCode(t, resp.Body.Bytes()); code != xerr.SignatureMissing {
t.Fatalf("expected SignatureMissing(%d), got %d", xerr.SignatureMissing, code)
}
}
func TestSignatureMiddlewarePassesWhenSignatureHeaderMissing(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
t.Fatalf("expected pass-through without X-Signature-Enabled, got code=%d body=%s", resp.Code, resp.Body.String())
}
}
func TestSignatureMiddlewarePassesWhenSignatureHeaderIsZero(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
req.Header.Set("X-Signature-Enabled", "0")
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
t.Fatalf("expected pass-through when X-Signature-Enabled=0, got code=%d body=%s", resp.Code, resp.Body.String())
}
}
func TestSignatureMiddlewarePassesWhenSystemSwitchDisabled(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContextWithSwitch(false)
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
req.Header.Set("X-Signature-Enabled", "1")
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
t.Fatalf("expected pass-through when system switch is disabled, got code=%d body=%s", resp.Code, resp.Body.String())
}
}
func TestSignatureMiddlewareSkipsNonPublicPath(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/admin/ping", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/admin/ping", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
t.Fatalf("expected pass-through for non-public path, got code=%d body=%s", resp.Code, resp.Body.String())
}
}
func TestSignatureMiddlewareHonorsSkipPrefix(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.GET("/v1/public/healthz", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/v1/public/healthz", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
t.Fatalf("expected skip-prefix pass-through, got code=%d body=%s", resp.Code, resp.Body.String())
}
}
func TestSignatureMiddlewareRestoresBodyAfterVerify(t *testing.T) {
gin.SetMode(gin.TestMode)
svcCtx := newTestServiceContext()
r := gin.New()
r.Use(SignatureMiddleware(svcCtx))
r.POST("/v1/public/body", func(c *gin.Context) {
body, _ := io.ReadAll(c.Request.Body)
c.String(http.StatusOK, string(body))
})
body := `{"hello":"world"}`
req := httptest.NewRequest(http.MethodPost, "/v1/public/body?a=1&b=2", strings.NewReader(body))
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "nonce-body-1"
sts := signature.BuildStringToSign(http.MethodPost, "/v1/public/body", "a=1&b=2", []byte(body), "web-client", ts, nonce)
req.Header.Set("X-Signature-Enabled", "1")
req.Header.Set("X-App-Id", "web-client")
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", makeTestSignature("test-secret", sts))
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK || resp.Body.String() != body {
t.Fatalf("expected restored body, got code=%d body=%s", resp.Code, resp.Body.String())
}
}

View File

@ -202,27 +202,36 @@ func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
// CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量 // CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量
func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) { func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) {
tags = normalizeNodeTags(tags)
if len(nodeIds) == 0 && len(tags) == 0 {
return 0, nil
}
var count int64 var count int64
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true) query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
if len(nodeIds) > 0 {
if len(nodeIds) > 0 || len(tags) > 0 { query = query.Where("id IN ?", nodeIds)
subQuery := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true) }
if len(tags) > 0 {
if len(nodeIds) > 0 && len(tags) > 0 { query = query.Scopes(InSet("tags", tags))
subQuery = subQuery.Where("id IN ? OR ?", nodeIds, InSet("tags", tags))
} else if len(nodeIds) > 0 {
subQuery = subQuery.Where("id IN ?", nodeIds)
} else {
subQuery = subQuery.Scopes(InSet("tags", tags))
}
query = subQuery
} }
err := query.Count(&count).Error err := query.Count(&count).Error
return count, err return count, err
} }
func normalizeNodeTags(tags []string) []string {
cleaned := make([]string, 0, len(tags))
for _, tag := range tags {
trimmed := strings.TrimSpace(tag)
if trimmed == "" {
continue
}
cleaned = append(cleaned, trimmed)
}
return tool.RemoveDuplicateElements(cleaned...)
}
// InSet 支持多值 OR 查询 // InSet 支持多值 OR 查询
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {

View File

@ -0,0 +1,12 @@
package node
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNormalizeNodeTags(t *testing.T) {
tags := normalizeNodeTags([]string{"美国", " 日本 ", "", "美国", " ", "日本"})
require.Equal(t, []string{"美国", "日本"}, tags)
}

View File

@ -15,7 +15,8 @@ import (
var _ Model = (*customSubscribeModel)(nil) var _ Model = (*customSubscribeModel)(nil)
var ( var (
cacheSubscribeIdPrefix = "cache:subscribe:id:" cacheSubscribeIdPrefix = "cache:subscribe:id:"
cacheUserSubscribeUserPrefix = "cache:user:subscribe:user:"
) )
type ( type (
@ -119,13 +120,31 @@ func (m *defaultSubscribeModel) Update(ctx context.Context, data *Subscribe, tx
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
var userIds []int64
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
return conn.Table("user_subscribe").
Where("subscribe_id = ? AND status IN (0, 1)", data.Id).
Distinct("user_id").
Pluck("user_id", &userIds).Error
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
userSubscribeCacheKeys := make([]string, 0, len(userIds))
for _, userId := range userIds {
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
}
allCacheKeys := append(m.getCacheKeys(old), userSubscribeCacheKeys...)
err = m.ExecCtx(ctx, func(conn *gorm.DB) error { err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn db := conn
if len(tx) > 0 { if len(tx) > 0 {
db = tx[0] db = tx[0]
} }
return db.Save(data).Error return db.Save(data).Error
}, m.getCacheKeys(old)...) }, allCacheKeys...)
return err return err
} }
@ -137,13 +156,31 @@ func (m *defaultSubscribeModel) Delete(ctx context.Context, id int64, tx ...*gor
} }
return err return err
} }
var userIds []int64
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
return conn.Table("user_subscribe").
Where("subscribe_id = ? AND status IN (0, 1)", id).
Distinct("user_id").
Pluck("user_id", &userIds).Error
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
userSubscribeCacheKeys := make([]string, 0, len(userIds))
for _, userId := range userIds {
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
}
allCacheKeys := append(m.getCacheKeys(data), userSubscribeCacheKeys...)
err = m.ExecCtx(ctx, func(conn *gorm.DB) error { err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn db := conn
if len(tx) > 0 { if len(tx) > 0 {
db = tx[0] db = tx[0]
} }
return db.Delete(&Subscribe{}, id).Error return db.Delete(&Subscribe{}, id).Error
}, m.getCacheKeys(data)...) }, allCacheKeys...)
return err return err
} }

View File

@ -19,6 +19,7 @@ type customSystemLogicModel interface {
GetTosConfig(ctx context.Context) ([]*System, error) GetTosConfig(ctx context.Context) ([]*System, error)
GetCurrencyConfig(ctx context.Context) ([]*System, error) GetCurrencyConfig(ctx context.Context) ([]*System, error)
GetVerifyCodeConfig(ctx context.Context) ([]*System, error) GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
GetSignatureConfig(ctx context.Context) ([]*System, error)
GetLogConfig(ctx context.Context) ([]*System, error) GetLogConfig(ctx context.Context) ([]*System, error)
UpdateNodeMultiplierConfig(ctx context.Context, config string) error UpdateNodeMultiplierConfig(ctx context.Context, config string) error
FindNodeMultiplierConfig(ctx context.Context) (*System, error) FindNodeMultiplierConfig(ctx context.Context) (*System, error)
@ -154,6 +155,15 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System,
return configs, err return configs, err
} }
// GetSignatureConfig returns the signature config.
func (m *customSystemModel) GetSignatureConfig(ctx context.Context) ([]*System, error) {
var configs []*System
err := m.QueryCtx(ctx, &configs, config.SignatureConfigKey, func(conn *gorm.DB, v interface{}) error {
return conn.Where("`category` = ?", "signature").Find(v).Error
})
return configs, err
}
// GetLogConfig returns the log config. // GetLogConfig returns the log config.
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) { func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
var configs []*System var configs []*System

View File

@ -50,7 +50,14 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
} }
r.Use(sessions.Sessions("ppanel", sessionStore)) r.Use(sessions.Sessions("ppanel", sessionStore))
// use cors middleware // use cors middleware
r.Use(middleware.TraceMiddleware(svc), middleware.ApiVersionMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery()) r.Use(
middleware.TraceMiddleware(svc),
middleware.ApiVersionMiddleware(svc),
middleware.LoggerMiddleware(svc),
middleware.SignatureMiddleware(svc),
middleware.CorsMiddleware,
gin.Recovery(),
)
// register handlers // register handlers
handler.RegisterHandlers(r, svc) handler.RegisterHandlers(r, svc)

View File

@ -28,6 +28,7 @@ import (
"github.com/perfect-panel/server/pkg/limit" "github.com/perfect-panel/server/pkg/limit"
"github.com/perfect-panel/server/pkg/nodeMultiplier" "github.com/perfect-panel/server/pkg/nodeMultiplier"
"github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/orm"
"github.com/perfect-panel/server/pkg/signature"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
@ -36,33 +37,34 @@ import (
) )
type ServiceContext struct { type ServiceContext struct {
DB *gorm.DB DB *gorm.DB
Redis *redis.Client Redis *redis.Client
Config config.Config Config config.Config
Queue *asynq.Client Queue *asynq.Client
ExchangeRate float64 ExchangeRate float64
GeoIP *IPLocation GeoIP *IPLocation
SignatureValidator *signature.Validator
//NodeCache *cache.NodeCacheClient //NodeCache *cache.NodeCacheClient
AuthModel auth.Model AuthModel auth.Model
AdsModel ads.Model AdsModel ads.Model
LogModel log.Model LogModel log.Model
LogMessageModel logmessage.Model LogMessageModel logmessage.Model
NodeModel node.Model NodeModel node.Model
UserModel user.Model UserModel user.Model
OrderModel order.Model OrderModel order.Model
ClientModel client.Model ClientModel client.Model
TicketModel ticket.Model TicketModel ticket.Model
//ServerModel server.Model //ServerModel server.Model
SystemModel system.Model SystemModel system.Model
CouponModel coupon.Model CouponModel coupon.Model
RedemptionCodeModel redemption.RedemptionCodeModel RedemptionCodeModel redemption.RedemptionCodeModel
RedemptionRecordModel redemption.RedemptionRecordModel RedemptionRecordModel redemption.RedemptionRecordModel
PaymentModel payment.Model PaymentModel payment.Model
DocumentModel document.Model DocumentModel document.Model
SubscribeModel subscribe.Model SubscribeModel subscribe.Model
TrafficLogModel traffic.Model TrafficLogModel traffic.Model
AnnouncementModel announcement.Model AnnouncementModel announcement.Model
IAPAppleTransactionModel iapapple.Model IAPAppleTransactionModel iapapple.Model
Restart func() error Restart func() error
@ -109,24 +111,27 @@ func NewServiceContext(c config.Config) *ServiceContext {
_ = rds.FlushAll(context.Background()).Err() _ = rds.FlushAll(context.Background()).Err()
} }
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
nonceStore := signature.NewRedisNonceStore(rds)
srv := &ServiceContext{ srv := &ServiceContext{
DB: db, DB: db,
Redis: rds, Redis: rds,
Config: c, Config: c,
Queue: NewAsynqClient(c), Queue: NewAsynqClient(c),
ExchangeRate: 0, ExchangeRate: 0,
GeoIP: geoIP, GeoIP: geoIP,
SignatureValidator: signature.NewValidator(c.AppSignature, nonceStore),
//NodeCache: cache.NewNodeCacheClient(rds), //NodeCache: cache.NewNodeCacheClient(rds),
AuthLimiter: authLimiter, AuthLimiter: authLimiter,
AdsModel: ads.NewModel(db, rds), AdsModel: ads.NewModel(db, rds),
LogModel: log.NewModel(db), LogModel: log.NewModel(db),
LogMessageModel: logmessage.NewModel(db), LogMessageModel: logmessage.NewModel(db),
NodeModel: node.NewModel(db, rds), NodeModel: node.NewModel(db, rds),
AuthModel: auth.NewModel(db, rds), AuthModel: auth.NewModel(db, rds),
UserModel: user.NewModel(db, rds), UserModel: user.NewModel(db, rds),
OrderModel: order.NewModel(db, rds), OrderModel: order.NewModel(db, rds),
ClientModel: client.NewSubscribeApplicationModel(db), ClientModel: client.NewSubscribeApplicationModel(db),
TicketModel: ticket.NewModel(db, rds), TicketModel: ticket.NewModel(db, rds),
//ServerModel: server.NewModel(db, rds), //ServerModel: server.NewModel(db, rds),
SystemModel: system.NewModel(db, rds), SystemModel: system.NewModel(db, rds),
CouponModel: coupon.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds),

View File

@ -0,0 +1,61 @@
package types
type ContactRequest struct {
Name string `json:"name" validate:"required,max=100"`
Email string `json:"email" validate:"required,email"`
OtherContact string `json:"other_contact" validate:"max=200"`
Notes string `json:"notes" validate:"max=2000"`
}
type GetDownloadLinkRequest struct {
InviteCode string `form:"invite_code,optional"`
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
}
type GetDownloadLinkResponse struct {
Url string `json:"url"`
}
type ReportLogMessageRequest struct {
Platform string `json:"platform" validate:"required,max=32"`
AppVersion string `json:"app_version" validate:"required,max=64"`
OsName string `json:"os_name" validate:"max=64"`
OsVersion string `json:"os_version" validate:"max=64"`
DeviceId string `json:"device_id" validate:"required,max=255"`
UserId int64 `json:"user_id"`
SessionId string `json:"session_id" validate:"max=255"`
Level uint8 `json:"level"`
ErrorCode string `json:"error_code" validate:"max=128"`
Message string `json:"message" validate:"required,max=65535"`
Stack string `json:"stack" validate:"max=1048576"`
Context interface{} `json:"context"`
OccurredAt int64 `json:"occurred_at"`
}
type ReportLogMessageResponse struct {
Id int64 `json:"id"`
}
type LegacyCheckVerificationCodeRequest struct {
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
Account string `json:"account" form:"account"`
Email string `json:"email" form:"email"`
Code string `json:"code" form:"code" validate:"required"`
Type uint8 `json:"type" form:"type" validate:"required"`
}
type LegacyCheckVerificationCodeResponse struct {
Status bool `json:"status"`
Exist bool `json:"exist"`
}
type EmailLoginRequest struct {
Identifier string `json:"identifier" form:"identifier"`
Email string `json:"email" form:"email" validate:"required,email"`
Code string `json:"code" form:"code" validate:"required"`
Invite string `json:"invite" form:"invite"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional" form:"cf_token"`
}

View File

@ -221,19 +221,6 @@ type CheckVerificationCodeRespone struct {
Exist bool `json:"exist,omitempty"` Exist bool `json:"exist,omitempty"`
} }
type LegacyCheckVerificationCodeRequest struct {
Method string `json:"method" form:"method"`
Account string `json:"account" form:"account"`
Email string `json:"email" form:"email"`
Code string `json:"code" form:"code" validate:"required"`
Type uint8 `json:"type" form:"type" validate:"required"`
}
type LegacyCheckVerificationCodeResponse struct {
Status bool `json:"status"`
Exist bool `json:"exist"`
}
type CheckoutOrderRequest struct { type CheckoutOrderRequest struct {
OrderNo string `json:"orderNo"` OrderNo string `json:"orderNo"`
ReturnUrl string `json:"returnUrl,omitempty"` ReturnUrl string `json:"returnUrl,omitempty"`
@ -576,6 +563,11 @@ type DeviceLoginRequest struct {
ShortCode string `json:"short_code,optional"` ShortCode string `json:"short_code,optional"`
} }
type DissolveFamilyRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
Reason string `json:"reason,omitempty"`
}
type Document struct { type Document struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
@ -615,6 +607,34 @@ type EmailAuthticateConfig struct {
DomainSuffixList string `json:"domain_suffix_list"` DomainSuffixList string `json:"domain_suffix_list"`
} }
type FamilyDetail struct {
Summary FamilySummary `json:"summary"`
Members []FamilyMemberItem `json:"members"`
}
type FamilyMemberItem struct {
UserId int64 `json:"user_id"`
Identifier string `json:"identifier"`
Role uint8 `json:"role"`
RoleName string `json:"role_name"`
Status uint8 `json:"status"`
StatusName string `json:"status_name"`
JoinSource string `json:"join_source"`
JoinedAt int64 `json:"joined_at"`
LeftAt int64 `json:"left_at,omitempty"`
}
type FamilySummary struct {
FamilyId int64 `json:"family_id"`
OwnerUserId int64 `json:"owner_user_id"`
OwnerIdentifier string `json:"owner_identifier"`
Status string `json:"status"`
ActiveMemberCount int64 `json:"active_member_count"`
MaxMembers int64 `json:"max_members"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type FilterBalanceLogRequest struct { type FilterBalanceLogRequest struct {
FilterLogParams FilterLogParams
UserId int64 `form:"user_id,optional"` UserId int64 `form:"user_id,optional"`
@ -887,6 +907,25 @@ type GetDocumentListResponse struct {
List []Document `json:"list"` List []Document `json:"list"`
} }
type GetFamilyDetailRequest struct {
Id int64 `form:"id" validate:"required"`
}
type GetFamilyListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Keyword string `form:"keyword,omitempty"`
Status string `form:"status,omitempty"`
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
FamilyId *int64 `form:"family_id,omitempty"`
UserId *int64 `form:"user_id,omitempty"`
}
type GetFamilyListResponse struct {
List []FamilySummary `json:"list"`
Total int64 `json:"total"`
}
type GetGlobalConfigResponse struct { type GetGlobalConfigResponse struct {
Site SiteConfig `json:"site"` Site SiteConfig `json:"site"`
Verify VeifyConfig `json:"verify"` Verify VeifyConfig `json:"verify"`
@ -894,6 +933,7 @@ type GetGlobalConfigResponse struct {
Invite InviteConfig `json:"invite"` Invite InviteConfig `json:"invite"`
Currency Currency `json:"currency"` Currency Currency `json:"currency"`
Subscribe SubscribeConfig `json:"subscribe"` Subscribe SubscribeConfig `json:"subscribe"`
Signature SignatureConfig `json:"signature"`
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"` VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
OAuthMethods []string `json:"oauth_methods"` OAuthMethods []string `json:"oauth_methods"`
WebAd bool `json:"web_ad"` WebAd bool `json:"web_ad"`
@ -1188,41 +1228,6 @@ type GetUserSubscribeResetTrafficLogsResponse struct {
Total int64 `json:"total"` Total int64 `json:"total"`
} }
type GetFamilyListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Keyword string `form:"keyword,omitempty"`
Status string `form:"status,omitempty"`
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
FamilyId *int64 `form:"family_id,omitempty"`
UserId *int64 `form:"user_id,omitempty"`
}
type GetFamilyListResponse struct {
List []FamilySummary `json:"list"`
Total int64 `json:"total"`
}
type GetFamilyDetailRequest struct {
Id int64 `form:"id" validate:"required"`
}
type UpdateFamilyMaxMembersRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
}
type RemoveFamilyMemberRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
UserId int64 `json:"user_id" validate:"required,gt=0"`
Reason string `json:"reason,omitempty"`
}
type DissolveFamilyRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
Reason string `json:"reason,omitempty"`
}
type GetUserSubscribeTrafficLogsRequest struct { type GetUserSubscribeTrafficLogsRequest struct {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
@ -1706,10 +1711,8 @@ type QueryOrderDetailRequest struct {
} }
type QueryOrderListRequest struct { type QueryOrderListRequest struct {
Page int `form:"page" validate:"required"` Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"` Size int `form:"size" validate:"required"`
Status int `form:"status"`
Search string `form:"search"`
} }
type QueryOrderListResponse struct { type QueryOrderListResponse struct {
@ -1932,6 +1935,12 @@ type RegisterLog struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
} }
type RemoveFamilyMemberRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
UserId int64 `json:"user_id" validate:"required,gt=0"`
Reason string `json:"reason,omitempty"`
}
type RenewalOrderRequest struct { type RenewalOrderRequest struct {
UserSubscribeID int64 `json:"user_subscribe_id"` UserSubscribeID int64 `json:"user_subscribe_id"`
Quantity int64 `json:"quantity" validate:"lte=1000"` Quantity int64 `json:"quantity" validate:"lte=1000"`
@ -2163,6 +2172,10 @@ type ShadowsocksProtocol struct {
Method string `json:"method"` Method string `json:"method"`
} }
type SignatureConfig struct {
EnableSignature bool `json:"enable_signature"`
}
type SiteConfig struct { type SiteConfig struct {
Host string `json:"host"` Host string `json:"host"`
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
@ -2523,6 +2536,11 @@ type UpdateDocumentRequest struct {
Show *bool `json:"show"` Show *bool `json:"show"`
} }
type UpdateFamilyMaxMembersRequest struct {
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
}
type UpdateNodeRequest struct { type UpdateNodeRequest struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -2699,7 +2717,6 @@ type User struct {
GiftAmount int64 `json:"gift_amount"` GiftAmount int64 `json:"gift_amount"`
Telegram int64 `json:"telegram"` Telegram int64 `json:"telegram"`
ReferCode string `json:"refer_code"` ReferCode string `json:"refer_code"`
ShareLink string `json:"share_link,omitempty"`
RefererId int64 `json:"referer_id"` RefererId int64 `json:"referer_id"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
IsAdmin bool `json:"is_admin,omitempty"` IsAdmin bool `json:"is_admin,omitempty"`
@ -2716,8 +2733,6 @@ type User struct {
IsDel bool `json:"is_del,omitempty"` IsDel bool `json:"is_del,omitempty"`
Remark string `json:"remark,omitempty"` Remark string `json:"remark,omitempty"`
PurchasedPackage string `json:"purchased_package,omitempty"` PurchasedPackage string `json:"purchased_package,omitempty"`
LastLoginTime int64 `json:"last_login_time"`
MemberStatus string `json:"member_status"`
FamilyJoined bool `json:"family_joined,omitempty"` FamilyJoined bool `json:"family_joined,omitempty"`
FamilyId int64 `json:"family_id,omitempty"` FamilyId int64 `json:"family_id,omitempty"`
FamilyRole uint8 `json:"family_role,omitempty"` FamilyRole uint8 `json:"family_role,omitempty"`
@ -2761,34 +2776,6 @@ type UserLoginLog struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
} }
type FamilySummary struct {
FamilyId int64 `json:"family_id"`
OwnerUserId int64 `json:"owner_user_id"`
OwnerIdentifier string `json:"owner_identifier"`
Status string `json:"status"`
ActiveMemberCount int64 `json:"active_member_count"`
MaxMembers int64 `json:"max_members"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
type FamilyMemberItem struct {
UserId int64 `json:"user_id"`
Identifier string `json:"identifier"`
Role uint8 `json:"role"`
RoleName string `json:"role_name"`
Status uint8 `json:"status"`
StatusName string `json:"status_name"`
JoinSource string `json:"join_source"`
JoinedAt int64 `json:"joined_at"`
LeftAt int64 `json:"left_at,omitempty"`
}
type FamilyDetail struct {
Summary FamilySummary `json:"summary"`
Members []FamilyMemberItem `json:"members"`
}
type UserLoginRequest struct { type UserLoginRequest struct {
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
@ -2844,7 +2831,6 @@ type UserSubscribe struct {
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"` EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
ReadOnly bool `json:"read_only"` ReadOnly bool `json:"read_only"`
Short string `json:"short"` Short string `json:"short"`
IsGift bool `json:"is_gift"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
@ -3023,221 +3009,3 @@ type WithdrawalLog struct {
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
type EmailLoginRequest struct {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
Invite string `json:"invite,optional"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"`
}
type BindEmailWithVerificationRequest struct {
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
}
type BindEmailWithVerificationResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Token string `json:"token,omitempty"`
UserId int64 `json:"user_id,omitempty"`
FamilyJoined bool `json:"family_joined,omitempty"`
FamilyId int64 `json:"family_id,omitempty"`
OwnerUserId int64 `json:"owner_user_id,omitempty"`
}
type BindInviteCodeRequest struct {
InviteCode string `json:"invite_code" validate:"required"`
}
type GetSubscribeStatusResponse struct {
DeviceStatus bool `json:"device_status"`
EmailStatus bool `json:"email_status"`
}
type GetSubscribeStatusRequest struct {
Email string `json:"email" validate:"required,email"`
}
type DeleteAccountResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
UserId int64 `json:"user_id"`
Code int `json:"code"`
}
type GetDownloadLinkRequest struct {
InviteCode string `form:"invite_code,optional"`
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
}
type GetDownloadLinkResponse struct {
Url string `json:"url"`
}
type ContactRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
OtherContact string `json:"other_contact,omitempty"`
Notes string `json:"notes,omitempty"`
}
type ReportLogMessageRequest struct {
Platform string `json:"platform" validate:"required"`
AppVersion string `json:"appVersion"`
OsName string `json:"osName"`
OsVersion string `json:"osVersion"`
DeviceId string `json:"deviceId"`
UserId int64 `json:"userId"`
SessionId string `json:"sessionId"`
Level uint8 `json:"level"`
ErrorCode string `json:"errorCode"`
Message string `json:"message" validate:"required"`
Stack string `json:"stack"`
Context map[string]interface{} `json:"context"`
OccurredAt int64 `json:"occurredAt"`
}
type ReportLogMessageResponse struct {
Id int64 `json:"id"`
}
type GetErrorLogMessageListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
Platform string `form:"platform"`
Level uint8 `form:"level"`
UserId int64 `form:"user_id"`
DeviceId string `form:"device_id"`
ErrorCode string `form:"error_code"`
Keyword string `form:"keyword"`
Start int64 `form:"start"`
End int64 `form:"end"`
}
type ErrorLogMessage struct {
Id int64 `json:"id"`
Platform string `json:"platform"`
AppVersion string `json:"app_version"`
OsName string `json:"os_name"`
OsVersion string `json:"os_version"`
DeviceId string `json:"device_id"`
UserId int64 `json:"user_id"`
SessionId string `json:"session_id"`
Level uint8 `json:"level"`
ErrorCode string `json:"error_code"`
Message string `json:"message"`
CreatedAt int64 `json:"created_at"`
}
type GetErrorLogMessageListResponse struct {
Total int64 `json:"total"`
List []ErrorLogMessage `json:"list"`
}
type GetErrorLogMessageDetailResponse struct {
Id int64 `json:"id"`
Platform string `json:"platform"`
AppVersion string `json:"app_version"`
OsName string `json:"os_name"`
OsVersion string `json:"os_version"`
DeviceId string `json:"device_id"`
UserId int64 `json:"user_id"`
SessionId string `json:"session_id"`
Level uint8 `json:"level"`
ErrorCode string `json:"error_code"`
Message string `json:"message"`
Stack string `json:"stack"`
Context map[string]interface{} `json:"context"`
ClientIP string `json:"client_ip"`
UserAgent string `json:"user_agent"`
Locale string `json:"locale"`
OccurredAt int64 `json:"occurred_at"`
CreatedAt int64 `json:"created_at"`
}
type AttachAppleTransactionRequest struct {
SignedTransactionJWS string `json:"signed_transaction_jws" validate:"required"`
DurationDays int64 `json:"duration_days,omitempty"`
Tier string `json:"tier,omitempty"`
SubscribeId int64 `json:"subscribe_id,omitempty"`
OrderNo string `json:"order_no" validate:"required"`
}
type AttachAppleTransactionResponse struct {
ExpiresAt int64 `json:"expires_at"`
Tier string `json:"tier"`
}
type AttachAppleTransactionByIdRequest struct {
TransactionId string `json:"transaction_id" validate:"required"`
OrderNo string `json:"order_no" validate:"required"`
Sandbox *bool `json:"sandbox,omitempty"`
}
type RestoreAppleTransactionsRequest struct {
Transactions []string `json:"transactions" validate:"required"`
}
type GetAppleStatusResponse struct {
Active bool `json:"active"`
ExpiresAt int64 `json:"expires_at"`
Tier string `json:"tier"`
}
type GetAgentRealtimeRequest struct{}
type GetAgentRealtimeResponse struct {
Total int64 `json:"total"`
Clicks int64 `json:"clicks"`
Views int64 `json:"views"`
Installs int64 `json:"installs"`
PaidCount int64 `json:"paid_count"`
GrowthRate string `json:"growth_rate"`
PaidGrowthRate string `json:"paid_growth_rate"`
}
type GetAgentDownloadsRequest struct{}
type GetAgentDownloadsResponse struct {
Total int64 `json:"total"`
Platforms *PlatformDownloads `json:"platforms"`
ComparisonRate *string `json:"comparison_rate,omitempty"`
}
type PlatformDownloads struct {
IOS int64 `json:"ios"`
Android int64 `json:"android"`
Windows int64 `json:"windows"`
Mac int64 `json:"mac"`
}
type GetUserInviteStatsRequest struct{}
type GetUserInviteStatsResponse struct {
FriendlyCount int64 `json:"friendly_count"`
HistoryCount int64 `json:"history_count"`
}
type GetInviteSalesRequest struct {
Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"`
StartTime int64 `form:"start_time,optional"`
EndTime int64 `form:"end_time,optional"`
}
type GetInviteSalesResponse struct {
Total int64 `json:"total"`
List []InvitedUserSale `json:"list"`
}
type InvitedUserSale struct {
Amount float64 `json:"amount"`
UpdatedAt int64 `json:"update_at"`
UserHash string `json:"user_hash"`
ProductName string `json:"product_name"`
}

View File

@ -0,0 +1,50 @@
package signature
import (
"crypto/sha256"
"fmt"
"net/url"
"sort"
"strings"
)
// BuildStringToSign StringToSign = METHOD\nPATH\nCANONICAL_QUERY\nBODY_SHA256\nX-App-Id\nX-Timestamp\nX-Nonce
func BuildStringToSign(method, path, rawQuery string, body []byte, appId, timestamp, nonce string) string {
canonical := canonicalQuery(rawQuery)
bodyHash := sha256Hex(body)
parts := []string{
strings.ToUpper(method),
path,
canonical,
bodyHash,
appId,
timestamp,
nonce,
}
return strings.Join(parts, "\n")
}
func canonicalQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
values, err := url.ParseQuery(rawQuery)
if err != nil {
return rawQuery
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", key, values.Get(key)))
}
return strings.Join(parts, "&")
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}

7
pkg/signature/config.go Normal file
View File

@ -0,0 +1,7 @@
package signature
type SignatureConf struct {
AppSecrets map[string]string `yaml:"AppSecrets"`
ValidWindowSeconds int64 `yaml:"ValidWindowSeconds"`
SkipPrefixes []string `yaml:"SkipPrefixes"`
}

10
pkg/signature/errors.go Normal file
View File

@ -0,0 +1,10 @@
package signature
import "errors"
var (
ErrSignatureMissing = errors.New("signature missing")
ErrSignatureExpired = errors.New("signature expired")
ErrSignatureInvalid = errors.New("signature invalid")
ErrSignatureReplay = errors.New("signature replay")
)

View File

@ -0,0 +1,30 @@
package signature
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type NonceStore interface {
SetIfNotExists(ctx context.Context, appId, nonce string, ttlSeconds int64) (bool, error)
}
type RedisNonceStore struct {
client *redis.Client
}
func NewRedisNonceStore(client *redis.Client) *RedisNonceStore {
return &RedisNonceStore{client: client}
}
func (s *RedisNonceStore) SetIfNotExists(ctx context.Context, appId, nonce string, ttlSeconds int64) (bool, error) {
key := fmt.Sprintf("sig:nonce:%s:%s", appId, nonce)
ok, err := s.client.SetNX(ctx, key, "1", time.Duration(ttlSeconds)*time.Second).Result()
if err != nil {
return false, err
}
return !ok, nil
}

View File

@ -0,0 +1,120 @@
package signature
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"testing"
"time"
)
type mockNonceStore struct {
seen map[string]bool
}
func newMockNonceStore() *mockNonceStore {
return &mockNonceStore{seen: map[string]bool{}}
}
func (m *mockNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, _ int64) (bool, error) {
key := appId + ":" + nonce
if m.seen[key] {
return true, nil
}
m.seen[key] = true
return false, nil
}
func makeSignature(secret, stringToSign string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
func TestValidateSuccess(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockNonceStore())
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
sts := BuildStringToSign("POST", "/v1/public/order/create", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != nil {
t.Fatalf("expected success, got %v", err)
}
}
func TestValidateExpired(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockNonceStore())
ts := strconv.FormatInt(time.Now().Unix()-400, 10)
nonce := "abc"
sts := BuildStringToSign("GET", "/v1/public/user/info", "", nil, "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureExpired {
t.Fatalf("expected ErrSignatureExpired, got %v", err)
}
}
func TestValidateReplay(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockNonceStore())
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "same-nonce-replay"
sts := BuildStringToSign("GET", "/v1/public/user/info", "", nil, "web-client", ts, nonce)
sig := makeSignature("uB4G,XxL2{7b", sts)
_ = v.Validate(context.Background(), "web-client", ts, nonce, sig, sts)
if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureReplay {
t.Fatalf("expected ErrSignatureReplay, got %v", err)
}
}
func TestValidateInvalidSignature(t *testing.T) {
conf := SignatureConf{
AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"},
ValidWindowSeconds: 300,
}
v := NewValidator(conf, newMockNonceStore())
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "nonce-invalid-sig"
sts := BuildStringToSign("POST", "/v1/public/order/create", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce)
if err := v.Validate(context.Background(), "web-client", ts, nonce, "badsignature", sts); err != ErrSignatureInvalid {
t.Fatalf("expected ErrSignatureInvalid, got %v", err)
}
}
func TestBuildStringToSignCanonicalQuery(t *testing.T) {
got := BuildStringToSign(
"get",
"/v1/public/order/list",
"b=2&a=1&a=3&c=",
nil,
"web-client",
"1700000000",
"nonce-1",
)
want := "GET\n/v1/public/order/list\na=1&b=2&c=\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nweb-client\n1700000000\nnonce-1"
if got != want {
t.Fatalf("unexpected stringToSign\nwant: %s\ngot: %s", want, got)
}
}

View File

@ -0,0 +1,61 @@
package signature
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
type Validator struct {
conf SignatureConf
nonceStore NonceStore
}
func NewValidator(conf SignatureConf, store NonceStore) *Validator {
return &Validator{
conf: conf,
nonceStore: store,
}
}
func (v *Validator) windowSeconds() int64 {
if v.conf.ValidWindowSeconds <= 0 {
return 300
}
return v.conf.ValidWindowSeconds
}
func (v *Validator) Validate(ctx context.Context, appId, timestamp, nonce, signature, stringToSign string) error {
secret, ok := v.conf.AppSecrets[appId]
if !ok {
return ErrSignatureMissing
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return ErrSignatureExpired
}
diff := time.Now().Unix() - ts
if diff < 0 {
diff = -diff
}
if diff > v.windowSeconds() {
return ErrSignatureExpired
}
replayed, err := v.nonceStore.SetIfNotExists(ctx, appId, nonce, v.windowSeconds()+60)
if err == nil && replayed {
return ErrSignatureReplay
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
return ErrSignatureInvalid
}
return nil
}

View File

@ -58,6 +58,10 @@ const (
InvalidAccess uint32 = 40005 InvalidAccess uint32 = 40005
InvalidCiphertext uint32 = 40006 InvalidCiphertext uint32 = 40006
SecretIsEmpty uint32 = 40007 SecretIsEmpty uint32 = 40007
SignatureMissing uint32 = 40008
SignatureExpired uint32 = 40009
SignatureInvalid uint32 = 40010
SignatureReplay uint32 = 40011
) )
//coupon error //coupon error

View File

@ -17,6 +17,10 @@ func init() {
SecretIsEmpty: "Secret is empty", SecretIsEmpty: "Secret is empty",
InvalidAccess: "Invalid access", InvalidAccess: "Invalid access",
InvalidCiphertext: "Invalid ciphertext", InvalidCiphertext: "Invalid ciphertext",
SignatureMissing: "Signature headers are missing",
SignatureExpired: "Signature is expired",
SignatureInvalid: "Signature is invalid",
SignatureReplay: "Signature nonce replay detected",
// Database error // Database error
DatabaseQueryError: "Database query error", DatabaseQueryError: "Database query error",
DatabaseUpdateError: "Database update error", DatabaseUpdateError: "Database update error",