无订阅 支付后出现两个订阅
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m37s
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m37s
This commit is contained in:
parent
a1ab0fefa4
commit
7308aa9191
7
.claude-flow/.gitignore
vendored
Normal file
7
.claude-flow/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Claude Flow runtime files
|
||||
data/
|
||||
logs/
|
||||
sessions/
|
||||
neural/
|
||||
*.log
|
||||
*.tmp
|
||||
403
.claude-flow/CAPABILITIES.md
Normal file
403
.claude-flow/CAPABILITIES.md
Normal 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
43
.claude-flow/config.yaml
Normal 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
|
||||
17
.claude-flow/metrics/learning.json
Normal file
17
.claude-flow/metrics/learning.json
Normal 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"
|
||||
}
|
||||
18
.claude-flow/metrics/swarm-activity.json
Normal file
18
.claude-flow/metrics/swarm-activity.json
Normal 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
|
||||
}
|
||||
26
.claude-flow/metrics/v3-progress.json
Normal file
26
.claude-flow/metrics/v3-progress.json
Normal 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"
|
||||
}
|
||||
8
.claude-flow/security/audit-status.json
Normal file
8
.claude-flow/security/audit-status.json
Normal 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
22
.mcp.json
Normal 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
188
CLAUDE.md
Normal 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 (var→const, 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
|
||||
@ -27,8 +27,8 @@ type (
|
||||
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
|
||||
}
|
||||
ToggleRedemptionCodeStatusRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Status int64 `json:"status" validate:"oneof=0 1"`
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Status int64 `json:"status" validate:"oneof=0 1"`
|
||||
}
|
||||
DeleteRedemptionCodeRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
@ -93,3 +93,4 @@ service ppanel {
|
||||
@handler GetRedemptionRecordList
|
||||
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -127,6 +127,14 @@ service ppanel {
|
||||
@handler UpdateVerifyCodeConfig
|
||||
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"
|
||||
@handler PreViewNodeMultiplier
|
||||
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
||||
|
||||
@ -15,18 +15,18 @@ import (
|
||||
type (
|
||||
// GetUserListRequest
|
||||
GetUserListRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Search string `form:"search,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
Unscoped bool `form:"unscoped,omitempty"`
|
||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||
ShortCode string `form:"short_code,omitempty"`
|
||||
FamilyJoined *bool `form:"family_joined,omitempty"`
|
||||
FamilyStatus string `form:"family_status,omitempty"`
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Search string `form:"search,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
Unscoped bool `form:"unscoped,omitempty"`
|
||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||
ShortCode string `form:"short_code,omitempty"`
|
||||
FamilyJoined *bool `form:"family_joined,omitempty"`
|
||||
FamilyStatus string `form:"family_status,omitempty"`
|
||||
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
}
|
||||
// GetUserListResponse
|
||||
GetUserListResponse {
|
||||
@ -366,3 +366,4 @@ service ppanel {
|
||||
@handler DissolveFamily
|
||||
put /family/dissolve (DissolveFamilyRequest)
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ type (
|
||||
Invite InviteConfig `json:"invite"`
|
||||
Currency Currency `json:"currency"`
|
||||
Subscribe SubscribeConfig `json:"subscribe"`
|
||||
Signature SignatureConfig `json:"signature"`
|
||||
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
||||
OAuthMethods []string `json:"oauth_methods"`
|
||||
WebAd bool `json:"web_ad"`
|
||||
|
||||
@ -30,3 +30,4 @@ service ppanel {
|
||||
@handler RedeemCode
|
||||
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,9 @@ type (
|
||||
QuerySubscribeListRequest {
|
||||
Language string `form:"language"`
|
||||
}
|
||||
|
||||
QueryUserSubscribeNodeListResponse {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
|
||||
UserSubscribeInfo {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
@ -41,7 +39,6 @@ type (
|
||||
IsTryOut bool `json:"is_try_out"`
|
||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||
}
|
||||
|
||||
UserSubscribeNodeInfo {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@ -75,3 +72,4 @@ service ppanel {
|
||||
@handler QueryUserSubscribeNodeList
|
||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -66,7 +66,6 @@ type (
|
||||
UnbindOAuthRequest {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
GetLoginLogRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@ -95,21 +94,17 @@ type (
|
||||
Email string `json:"email" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
GetDeviceListResponse {
|
||||
List []UserDevice `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
UnbindDeviceRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
GetDeviceListResponse {
|
||||
List []UserDevice `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
UnbindDeviceRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
UpdateUserSubscribeNoteRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||
Note string `json:"note" validate:"max=500"`
|
||||
}
|
||||
|
||||
UpdateUserRulesRequest {
|
||||
Rules []string `json:"rules" validate:"required"`
|
||||
}
|
||||
@ -135,23 +130,20 @@ type (
|
||||
List []WithdrawalLog `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
|
||||
GetDeviceOnlineStatsResponse {
|
||||
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
||||
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
||||
}
|
||||
|
||||
WeeklyStat {
|
||||
Day int `json:"day"`
|
||||
DayName string `json:"day_name"`
|
||||
Hours float64 `json:"hours"`
|
||||
}
|
||||
ConnectionRecords {
|
||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||
}
|
||||
GetDeviceOnlineStatsResponse {
|
||||
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
||||
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
||||
}
|
||||
WeeklyStat {
|
||||
Day int `json:"day"`
|
||||
DayName string `json:"day_name"`
|
||||
Hours float64 `json:"hours"`
|
||||
}
|
||||
ConnectionRecords {
|
||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@ -248,9 +240,9 @@ service ppanel {
|
||||
@handler UpdateBindEmail
|
||||
put /bind_email (UpdateBindEmailRequest)
|
||||
|
||||
@doc "Get Device List"
|
||||
@handler GetDeviceList
|
||||
get /devices returns (GetDeviceListResponse)
|
||||
@doc "Get Device List"
|
||||
@handler GetDeviceList
|
||||
get /devices returns (GetDeviceListResponse)
|
||||
|
||||
@doc "Unbind Device"
|
||||
@handler UnbindDevice
|
||||
@ -272,23 +264,23 @@ service ppanel {
|
||||
@handler QueryWithdrawalLog
|
||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||
|
||||
@doc "Device Online Statistics"
|
||||
@handler DeviceOnlineStatistics
|
||||
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
||||
|
||||
@doc "Delete Current User Account"
|
||||
@handler DeleteCurrentUserAccount
|
||||
delete /current_user_account
|
||||
@doc "Device Online Statistics"
|
||||
@handler DeviceOnlineStatistics
|
||||
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
||||
|
||||
@doc "Delete Current User Account"
|
||||
@handler DeleteCurrentUserAccount
|
||||
delete /current_user_account
|
||||
}
|
||||
@server(
|
||||
prefix: v1/public/user
|
||||
group: public/user/ws
|
||||
middleware: AuthMiddleware
|
||||
|
||||
@server (
|
||||
prefix: v1/public/user
|
||||
group: public/user/ws
|
||||
middleware: AuthMiddleware
|
||||
)
|
||||
|
||||
service ppanel {
|
||||
@doc "Webosocket Device Connect"
|
||||
@handler DeviceWsConnect
|
||||
get /device_ws_connect
|
||||
@doc "Webosocket Device Connect"
|
||||
@handler DeviceWsConnect
|
||||
get /device_ws_connect
|
||||
}
|
||||
|
||||
|
||||
@ -84,6 +84,9 @@ type (
|
||||
VerifyCodeLimit int64 `json:"verify_code_limit"`
|
||||
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
||||
}
|
||||
SignatureConfig {
|
||||
EnableSignature bool `json:"enable_signature"`
|
||||
}
|
||||
PubilcVerifyCodeConfig {
|
||||
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
||||
}
|
||||
@ -217,7 +220,7 @@ type (
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
}
|
||||
SubscribeDiscount {
|
||||
Quantity int64 `json:"quantity"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
Discount float64 `json:"discount"`
|
||||
}
|
||||
Subscribe {
|
||||
@ -493,26 +496,26 @@ type (
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
UserSubscribe {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
EntitlementSource string `json:"entitlement_source"`
|
||||
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Short string `json:"short"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Short string `json:"short"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
UserAffiliate {
|
||||
Avatar string `json:"avatar"`
|
||||
@ -555,15 +558,15 @@ type (
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
FamilyMemberItem {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
FamilyDetail {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
@ -737,7 +740,6 @@ type (
|
||||
List []SubscribeGroup `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
GetUserSubscribeTrafficLogsRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@ -915,3 +917,4 @@ type (
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -40,6 +40,21 @@ Redis:
|
||||
ReadTimeout: 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:
|
||||
Email: admin@ppanel.dev # 后台登录邮箱,请修改
|
||||
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码
|
||||
|
||||
@ -13,6 +13,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
|
||||
Invite(svc)
|
||||
Verify(svc)
|
||||
Subscribe(svc)
|
||||
Signature(svc)
|
||||
Register(svc)
|
||||
Mobile(svc)
|
||||
Currency(svc)
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
DELETE
|
||||
FROM `system`
|
||||
WHERE `category` = 'signature'
|
||||
AND `key` = 'EnableSignature';
|
||||
14
initialize/migrate/database/02138_signature_config.up.sql
Normal file
14
initialize/migrate/database/02138_signature_config.up.sql
Normal 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
22
initialize/signature.go
Normal 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
|
||||
}
|
||||
@ -36,6 +36,9 @@ const TosConfigKey = "system:tos_config"
|
||||
// VerifyCodeConfigKey Verify Code Config Key
|
||||
const VerifyCodeConfigKey = "system:verify_code_config"
|
||||
|
||||
// SignatureConfigKey Signature Config Key
|
||||
const SignatureConfigKey = "system:signature_config"
|
||||
|
||||
// SessionIdKey cache session key
|
||||
const SessionIdKey = "auth:session_id"
|
||||
|
||||
|
||||
@ -5,36 +5,39 @@ import (
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
"github.com/perfect-panel/server/pkg/signature"
|
||||
"github.com/perfect-panel/server/pkg/trace"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Loki LokiConfig `yaml:"Loki"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Currency Currency `yaml:"Currency"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
AppSignature signature.SignatureConf `yaml:"AppSignature"`
|
||||
Signature Signature `yaml:"Signature"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Loki LokiConfig `yaml:"Loki"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Currency Currency `yaml:"Currency"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Administrator struct {
|
||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||
Password string `yaml:"Password" default:"password"`
|
||||
@ -122,6 +125,10 @@ type DeviceConfig struct {
|
||||
SecuritySecret string `yaml:"security_secret"`
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
EnableSignature bool `yaml:"EnableSignature" default:"false"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host string `yaml:"Host" default:""`
|
||||
SiteName string `yaml:"SiteName" default:""`
|
||||
|
||||
18
internal/handler/admin/system/getSignatureConfigHandler.go
Normal file
18
internal/handler/admin/system/getSignatureConfigHandler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,6 @@ import (
|
||||
common "github.com/perfect-panel/server/internal/handler/common"
|
||||
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
|
||||
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"
|
||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||
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
|
||||
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")
|
||||
@ -471,6 +464,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// setting telegram bot
|
||||
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
|
||||
adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx))
|
||||
|
||||
@ -579,6 +578,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// kick offline user device
|
||||
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
|
||||
adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx))
|
||||
|
||||
@ -623,21 +637,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get user subcribe traffic logs
|
||||
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")
|
||||
@ -650,18 +649,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Check user telephone is exist
|
||||
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
|
||||
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
||||
|
||||
// Email login
|
||||
authGroupRouter.POST("/login/email", auth.EmailLoginHandler(serverCtx))
|
||||
|
||||
// Device Login
|
||||
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))
|
||||
|
||||
// Check verification code
|
||||
commonGroupRouter.POST("/check_verification_code", middleware.ApiVersionSwitchHandler(
|
||||
common.CheckVerificationCodeV1Handler(serverCtx),
|
||||
common.CheckVerificationCodeV2Handler(serverCtx),
|
||||
))
|
||||
commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx))
|
||||
|
||||
// Get Client
|
||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||
|
||||
// Get Download Link
|
||||
commonGroupRouter.GET("/client/download", common.GetDownloadLinkHandler(serverCtx))
|
||||
|
||||
// Heartbeat
|
||||
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
||||
|
||||
@ -733,12 +717,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get Tos Content
|
||||
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")
|
||||
@ -828,15 +806,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
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.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
@ -961,30 +930,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Query Withdrawal Log
|
||||
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")
|
||||
@ -1015,10 +960,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||
}
|
||||
|
||||
serverV2GroupRouter := router.Group("/v2/server")
|
||||
serverGroupRouter := router.Group("/v2/server")
|
||||
|
||||
{
|
||||
// Get Server Protocol Config
|
||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
}
|
||||
}
|
||||
|
||||
38
internal/logic/admin/system/getSignatureConfigLogic.go
Normal file
38
internal/logic/admin/system/getSignatureConfigLogic.go
Normal 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
|
||||
}
|
||||
53
internal/logic/admin/system/updateSignatureConfigLogic.go
Normal file
53
internal/logic/admin/system/updateSignatureConfigLogic.go
Normal 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
|
||||
}
|
||||
@ -47,6 +47,7 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
|
||||
tool.DeepCopy(&resp.Auth.Email, l.svcCtx.Config.Email)
|
||||
tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile)
|
||||
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.Invite, l.svcCtx.Config.Invite)
|
||||
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)
|
||||
|
||||
@ -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())
|
||||
}
|
||||
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
|
||||
tags := strings.Split(subDetails.NodeTags, ",")
|
||||
tags := normalizeSubscribeNodeTags(subDetails.NodeTags)
|
||||
|
||||
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
|
||||
|
||||
enable := true
|
||||
|
||||
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||
Page: 0,
|
||||
Page: 1,
|
||||
Size: 1000,
|
||||
NodeId: nodeIds,
|
||||
Tag: tags,
|
||||
Enabled: &enable, // Only get enabled nodes
|
||||
})
|
||||
|
||||
@ -213,3 +214,21 @@ func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.
|
||||
|
||||
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...)
|
||||
}
|
||||
|
||||
@ -23,3 +23,11 @@ func TestFillUserSubscribeInfoEntitlementFields(t *testing.T) {
|
||||
require.Equal(t, int64(3001), sub.EntitlementOwnerUserId)
|
||||
require.True(t, sub.ReadOnly)
|
||||
}
|
||||
|
||||
func TestNormalizeSubscribeNodeTags(t *testing.T) {
|
||||
tags := normalizeSubscribeNodeTags("美国, 日本, , 美国, ,日本")
|
||||
require.Equal(t, []string{"美国", "日本"}, tags)
|
||||
|
||||
empty := normalizeSubscribeNodeTags("")
|
||||
require.Nil(t, empty)
|
||||
}
|
||||
|
||||
@ -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-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-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "172800")
|
||||
|
||||
137
internal/middleware/signatureMiddleware.go
Normal file
137
internal/middleware/signatureMiddleware.go
Normal 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
|
||||
}
|
||||
}
|
||||
239
internal/middleware/signatureMiddleware_test.go
Normal file
239
internal/middleware/signatureMiddleware_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@ -202,27 +202,36 @@ func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
|
||||
|
||||
// CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量
|
||||
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
|
||||
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
||||
|
||||
if len(nodeIds) > 0 || len(tags) > 0 {
|
||||
subQuery := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
||||
|
||||
if len(nodeIds) > 0 && len(tags) > 0 {
|
||||
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
|
||||
if len(nodeIds) > 0 {
|
||||
query = query.Where("id IN ?", nodeIds)
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
query = query.Scopes(InSet("tags", tags))
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
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 查询
|
||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
12
internal/model/node/model_test.go
Normal file
12
internal/model/node/model_test.go
Normal 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)
|
||||
}
|
||||
@ -15,7 +15,8 @@ import (
|
||||
|
||||
var _ Model = (*customSubscribeModel)(nil)
|
||||
var (
|
||||
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
||||
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
||||
cacheUserSubscribeUserPrefix = "cache:user:subscribe:user:"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -119,13 +120,31 @@ func (m *defaultSubscribeModel) Update(ctx context.Context, data *Subscribe, tx
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
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 {
|
||||
db := conn
|
||||
if len(tx) > 0 {
|
||||
db = tx[0]
|
||||
}
|
||||
return db.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
}, allCacheKeys...)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -137,13 +156,31 @@ func (m *defaultSubscribeModel) Delete(ctx context.Context, id int64, tx ...*gor
|
||||
}
|
||||
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 {
|
||||
db := conn
|
||||
if len(tx) > 0 {
|
||||
db = tx[0]
|
||||
}
|
||||
return db.Delete(&Subscribe{}, id).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}, allCacheKeys...)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ type customSystemLogicModel interface {
|
||||
GetTosConfig(ctx context.Context) ([]*System, error)
|
||||
GetCurrencyConfig(ctx context.Context) ([]*System, error)
|
||||
GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
|
||||
GetSignatureConfig(ctx context.Context) ([]*System, error)
|
||||
GetLogConfig(ctx context.Context) ([]*System, error)
|
||||
UpdateNodeMultiplierConfig(ctx context.Context, config string) error
|
||||
FindNodeMultiplierConfig(ctx context.Context) (*System, error)
|
||||
@ -154,6 +155,15 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System,
|
||||
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.
|
||||
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
|
||||
var configs []*System
|
||||
|
||||
@ -50,7 +50,14 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
|
||||
}
|
||||
r.Use(sessions.Sessions("ppanel", sessionStore))
|
||||
// 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
|
||||
handler.RegisterHandlers(r, svc)
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/limit"
|
||||
"github.com/perfect-panel/server/pkg/nodeMultiplier"
|
||||
"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"
|
||||
"github.com/hibiken/asynq"
|
||||
@ -36,33 +37,34 @@ import (
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
DB *gorm.DB
|
||||
Redis *redis.Client
|
||||
Config config.Config
|
||||
Queue *asynq.Client
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
DB *gorm.DB
|
||||
Redis *redis.Client
|
||||
Config config.Config
|
||||
Queue *asynq.Client
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
SignatureValidator *signature.Validator
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
LogMessageModel logmessage.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
//ServerModel server.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
RedemptionCodeModel redemption.RedemptionCodeModel
|
||||
RedemptionRecordModel redemption.RedemptionRecordModel
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
RedemptionCodeModel redemption.RedemptionCodeModel
|
||||
RedemptionRecordModel redemption.RedemptionRecordModel
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
IAPAppleTransactionModel iapapple.Model
|
||||
|
||||
Restart func() error
|
||||
@ -109,24 +111,27 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
_ = rds.FlushAll(context.Background()).Err()
|
||||
}
|
||||
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
||||
nonceStore := signature.NewRedisNonceStore(rds)
|
||||
|
||||
srv := &ServiceContext{
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
Config: c,
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 0,
|
||||
GeoIP: geoIP,
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
Config: c,
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 0,
|
||||
GeoIP: geoIP,
|
||||
SignatureValidator: signature.NewValidator(c.AppSignature, nonceStore),
|
||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
LogMessageModel: logmessage.NewModel(db),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
//ServerModel: server.NewModel(db, rds),
|
||||
SystemModel: system.NewModel(db, rds),
|
||||
CouponModel: coupon.NewModel(db, rds),
|
||||
|
||||
61
internal/types/compat_types.go
Normal file
61
internal/types/compat_types.go
Normal 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"`
|
||||
}
|
||||
@ -221,19 +221,6 @@ type CheckVerificationCodeRespone struct {
|
||||
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 {
|
||||
OrderNo string `json:"orderNo"`
|
||||
ReturnUrl string `json:"returnUrl,omitempty"`
|
||||
@ -576,6 +563,11 @@ type DeviceLoginRequest struct {
|
||||
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 {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@ -615,6 +607,34 @@ type EmailAuthticateConfig struct {
|
||||
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 {
|
||||
FilterLogParams
|
||||
UserId int64 `form:"user_id,optional"`
|
||||
@ -887,6 +907,25 @@ type GetDocumentListResponse struct {
|
||||
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 {
|
||||
Site SiteConfig `json:"site"`
|
||||
Verify VeifyConfig `json:"verify"`
|
||||
@ -894,6 +933,7 @@ type GetGlobalConfigResponse struct {
|
||||
Invite InviteConfig `json:"invite"`
|
||||
Currency Currency `json:"currency"`
|
||||
Subscribe SubscribeConfig `json:"subscribe"`
|
||||
Signature SignatureConfig `json:"signature"`
|
||||
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
||||
OAuthMethods []string `json:"oauth_methods"`
|
||||
WebAd bool `json:"web_ad"`
|
||||
@ -1188,41 +1228,6 @@ type GetUserSubscribeResetTrafficLogsResponse struct {
|
||||
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 {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@ -1706,10 +1711,8 @@ type QueryOrderDetailRequest struct {
|
||||
}
|
||||
|
||||
type QueryOrderListRequest struct {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
Status int `form:"status"`
|
||||
Search string `form:"search"`
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
}
|
||||
|
||||
type QueryOrderListResponse struct {
|
||||
@ -1932,6 +1935,12 @@ type RegisterLog struct {
|
||||
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 {
|
||||
UserSubscribeID int64 `json:"user_subscribe_id"`
|
||||
Quantity int64 `json:"quantity" validate:"lte=1000"`
|
||||
@ -2163,6 +2172,10 @@ type ShadowsocksProtocol struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type SignatureConfig struct {
|
||||
EnableSignature bool `json:"enable_signature"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host string `json:"host"`
|
||||
SiteName string `json:"site_name"`
|
||||
@ -2523,6 +2536,11 @@ type UpdateDocumentRequest struct {
|
||||
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 {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@ -2699,7 +2717,6 @@ type User struct {
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
ShareLink string `json:"share_link,omitempty"`
|
||||
RefererId int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
@ -2716,8 +2733,6 @@ type User struct {
|
||||
IsDel bool `json:"is_del,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
PurchasedPackage string `json:"purchased_package,omitempty"`
|
||||
LastLoginTime int64 `json:"last_login_time"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||
FamilyId int64 `json:"family_id,omitempty"`
|
||||
FamilyRole uint8 `json:"family_role,omitempty"`
|
||||
@ -2761,34 +2776,6 @@ type UserLoginLog struct {
|
||||
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 {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
@ -2844,7 +2831,6 @@ type UserSubscribe struct {
|
||||
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Short string `json:"short"`
|
||||
IsGift bool `json:"is_gift"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
@ -3023,221 +3009,3 @@ type WithdrawalLog struct {
|
||||
CreatedAt int64 `json:"created_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"`
|
||||
}
|
||||
|
||||
50
pkg/signature/canonical.go
Normal file
50
pkg/signature/canonical.go
Normal 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
7
pkg/signature/config.go
Normal 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
10
pkg/signature/errors.go
Normal 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")
|
||||
)
|
||||
30
pkg/signature/nonce_store.go
Normal file
30
pkg/signature/nonce_store.go
Normal 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
|
||||
}
|
||||
120
pkg/signature/signature_test.go
Normal file
120
pkg/signature/signature_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
61
pkg/signature/validator.go
Normal file
61
pkg/signature/validator.go
Normal 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
|
||||
}
|
||||
@ -58,6 +58,10 @@ const (
|
||||
InvalidAccess uint32 = 40005
|
||||
InvalidCiphertext uint32 = 40006
|
||||
SecretIsEmpty uint32 = 40007
|
||||
SignatureMissing uint32 = 40008
|
||||
SignatureExpired uint32 = 40009
|
||||
SignatureInvalid uint32 = 40010
|
||||
SignatureReplay uint32 = 40011
|
||||
)
|
||||
|
||||
//coupon error
|
||||
|
||||
@ -17,6 +17,10 @@ func init() {
|
||||
SecretIsEmpty: "Secret is empty",
|
||||
InvalidAccess: "Invalid access",
|
||||
InvalidCiphertext: "Invalid ciphertext",
|
||||
SignatureMissing: "Signature headers are missing",
|
||||
SignatureExpired: "Signature is expired",
|
||||
SignatureInvalid: "Signature is invalid",
|
||||
SignatureReplay: "Signature nonce replay detected",
|
||||
// Database error
|
||||
DatabaseQueryError: "Database query error",
|
||||
DatabaseUpdateError: "Database update error",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user