无订阅 支付后出现两个订阅
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"`
|
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
|
||||||
}
|
}
|
||||||
ToggleRedemptionCodeStatusRequest {
|
ToggleRedemptionCodeStatusRequest {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Status int64 `json:"status" validate:"oneof=0 1"`
|
Status int64 `json:"status" validate:"oneof=0 1"`
|
||||||
}
|
}
|
||||||
DeleteRedemptionCodeRequest {
|
DeleteRedemptionCodeRequest {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
@ -93,3 +93,4 @@ service ppanel {
|
|||||||
@handler GetRedemptionRecordList
|
@handler GetRedemptionRecordList
|
||||||
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
|
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -127,6 +127,14 @@ service ppanel {
|
|||||||
@handler UpdateVerifyCodeConfig
|
@handler UpdateVerifyCodeConfig
|
||||||
put /verify_code_config (VerifyCodeConfig)
|
put /verify_code_config (VerifyCodeConfig)
|
||||||
|
|
||||||
|
@doc "Get Signature Config"
|
||||||
|
@handler GetSignatureConfig
|
||||||
|
get /signature_config returns (SignatureConfig)
|
||||||
|
|
||||||
|
@doc "Update Signature Config"
|
||||||
|
@handler UpdateSignatureConfig
|
||||||
|
put /signature_config (SignatureConfig)
|
||||||
|
|
||||||
@doc "PreView Node Multiplier"
|
@doc "PreView Node Multiplier"
|
||||||
@handler PreViewNodeMultiplier
|
@handler PreViewNodeMultiplier
|
||||||
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
|
||||||
|
|||||||
@ -15,18 +15,18 @@ import (
|
|||||||
type (
|
type (
|
||||||
// GetUserListRequest
|
// GetUserListRequest
|
||||||
GetUserListRequest {
|
GetUserListRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
Search string `form:"search,omitempty"`
|
Search string `form:"search,omitempty"`
|
||||||
UserId *int64 `form:"user_id,omitempty"`
|
UserId *int64 `form:"user_id,omitempty"`
|
||||||
Unscoped bool `form:"unscoped,omitempty"`
|
Unscoped bool `form:"unscoped,omitempty"`
|
||||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||||
ShortCode string `form:"short_code,omitempty"`
|
ShortCode string `form:"short_code,omitempty"`
|
||||||
FamilyJoined *bool `form:"family_joined,omitempty"`
|
FamilyJoined *bool `form:"family_joined,omitempty"`
|
||||||
FamilyStatus string `form:"family_status,omitempty"`
|
FamilyStatus string `form:"family_status,omitempty"`
|
||||||
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
||||||
FamilyId *int64 `form:"family_id,omitempty"`
|
FamilyId *int64 `form:"family_id,omitempty"`
|
||||||
}
|
}
|
||||||
// GetUserListResponse
|
// GetUserListResponse
|
||||||
GetUserListResponse {
|
GetUserListResponse {
|
||||||
@ -366,3 +366,4 @@ service ppanel {
|
|||||||
@handler DissolveFamily
|
@handler DissolveFamily
|
||||||
put /family/dissolve (DissolveFamilyRequest)
|
put /family/dissolve (DissolveFamilyRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type (
|
|||||||
Invite InviteConfig `json:"invite"`
|
Invite InviteConfig `json:"invite"`
|
||||||
Currency Currency `json:"currency"`
|
Currency Currency `json:"currency"`
|
||||||
Subscribe SubscribeConfig `json:"subscribe"`
|
Subscribe SubscribeConfig `json:"subscribe"`
|
||||||
|
Signature SignatureConfig `json:"signature"`
|
||||||
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
||||||
OAuthMethods []string `json:"oauth_methods"`
|
OAuthMethods []string `json:"oauth_methods"`
|
||||||
WebAd bool `json:"web_ad"`
|
WebAd bool `json:"web_ad"`
|
||||||
|
|||||||
@ -30,3 +30,4 @@ service ppanel {
|
|||||||
@handler RedeemCode
|
@handler RedeemCode
|
||||||
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
|
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,9 @@ type (
|
|||||||
QuerySubscribeListRequest {
|
QuerySubscribeListRequest {
|
||||||
Language string `form:"language"`
|
Language string `form:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryUserSubscribeNodeListResponse {
|
QueryUserSubscribeNodeListResponse {
|
||||||
List []UserSubscribeInfo `json:"list"`
|
List []UserSubscribeInfo `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSubscribeInfo {
|
UserSubscribeInfo {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
@ -41,7 +39,6 @@ type (
|
|||||||
IsTryOut bool `json:"is_try_out"`
|
IsTryOut bool `json:"is_try_out"`
|
||||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSubscribeNodeInfo {
|
UserSubscribeNodeInfo {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -75,3 +72,4 @@ service ppanel {
|
|||||||
@handler QueryUserSubscribeNodeList
|
@handler QueryUserSubscribeNodeList
|
||||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,6 @@ type (
|
|||||||
UnbindOAuthRequest {
|
UnbindOAuthRequest {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetLoginLogRequest {
|
GetLoginLogRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@ -95,21 +94,17 @@ type (
|
|||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
Code string `json:"code" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
}
|
}
|
||||||
|
GetDeviceListResponse {
|
||||||
GetDeviceListResponse {
|
List []UserDevice `json:"list"`
|
||||||
List []UserDevice `json:"list"`
|
Total int64 `json:"total"`
|
||||||
Total int64 `json:"total"`
|
}
|
||||||
}
|
UnbindDeviceRequest {
|
||||||
|
Id int64 `json:"id" validate:"required"`
|
||||||
UnbindDeviceRequest {
|
}
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateUserSubscribeNoteRequest {
|
UpdateUserSubscribeNoteRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||||
Note string `json:"note" validate:"max=500"`
|
Note string `json:"note" validate:"max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateUserRulesRequest {
|
UpdateUserRulesRequest {
|
||||||
Rules []string `json:"rules" validate:"required"`
|
Rules []string `json:"rules" validate:"required"`
|
||||||
}
|
}
|
||||||
@ -135,23 +130,20 @@ type (
|
|||||||
List []WithdrawalLog `json:"list"`
|
List []WithdrawalLog `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
GetDeviceOnlineStatsResponse {
|
||||||
|
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
||||||
GetDeviceOnlineStatsResponse {
|
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
||||||
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
}
|
||||||
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
WeeklyStat {
|
||||||
}
|
Day int `json:"day"`
|
||||||
|
DayName string `json:"day_name"`
|
||||||
WeeklyStat {
|
Hours float64 `json:"hours"`
|
||||||
Day int `json:"day"`
|
}
|
||||||
DayName string `json:"day_name"`
|
ConnectionRecords {
|
||||||
Hours float64 `json:"hours"`
|
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||||
}
|
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||||
ConnectionRecords {
|
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
}
|
||||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
|
||||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -248,9 +240,9 @@ service ppanel {
|
|||||||
@handler UpdateBindEmail
|
@handler UpdateBindEmail
|
||||||
put /bind_email (UpdateBindEmailRequest)
|
put /bind_email (UpdateBindEmailRequest)
|
||||||
|
|
||||||
@doc "Get Device List"
|
@doc "Get Device List"
|
||||||
@handler GetDeviceList
|
@handler GetDeviceList
|
||||||
get /devices returns (GetDeviceListResponse)
|
get /devices returns (GetDeviceListResponse)
|
||||||
|
|
||||||
@doc "Unbind Device"
|
@doc "Unbind Device"
|
||||||
@handler UnbindDevice
|
@handler UnbindDevice
|
||||||
@ -272,23 +264,23 @@ service ppanel {
|
|||||||
@handler QueryWithdrawalLog
|
@handler QueryWithdrawalLog
|
||||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||||
|
|
||||||
@doc "Device Online Statistics"
|
@doc "Device Online Statistics"
|
||||||
@handler DeviceOnlineStatistics
|
@handler DeviceOnlineStatistics
|
||||||
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
||||||
|
|
||||||
@doc "Delete Current User Account"
|
|
||||||
@handler DeleteCurrentUserAccount
|
|
||||||
delete /current_user_account
|
|
||||||
|
|
||||||
|
@doc "Delete Current User Account"
|
||||||
|
@handler DeleteCurrentUserAccount
|
||||||
|
delete /current_user_account
|
||||||
}
|
}
|
||||||
@server(
|
|
||||||
prefix: v1/public/user
|
@server (
|
||||||
group: public/user/ws
|
prefix: v1/public/user
|
||||||
middleware: AuthMiddleware
|
group: public/user/ws
|
||||||
|
middleware: AuthMiddleware
|
||||||
)
|
)
|
||||||
|
|
||||||
service ppanel {
|
service ppanel {
|
||||||
@doc "Webosocket Device Connect"
|
@doc "Webosocket Device Connect"
|
||||||
@handler DeviceWsConnect
|
@handler DeviceWsConnect
|
||||||
get /device_ws_connect
|
get /device_ws_connect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -84,6 +84,9 @@ type (
|
|||||||
VerifyCodeLimit int64 `json:"verify_code_limit"`
|
VerifyCodeLimit int64 `json:"verify_code_limit"`
|
||||||
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
||||||
}
|
}
|
||||||
|
SignatureConfig {
|
||||||
|
EnableSignature bool `json:"enable_signature"`
|
||||||
|
}
|
||||||
PubilcVerifyCodeConfig {
|
PubilcVerifyCodeConfig {
|
||||||
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
VerifyCodeInterval int64 `json:"verify_code_interval"`
|
||||||
}
|
}
|
||||||
@ -217,7 +220,7 @@ type (
|
|||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
}
|
}
|
||||||
SubscribeDiscount {
|
SubscribeDiscount {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
Subscribe {
|
Subscribe {
|
||||||
@ -493,26 +496,26 @@ type (
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
UserSubscribe {
|
UserSubscribe {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
OrderId int64 `json:"order_id"`
|
OrderId int64 `json:"order_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
Subscribe Subscribe `json:"subscribe"`
|
Subscribe Subscribe `json:"subscribe"`
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
ExpireTime int64 `json:"expire_time"`
|
ExpireTime int64 `json:"expire_time"`
|
||||||
FinishedAt int64 `json:"finished_at"`
|
FinishedAt int64 `json:"finished_at"`
|
||||||
ResetTime int64 `json:"reset_time"`
|
ResetTime int64 `json:"reset_time"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
Download int64 `json:"download"`
|
Download int64 `json:"download"`
|
||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
EntitlementSource string `json:"entitlement_source"`
|
EntitlementSource string `json:"entitlement_source"`
|
||||||
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
||||||
ReadOnly bool `json:"read_only"`
|
ReadOnly bool `json:"read_only"`
|
||||||
Short string `json:"short"`
|
Short string `json:"short"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
UserAffiliate {
|
UserAffiliate {
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
@ -555,15 +558,15 @@ type (
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
FamilyMemberItem {
|
FamilyMemberItem {
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
Role uint8 `json:"role"`
|
Role uint8 `json:"role"`
|
||||||
RoleName string `json:"role_name"`
|
RoleName string `json:"role_name"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
StatusName string `json:"status_name"`
|
StatusName string `json:"status_name"`
|
||||||
JoinSource string `json:"join_source"`
|
JoinSource string `json:"join_source"`
|
||||||
JoinedAt int64 `json:"joined_at"`
|
JoinedAt int64 `json:"joined_at"`
|
||||||
LeftAt int64 `json:"left_at,omitempty"`
|
LeftAt int64 `json:"left_at,omitempty"`
|
||||||
}
|
}
|
||||||
FamilyDetail {
|
FamilyDetail {
|
||||||
Summary FamilySummary `json:"summary"`
|
Summary FamilySummary `json:"summary"`
|
||||||
@ -737,7 +740,6 @@ type (
|
|||||||
List []SubscribeGroup `json:"list"`
|
List []SubscribeGroup `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetUserSubscribeTrafficLogsRequest {
|
GetUserSubscribeTrafficLogsRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@ -915,3 +917,4 @@ type (
|
|||||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,21 @@ Redis:
|
|||||||
ReadTimeout: 3 # 读操作超时时间(秒),建议2-3秒
|
ReadTimeout: 3 # 读操作超时时间(秒),建议2-3秒
|
||||||
WriteTimeout: 3 # 写操作超时时间(秒),建议2-3秒
|
WriteTimeout: 3 # 写操作超时时间(秒),建议2-3秒
|
||||||
|
|
||||||
|
AppSignature:
|
||||||
|
AppSecrets:
|
||||||
|
android-client: uB4G,XxL2{7b # Android 客户端签名密钥
|
||||||
|
ios-client: uB4G,XxL2{7b # iOS 客户端签名密钥
|
||||||
|
web-client: uB4G,XxL2{7b # Web 客户端签名密钥
|
||||||
|
ValidWindowSeconds: 300 # 签名时间窗口(秒)
|
||||||
|
SkipPrefixes:
|
||||||
|
- /v1/notify/ # 支付回调不验签
|
||||||
|
- /v1/iap/notifications # Apple IAP 回调不验签
|
||||||
|
- /v1/telegram/webhook # Telegram 回调不验签
|
||||||
|
- /v1/subscribe/config # 订阅导出不验签
|
||||||
|
|
||||||
|
Signature:
|
||||||
|
EnableSignature: false # 系统签名开关(实际运行会以数据库 system.signature.EnableSignature 为准)
|
||||||
|
|
||||||
Administrator:
|
Administrator:
|
||||||
Email: admin@ppanel.dev # 后台登录邮箱,请修改
|
Email: admin@ppanel.dev # 后台登录邮箱,请修改
|
||||||
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码
|
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码
|
||||||
|
|||||||
@ -13,6 +13,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
|
|||||||
Invite(svc)
|
Invite(svc)
|
||||||
Verify(svc)
|
Verify(svc)
|
||||||
Subscribe(svc)
|
Subscribe(svc)
|
||||||
|
Signature(svc)
|
||||||
Register(svc)
|
Register(svc)
|
||||||
Mobile(svc)
|
Mobile(svc)
|
||||||
Currency(svc)
|
Currency(svc)
|
||||||
|
|||||||
@ -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
|
// VerifyCodeConfigKey Verify Code Config Key
|
||||||
const VerifyCodeConfigKey = "system:verify_code_config"
|
const VerifyCodeConfigKey = "system:verify_code_config"
|
||||||
|
|
||||||
|
// SignatureConfigKey Signature Config Key
|
||||||
|
const SignatureConfigKey = "system:signature_config"
|
||||||
|
|
||||||
// SessionIdKey cache session key
|
// SessionIdKey cache session key
|
||||||
const SessionIdKey = "auth:session_id"
|
const SessionIdKey = "auth:session_id"
|
||||||
|
|
||||||
|
|||||||
@ -5,36 +5,39 @@ import (
|
|||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"github.com/perfect-panel/server/pkg/orm"
|
"github.com/perfect-panel/server/pkg/orm"
|
||||||
|
"github.com/perfect-panel/server/pkg/signature"
|
||||||
"github.com/perfect-panel/server/pkg/trace"
|
"github.com/perfect-panel/server/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Model string `yaml:"Model" default:"prod"`
|
Model string `yaml:"Model" default:"prod"`
|
||||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||||
Port int `yaml:"Port" default:"8080"`
|
Port int `yaml:"Port" default:"8080"`
|
||||||
Debug bool `yaml:"Debug" default:"false"`
|
Debug bool `yaml:"Debug" default:"false"`
|
||||||
TLS TLS `yaml:"TLS"`
|
TLS TLS `yaml:"TLS"`
|
||||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||||
Logger logger.LogConf `yaml:"Logger"`
|
Logger logger.LogConf `yaml:"Logger"`
|
||||||
MySQL orm.Config `yaml:"MySQL"`
|
MySQL orm.Config `yaml:"MySQL"`
|
||||||
Redis RedisConfig `yaml:"Redis"`
|
Redis RedisConfig `yaml:"Redis"`
|
||||||
Site SiteConfig `yaml:"Site"`
|
Site SiteConfig `yaml:"Site"`
|
||||||
Node NodeConfig `yaml:"Node"`
|
Node NodeConfig `yaml:"Node"`
|
||||||
Mobile MobileConfig `yaml:"Mobile"`
|
Mobile MobileConfig `yaml:"Mobile"`
|
||||||
Email EmailConfig `yaml:"Email"`
|
Email EmailConfig `yaml:"Email"`
|
||||||
Device DeviceConfig `yaml:"device"`
|
Device DeviceConfig `yaml:"device"`
|
||||||
Verify Verify `yaml:"Verify"`
|
AppSignature signature.SignatureConf `yaml:"AppSignature"`
|
||||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
Signature Signature `yaml:"Signature"`
|
||||||
Register RegisterConfig `yaml:"Register"`
|
Verify Verify `yaml:"Verify"`
|
||||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||||
Invite InviteConfig `yaml:"Invite"`
|
Register RegisterConfig `yaml:"Register"`
|
||||||
Kutt KuttConfig `yaml:"Kutt"`
|
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
Invite InviteConfig `yaml:"Invite"`
|
||||||
Loki LokiConfig `yaml:"Loki"`
|
Kutt KuttConfig `yaml:"Kutt"`
|
||||||
Telegram Telegram `yaml:"Telegram"`
|
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||||
Log Log `yaml:"Log"`
|
Loki LokiConfig `yaml:"Loki"`
|
||||||
Currency Currency `yaml:"Currency"`
|
Telegram Telegram `yaml:"Telegram"`
|
||||||
Trace trace.Config `yaml:"Trace"`
|
Log Log `yaml:"Log"`
|
||||||
|
Currency Currency `yaml:"Currency"`
|
||||||
|
Trace trace.Config `yaml:"Trace"`
|
||||||
Administrator struct {
|
Administrator struct {
|
||||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||||
Password string `yaml:"Password" default:"password"`
|
Password string `yaml:"Password" default:"password"`
|
||||||
@ -122,6 +125,10 @@ type DeviceConfig struct {
|
|||||||
SecuritySecret string `yaml:"security_secret"`
|
SecuritySecret string `yaml:"security_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Signature struct {
|
||||||
|
EnableSignature bool `yaml:"EnableSignature" default:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
Host string `yaml:"Host" default:""`
|
Host string `yaml:"Host" default:""`
|
||||||
SiteName string `yaml:"SiteName" default:""`
|
SiteName string `yaml:"SiteName" default:""`
|
||||||
|
|||||||
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"
|
common "github.com/perfect-panel/server/internal/handler/common"
|
||||||
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
|
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
|
||||||
publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
|
publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
|
||||||
publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple"
|
|
||||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||||
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
||||||
@ -237,12 +236,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Filter traffic log details
|
// Filter traffic log details
|
||||||
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
|
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
|
||||||
|
|
||||||
// Error message log list
|
|
||||||
adminLogGroupRouter.GET("/message/error/list", adminLog.GetErrorLogMessageListHandler(serverCtx))
|
|
||||||
|
|
||||||
// Error message log detail
|
|
||||||
adminLogGroupRouter.GET("/message/error/detail", adminLog.GetErrorLogMessageDetailHandler(serverCtx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
|
adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
|
||||||
@ -471,6 +464,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// setting telegram bot
|
// setting telegram bot
|
||||||
adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx))
|
adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get Signature Config
|
||||||
|
adminSystemGroupRouter.GET("/signature_config", adminSystem.GetSignatureConfigHandler(serverCtx))
|
||||||
|
|
||||||
|
// Update Signature Config
|
||||||
|
adminSystemGroupRouter.PUT("/signature_config", adminSystem.UpdateSignatureConfigHandler(serverCtx))
|
||||||
|
|
||||||
// Get site config
|
// Get site config
|
||||||
adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx))
|
adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx))
|
||||||
|
|
||||||
@ -579,6 +578,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// kick offline user device
|
// kick offline user device
|
||||||
adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx))
|
adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get family detail
|
||||||
|
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
|
||||||
|
|
||||||
|
// Dissolve family
|
||||||
|
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get family list
|
||||||
|
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
|
||||||
|
|
||||||
|
// Update family max members
|
||||||
|
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
|
||||||
|
|
||||||
|
// Remove family member
|
||||||
|
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
|
||||||
|
|
||||||
// Get user list
|
// Get user list
|
||||||
adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx))
|
adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx))
|
||||||
|
|
||||||
@ -623,21 +637,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Get user subcribe traffic logs
|
// Get user subcribe traffic logs
|
||||||
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
|
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
|
||||||
|
|
||||||
// Get family list
|
|
||||||
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get family detail
|
|
||||||
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
|
|
||||||
|
|
||||||
// Update family max members
|
|
||||||
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
|
|
||||||
|
|
||||||
// Remove family member
|
|
||||||
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
|
|
||||||
|
|
||||||
// Dissolve family
|
|
||||||
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authGroupRouter := router.Group("/v1/auth")
|
authGroupRouter := router.Group("/v1/auth")
|
||||||
@ -650,18 +649,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Check user telephone is exist
|
// Check user telephone is exist
|
||||||
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
|
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
|
||||||
|
|
||||||
// Check legacy verification code
|
|
||||||
authGroupRouter.POST("/check-code", middleware.ApiVersionSwitchHandler(
|
|
||||||
auth.CheckCodeLegacyV1Handler(serverCtx),
|
|
||||||
auth.CheckCodeLegacyV2Handler(serverCtx),
|
|
||||||
))
|
|
||||||
|
|
||||||
// User login
|
// User login
|
||||||
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
||||||
|
|
||||||
// Email login
|
|
||||||
authGroupRouter.POST("/login/email", auth.EmailLoginHandler(serverCtx))
|
|
||||||
|
|
||||||
// Device Login
|
// Device Login
|
||||||
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
|
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
|
||||||
|
|
||||||
@ -702,17 +692,11 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
|
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
|
||||||
|
|
||||||
// Check verification code
|
// Check verification code
|
||||||
commonGroupRouter.POST("/check_verification_code", middleware.ApiVersionSwitchHandler(
|
commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx))
|
||||||
common.CheckVerificationCodeV1Handler(serverCtx),
|
|
||||||
common.CheckVerificationCodeV2Handler(serverCtx),
|
|
||||||
))
|
|
||||||
|
|
||||||
// Get Client
|
// Get Client
|
||||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||||
|
|
||||||
// Get Download Link
|
|
||||||
commonGroupRouter.GET("/client/download", common.GetDownloadLinkHandler(serverCtx))
|
|
||||||
|
|
||||||
// Heartbeat
|
// Heartbeat
|
||||||
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
||||||
|
|
||||||
@ -733,12 +717,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Get Tos Content
|
// Get Tos Content
|
||||||
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
|
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
|
||||||
|
|
||||||
// Submit contact info
|
|
||||||
commonGroupRouter.POST("/contact", common.SubmitContactHandler(serverCtx))
|
|
||||||
|
|
||||||
// Report client error log
|
|
||||||
commonGroupRouter.POST("/log/message/report", common.ReportLogMessageHandler(serverCtx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
||||||
@ -828,15 +806,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
iapAppleGroupRouter := router.Group("/v1/public/iap/apple")
|
|
||||||
iapAppleGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
|
||||||
{
|
|
||||||
iapAppleGroupRouter.GET("/status", publicIapApple.GetAppleStatusHandler(serverCtx))
|
|
||||||
iapAppleGroupRouter.POST("/transactions/attach", publicIapApple.AttachAppleTransactionHandler(serverCtx))
|
|
||||||
iapAppleGroupRouter.POST("/transactions/attach_by_id", publicIapApple.AttachAppleTransactionByIdHandler(serverCtx))
|
|
||||||
iapAppleGroupRouter.POST("/restore", publicIapApple.RestoreAppleTransactionsHandler(serverCtx))
|
|
||||||
}
|
|
||||||
|
|
||||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -961,30 +930,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
|
|
||||||
// Query Withdrawal Log
|
// Query Withdrawal Log
|
||||||
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||||
|
|
||||||
// Bind Email With Verification
|
|
||||||
publicUserGroupRouter.POST("/bind_email_with_verification", publicUser.BindEmailWithVerificationHandler(serverCtx))
|
|
||||||
|
|
||||||
// Bind Invite Code
|
|
||||||
publicUserGroupRouter.POST("/bind_invite_code", publicUser.BindInviteCodeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get Subscribe Status
|
|
||||||
publicUserGroupRouter.POST("/subscribe_status", publicUser.GetSubscribeStatusHandler(serverCtx))
|
|
||||||
|
|
||||||
// Delete Account
|
|
||||||
publicUserGroupRouter.POST("/delete_account", publicUser.DeleteAccountHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get agent realtime data
|
|
||||||
publicUserGroupRouter.GET("/agent/realtime", publicUser.GetAgentRealtimeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get agent downloads data
|
|
||||||
publicUserGroupRouter.GET("/agent/downloads", publicUser.GetAgentDownloadsHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get user invite statistics
|
|
||||||
publicUserGroupRouter.GET("/invite/stats", publicUser.GetUserInviteStatsHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get invite sales data
|
|
||||||
publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
||||||
@ -1015,10 +960,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverV2GroupRouter := router.Group("/v2/server")
|
serverGroupRouter := router.Group("/v2/server")
|
||||||
|
|
||||||
{
|
{
|
||||||
// Get Server Protocol Config
|
// Get Server Protocol Config
|
||||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.Email, l.svcCtx.Config.Email)
|
||||||
tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile)
|
tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile)
|
||||||
tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register)
|
tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register)
|
||||||
|
tool.DeepCopy(&resp.Signature, l.svcCtx.Config.Signature)
|
||||||
tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify)
|
tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify)
|
||||||
tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite)
|
tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite)
|
||||||
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)
|
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)
|
||||||
|
|||||||
@ -113,16 +113,17 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
|
||||||
}
|
}
|
||||||
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
|
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
|
||||||
tags := strings.Split(subDetails.NodeTags, ",")
|
tags := normalizeSubscribeNodeTags(subDetails.NodeTags)
|
||||||
|
|
||||||
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
|
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
|
||||||
|
|
||||||
enable := true
|
enable := true
|
||||||
|
|
||||||
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||||
Page: 0,
|
Page: 1,
|
||||||
Size: 1000,
|
Size: 1000,
|
||||||
NodeId: nodeIds,
|
NodeId: nodeIds,
|
||||||
|
Tag: tags,
|
||||||
Enabled: &enable, // Only get enabled nodes
|
Enabled: &enable, // Only get enabled nodes
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -213,3 +214,21 @@ func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.
|
|||||||
|
|
||||||
return userSub, nil
|
return userSub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeSubscribeNodeTags(raw string) []string {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
cleaned := make([]string, 0, len(parts))
|
||||||
|
for _, tag := range parts {
|
||||||
|
trimmed := strings.TrimSpace(tag)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cleaned = append(cleaned, trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tool.RemoveDuplicateElements(cleaned...)
|
||||||
|
}
|
||||||
|
|||||||
@ -23,3 +23,11 @@ func TestFillUserSubscribeInfoEntitlementFields(t *testing.T) {
|
|||||||
require.Equal(t, int64(3001), sub.EntitlementOwnerUserId)
|
require.Equal(t, int64(3001), sub.EntitlementOwnerUserId)
|
||||||
require.True(t, sub.ReadOnly)
|
require.True(t, sub.ReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeSubscribeNodeTags(t *testing.T) {
|
||||||
|
tags := normalizeSubscribeNodeTags("美国, 日本, , 美国, ,日本")
|
||||||
|
require.Equal(t, []string{"美国", "日本"}, tags)
|
||||||
|
|
||||||
|
empty := normalizeSubscribeNodeTags("")
|
||||||
|
require.Nil(t, empty)
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,10 @@ func CorsMiddleware(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
// c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host)
|
// c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host)
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header")
|
c.Writer.Header().Set(
|
||||||
|
"Access-Control-Allow-Headers",
|
||||||
|
"Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header, X-Signature-Enabled, X-App-Id, X-Timestamp, X-Nonce, X-Signature",
|
||||||
|
)
|
||||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
c.Writer.Header().Set("Access-Control-Max-Age", "172800")
|
c.Writer.Header().Set("Access-Control-Max-Age", "172800")
|
||||||
|
|||||||
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和标签计算启用的节点数量
|
// CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量
|
||||||
func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) {
|
func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) {
|
||||||
|
tags = normalizeNodeTags(tags)
|
||||||
|
if len(nodeIds) == 0 && len(tags) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
var count int64
|
var count int64
|
||||||
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
||||||
|
if len(nodeIds) > 0 {
|
||||||
if len(nodeIds) > 0 || len(tags) > 0 {
|
query = query.Where("id IN ?", nodeIds)
|
||||||
subQuery := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
if len(nodeIds) > 0 && len(tags) > 0 {
|
query = query.Scopes(InSet("tags", tags))
|
||||||
subQuery = subQuery.Where("id IN ? OR ?", nodeIds, InSet("tags", tags))
|
|
||||||
} else if len(nodeIds) > 0 {
|
|
||||||
subQuery = subQuery.Where("id IN ?", nodeIds)
|
|
||||||
} else {
|
|
||||||
subQuery = subQuery.Scopes(InSet("tags", tags))
|
|
||||||
}
|
|
||||||
|
|
||||||
query = subQuery
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := query.Count(&count).Error
|
err := query.Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeNodeTags(tags []string) []string {
|
||||||
|
cleaned := make([]string, 0, len(tags))
|
||||||
|
for _, tag := range tags {
|
||||||
|
trimmed := strings.TrimSpace(tag)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cleaned = append(cleaned, trimmed)
|
||||||
|
}
|
||||||
|
return tool.RemoveDuplicateElements(cleaned...)
|
||||||
|
}
|
||||||
|
|
||||||
// InSet 支持多值 OR 查询
|
// InSet 支持多值 OR 查询
|
||||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
|||||||
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 _ Model = (*customSubscribeModel)(nil)
|
||||||
var (
|
var (
|
||||||
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
||||||
|
cacheUserSubscribeUserPrefix = "cache:user:subscribe:user:"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -119,13 +120,31 @@ func (m *defaultSubscribeModel) Update(ctx context.Context, data *Subscribe, tx
|
|||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userIds []int64
|
||||||
|
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
return conn.Table("user_subscribe").
|
||||||
|
Where("subscribe_id = ? AND status IN (0, 1)", data.Id).
|
||||||
|
Distinct("user_id").
|
||||||
|
Pluck("user_id", &userIds).Error
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userSubscribeCacheKeys := make([]string, 0, len(userIds))
|
||||||
|
for _, userId := range userIds {
|
||||||
|
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
|
||||||
|
}
|
||||||
|
allCacheKeys := append(m.getCacheKeys(old), userSubscribeCacheKeys...)
|
||||||
|
|
||||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||||
db := conn
|
db := conn
|
||||||
if len(tx) > 0 {
|
if len(tx) > 0 {
|
||||||
db = tx[0]
|
db = tx[0]
|
||||||
}
|
}
|
||||||
return db.Save(data).Error
|
return db.Save(data).Error
|
||||||
}, m.getCacheKeys(old)...)
|
}, allCacheKeys...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,13 +156,31 @@ func (m *defaultSubscribeModel) Delete(ctx context.Context, id int64, tx ...*gor
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userIds []int64
|
||||||
|
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
return conn.Table("user_subscribe").
|
||||||
|
Where("subscribe_id = ? AND status IN (0, 1)", id).
|
||||||
|
Distinct("user_id").
|
||||||
|
Pluck("user_id", &userIds).Error
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userSubscribeCacheKeys := make([]string, 0, len(userIds))
|
||||||
|
for _, userId := range userIds {
|
||||||
|
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
|
||||||
|
}
|
||||||
|
allCacheKeys := append(m.getCacheKeys(data), userSubscribeCacheKeys...)
|
||||||
|
|
||||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||||
db := conn
|
db := conn
|
||||||
if len(tx) > 0 {
|
if len(tx) > 0 {
|
||||||
db = tx[0]
|
db = tx[0]
|
||||||
}
|
}
|
||||||
return db.Delete(&Subscribe{}, id).Error
|
return db.Delete(&Subscribe{}, id).Error
|
||||||
}, m.getCacheKeys(data)...)
|
}, allCacheKeys...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type customSystemLogicModel interface {
|
|||||||
GetTosConfig(ctx context.Context) ([]*System, error)
|
GetTosConfig(ctx context.Context) ([]*System, error)
|
||||||
GetCurrencyConfig(ctx context.Context) ([]*System, error)
|
GetCurrencyConfig(ctx context.Context) ([]*System, error)
|
||||||
GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
|
GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
|
||||||
|
GetSignatureConfig(ctx context.Context) ([]*System, error)
|
||||||
GetLogConfig(ctx context.Context) ([]*System, error)
|
GetLogConfig(ctx context.Context) ([]*System, error)
|
||||||
UpdateNodeMultiplierConfig(ctx context.Context, config string) error
|
UpdateNodeMultiplierConfig(ctx context.Context, config string) error
|
||||||
FindNodeMultiplierConfig(ctx context.Context) (*System, error)
|
FindNodeMultiplierConfig(ctx context.Context) (*System, error)
|
||||||
@ -154,6 +155,15 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System,
|
|||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSignatureConfig returns the signature config.
|
||||||
|
func (m *customSystemModel) GetSignatureConfig(ctx context.Context) ([]*System, error) {
|
||||||
|
var configs []*System
|
||||||
|
err := m.QueryCtx(ctx, &configs, config.SignatureConfigKey, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
return conn.Where("`category` = ?", "signature").Find(v).Error
|
||||||
|
})
|
||||||
|
return configs, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetLogConfig returns the log config.
|
// GetLogConfig returns the log config.
|
||||||
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
|
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
|
||||||
var configs []*System
|
var configs []*System
|
||||||
|
|||||||
@ -50,7 +50,14 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
|
|||||||
}
|
}
|
||||||
r.Use(sessions.Sessions("ppanel", sessionStore))
|
r.Use(sessions.Sessions("ppanel", sessionStore))
|
||||||
// use cors middleware
|
// use cors middleware
|
||||||
r.Use(middleware.TraceMiddleware(svc), middleware.ApiVersionMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery())
|
r.Use(
|
||||||
|
middleware.TraceMiddleware(svc),
|
||||||
|
middleware.ApiVersionMiddleware(svc),
|
||||||
|
middleware.LoggerMiddleware(svc),
|
||||||
|
middleware.SignatureMiddleware(svc),
|
||||||
|
middleware.CorsMiddleware,
|
||||||
|
gin.Recovery(),
|
||||||
|
)
|
||||||
|
|
||||||
// register handlers
|
// register handlers
|
||||||
handler.RegisterHandlers(r, svc)
|
handler.RegisterHandlers(r, svc)
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/limit"
|
"github.com/perfect-panel/server/pkg/limit"
|
||||||
"github.com/perfect-panel/server/pkg/nodeMultiplier"
|
"github.com/perfect-panel/server/pkg/nodeMultiplier"
|
||||||
"github.com/perfect-panel/server/pkg/orm"
|
"github.com/perfect-panel/server/pkg/orm"
|
||||||
|
"github.com/perfect-panel/server/pkg/signature"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
@ -36,33 +37,34 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceContext struct {
|
type ServiceContext struct {
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
Config config.Config
|
Config config.Config
|
||||||
Queue *asynq.Client
|
Queue *asynq.Client
|
||||||
ExchangeRate float64
|
ExchangeRate float64
|
||||||
GeoIP *IPLocation
|
GeoIP *IPLocation
|
||||||
|
SignatureValidator *signature.Validator
|
||||||
|
|
||||||
//NodeCache *cache.NodeCacheClient
|
//NodeCache *cache.NodeCacheClient
|
||||||
AuthModel auth.Model
|
AuthModel auth.Model
|
||||||
AdsModel ads.Model
|
AdsModel ads.Model
|
||||||
LogModel log.Model
|
LogModel log.Model
|
||||||
LogMessageModel logmessage.Model
|
LogMessageModel logmessage.Model
|
||||||
NodeModel node.Model
|
NodeModel node.Model
|
||||||
UserModel user.Model
|
UserModel user.Model
|
||||||
OrderModel order.Model
|
OrderModel order.Model
|
||||||
ClientModel client.Model
|
ClientModel client.Model
|
||||||
TicketModel ticket.Model
|
TicketModel ticket.Model
|
||||||
//ServerModel server.Model
|
//ServerModel server.Model
|
||||||
SystemModel system.Model
|
SystemModel system.Model
|
||||||
CouponModel coupon.Model
|
CouponModel coupon.Model
|
||||||
RedemptionCodeModel redemption.RedemptionCodeModel
|
RedemptionCodeModel redemption.RedemptionCodeModel
|
||||||
RedemptionRecordModel redemption.RedemptionRecordModel
|
RedemptionRecordModel redemption.RedemptionRecordModel
|
||||||
PaymentModel payment.Model
|
PaymentModel payment.Model
|
||||||
DocumentModel document.Model
|
DocumentModel document.Model
|
||||||
SubscribeModel subscribe.Model
|
SubscribeModel subscribe.Model
|
||||||
TrafficLogModel traffic.Model
|
TrafficLogModel traffic.Model
|
||||||
AnnouncementModel announcement.Model
|
AnnouncementModel announcement.Model
|
||||||
IAPAppleTransactionModel iapapple.Model
|
IAPAppleTransactionModel iapapple.Model
|
||||||
|
|
||||||
Restart func() error
|
Restart func() error
|
||||||
@ -109,24 +111,27 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
_ = rds.FlushAll(context.Background()).Err()
|
_ = rds.FlushAll(context.Background()).Err()
|
||||||
}
|
}
|
||||||
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
||||||
|
nonceStore := signature.NewRedisNonceStore(rds)
|
||||||
|
|
||||||
srv := &ServiceContext{
|
srv := &ServiceContext{
|
||||||
DB: db,
|
DB: db,
|
||||||
Redis: rds,
|
Redis: rds,
|
||||||
Config: c,
|
Config: c,
|
||||||
Queue: NewAsynqClient(c),
|
Queue: NewAsynqClient(c),
|
||||||
ExchangeRate: 0,
|
ExchangeRate: 0,
|
||||||
GeoIP: geoIP,
|
GeoIP: geoIP,
|
||||||
|
SignatureValidator: signature.NewValidator(c.AppSignature, nonceStore),
|
||||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||||
AuthLimiter: authLimiter,
|
AuthLimiter: authLimiter,
|
||||||
AdsModel: ads.NewModel(db, rds),
|
AdsModel: ads.NewModel(db, rds),
|
||||||
LogModel: log.NewModel(db),
|
LogModel: log.NewModel(db),
|
||||||
LogMessageModel: logmessage.NewModel(db),
|
LogMessageModel: logmessage.NewModel(db),
|
||||||
NodeModel: node.NewModel(db, rds),
|
NodeModel: node.NewModel(db, rds),
|
||||||
AuthModel: auth.NewModel(db, rds),
|
AuthModel: auth.NewModel(db, rds),
|
||||||
UserModel: user.NewModel(db, rds),
|
UserModel: user.NewModel(db, rds),
|
||||||
OrderModel: order.NewModel(db, rds),
|
OrderModel: order.NewModel(db, rds),
|
||||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||||
TicketModel: ticket.NewModel(db, rds),
|
TicketModel: ticket.NewModel(db, rds),
|
||||||
//ServerModel: server.NewModel(db, rds),
|
//ServerModel: server.NewModel(db, rds),
|
||||||
SystemModel: system.NewModel(db, rds),
|
SystemModel: system.NewModel(db, rds),
|
||||||
CouponModel: coupon.NewModel(db, rds),
|
CouponModel: coupon.NewModel(db, rds),
|
||||||
|
|||||||
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"`
|
Exist bool `json:"exist,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyCheckVerificationCodeRequest struct {
|
|
||||||
Method string `json:"method" form:"method"`
|
|
||||||
Account string `json:"account" form:"account"`
|
|
||||||
Email string `json:"email" form:"email"`
|
|
||||||
Code string `json:"code" form:"code" validate:"required"`
|
|
||||||
Type uint8 `json:"type" form:"type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LegacyCheckVerificationCodeResponse struct {
|
|
||||||
Status bool `json:"status"`
|
|
||||||
Exist bool `json:"exist"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheckoutOrderRequest struct {
|
type CheckoutOrderRequest struct {
|
||||||
OrderNo string `json:"orderNo"`
|
OrderNo string `json:"orderNo"`
|
||||||
ReturnUrl string `json:"returnUrl,omitempty"`
|
ReturnUrl string `json:"returnUrl,omitempty"`
|
||||||
@ -576,6 +563,11 @@ type DeviceLoginRequest struct {
|
|||||||
ShortCode string `json:"short_code,optional"`
|
ShortCode string `json:"short_code,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DissolveFamilyRequest struct {
|
||||||
|
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -615,6 +607,34 @@ type EmailAuthticateConfig struct {
|
|||||||
DomainSuffixList string `json:"domain_suffix_list"`
|
DomainSuffixList string `json:"domain_suffix_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FamilyDetail struct {
|
||||||
|
Summary FamilySummary `json:"summary"`
|
||||||
|
Members []FamilyMemberItem `json:"members"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FamilyMemberItem struct {
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Role uint8 `json:"role"`
|
||||||
|
RoleName string `json:"role_name"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
StatusName string `json:"status_name"`
|
||||||
|
JoinSource string `json:"join_source"`
|
||||||
|
JoinedAt int64 `json:"joined_at"`
|
||||||
|
LeftAt int64 `json:"left_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FamilySummary struct {
|
||||||
|
FamilyId int64 `json:"family_id"`
|
||||||
|
OwnerUserId int64 `json:"owner_user_id"`
|
||||||
|
OwnerIdentifier string `json:"owner_identifier"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ActiveMemberCount int64 `json:"active_member_count"`
|
||||||
|
MaxMembers int64 `json:"max_members"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilterBalanceLogRequest struct {
|
type FilterBalanceLogRequest struct {
|
||||||
FilterLogParams
|
FilterLogParams
|
||||||
UserId int64 `form:"user_id,optional"`
|
UserId int64 `form:"user_id,optional"`
|
||||||
@ -887,6 +907,25 @@ type GetDocumentListResponse struct {
|
|||||||
List []Document `json:"list"`
|
List []Document `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetFamilyDetailRequest struct {
|
||||||
|
Id int64 `form:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFamilyListRequest struct {
|
||||||
|
Page int `form:"page"`
|
||||||
|
Size int `form:"size"`
|
||||||
|
Keyword string `form:"keyword,omitempty"`
|
||||||
|
Status string `form:"status,omitempty"`
|
||||||
|
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
||||||
|
FamilyId *int64 `form:"family_id,omitempty"`
|
||||||
|
UserId *int64 `form:"user_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFamilyListResponse struct {
|
||||||
|
List []FamilySummary `json:"list"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetGlobalConfigResponse struct {
|
type GetGlobalConfigResponse struct {
|
||||||
Site SiteConfig `json:"site"`
|
Site SiteConfig `json:"site"`
|
||||||
Verify VeifyConfig `json:"verify"`
|
Verify VeifyConfig `json:"verify"`
|
||||||
@ -894,6 +933,7 @@ type GetGlobalConfigResponse struct {
|
|||||||
Invite InviteConfig `json:"invite"`
|
Invite InviteConfig `json:"invite"`
|
||||||
Currency Currency `json:"currency"`
|
Currency Currency `json:"currency"`
|
||||||
Subscribe SubscribeConfig `json:"subscribe"`
|
Subscribe SubscribeConfig `json:"subscribe"`
|
||||||
|
Signature SignatureConfig `json:"signature"`
|
||||||
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
||||||
OAuthMethods []string `json:"oauth_methods"`
|
OAuthMethods []string `json:"oauth_methods"`
|
||||||
WebAd bool `json:"web_ad"`
|
WebAd bool `json:"web_ad"`
|
||||||
@ -1188,41 +1228,6 @@ type GetUserSubscribeResetTrafficLogsResponse struct {
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetFamilyListRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
Size int `form:"size"`
|
|
||||||
Keyword string `form:"keyword,omitempty"`
|
|
||||||
Status string `form:"status,omitempty"`
|
|
||||||
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
|
||||||
FamilyId *int64 `form:"family_id,omitempty"`
|
|
||||||
UserId *int64 `form:"user_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetFamilyListResponse struct {
|
|
||||||
List []FamilySummary `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetFamilyDetailRequest struct {
|
|
||||||
Id int64 `form:"id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateFamilyMaxMembersRequest struct {
|
|
||||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
|
||||||
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveFamilyMemberRequest struct {
|
|
||||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
|
||||||
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
|
||||||
Reason string `json:"reason,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DissolveFamilyRequest struct {
|
|
||||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
|
||||||
Reason string `json:"reason,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetUserSubscribeTrafficLogsRequest struct {
|
type GetUserSubscribeTrafficLogsRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@ -1706,10 +1711,8 @@ type QueryOrderDetailRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QueryOrderListRequest struct {
|
type QueryOrderListRequest struct {
|
||||||
Page int `form:"page" validate:"required"`
|
Page int `form:"page" validate:"required"`
|
||||||
Size int `form:"size" validate:"required"`
|
Size int `form:"size" validate:"required"`
|
||||||
Status int `form:"status"`
|
|
||||||
Search string `form:"search"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryOrderListResponse struct {
|
type QueryOrderListResponse struct {
|
||||||
@ -1932,6 +1935,12 @@ type RegisterLog struct {
|
|||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RemoveFamilyMemberRequest struct {
|
||||||
|
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||||
|
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RenewalOrderRequest struct {
|
type RenewalOrderRequest struct {
|
||||||
UserSubscribeID int64 `json:"user_subscribe_id"`
|
UserSubscribeID int64 `json:"user_subscribe_id"`
|
||||||
Quantity int64 `json:"quantity" validate:"lte=1000"`
|
Quantity int64 `json:"quantity" validate:"lte=1000"`
|
||||||
@ -2163,6 +2172,10 @@ type ShadowsocksProtocol struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignatureConfig struct {
|
||||||
|
EnableSignature bool `json:"enable_signature"`
|
||||||
|
}
|
||||||
|
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
SiteName string `json:"site_name"`
|
SiteName string `json:"site_name"`
|
||||||
@ -2523,6 +2536,11 @@ type UpdateDocumentRequest struct {
|
|||||||
Show *bool `json:"show"`
|
Show *bool `json:"show"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateFamilyMaxMembersRequest struct {
|
||||||
|
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||||
|
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateNodeRequest struct {
|
type UpdateNodeRequest struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -2699,7 +2717,6 @@ type User struct {
|
|||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount int64 `json:"gift_amount"`
|
||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
ShareLink string `json:"share_link,omitempty"`
|
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId int64 `json:"referer_id"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
IsAdmin bool `json:"is_admin,omitempty"`
|
IsAdmin bool `json:"is_admin,omitempty"`
|
||||||
@ -2716,8 +2733,6 @@ type User struct {
|
|||||||
IsDel bool `json:"is_del,omitempty"`
|
IsDel bool `json:"is_del,omitempty"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
PurchasedPackage string `json:"purchased_package,omitempty"`
|
PurchasedPackage string `json:"purchased_package,omitempty"`
|
||||||
LastLoginTime int64 `json:"last_login_time"`
|
|
||||||
MemberStatus string `json:"member_status"`
|
|
||||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||||
FamilyId int64 `json:"family_id,omitempty"`
|
FamilyId int64 `json:"family_id,omitempty"`
|
||||||
FamilyRole uint8 `json:"family_role,omitempty"`
|
FamilyRole uint8 `json:"family_role,omitempty"`
|
||||||
@ -2761,34 +2776,6 @@ type UserLoginLog struct {
|
|||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FamilySummary struct {
|
|
||||||
FamilyId int64 `json:"family_id"`
|
|
||||||
OwnerUserId int64 `json:"owner_user_id"`
|
|
||||||
OwnerIdentifier string `json:"owner_identifier"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ActiveMemberCount int64 `json:"active_member_count"`
|
|
||||||
MaxMembers int64 `json:"max_members"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FamilyMemberItem struct {
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
Identifier string `json:"identifier"`
|
|
||||||
Role uint8 `json:"role"`
|
|
||||||
RoleName string `json:"role_name"`
|
|
||||||
Status uint8 `json:"status"`
|
|
||||||
StatusName string `json:"status_name"`
|
|
||||||
JoinSource string `json:"join_source"`
|
|
||||||
JoinedAt int64 `json:"joined_at"`
|
|
||||||
LeftAt int64 `json:"left_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FamilyDetail struct {
|
|
||||||
Summary FamilySummary `json:"summary"`
|
|
||||||
Members []FamilyMemberItem `json:"members"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserLoginRequest struct {
|
type UserLoginRequest struct {
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
@ -2844,7 +2831,6 @@ type UserSubscribe struct {
|
|||||||
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
||||||
ReadOnly bool `json:"read_only"`
|
ReadOnly bool `json:"read_only"`
|
||||||
Short string `json:"short"`
|
Short string `json:"short"`
|
||||||
IsGift bool `json:"is_gift"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@ -3023,221 +3009,3 @@ type WithdrawalLog struct {
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailLoginRequest struct {
|
|
||||||
Identifier string `json:"identifier"`
|
|
||||||
Email string `json:"email" validate:"required"`
|
|
||||||
Code string `json:"code" validate:"required"`
|
|
||||||
Invite string `json:"invite,optional"`
|
|
||||||
IP string `header:"X-Original-Forwarded-For"`
|
|
||||||
UserAgent string `header:"User-Agent"`
|
|
||||||
LoginType string `header:"Login-Type"`
|
|
||||||
CfToken string `json:"cf_token,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BindEmailWithVerificationRequest struct {
|
|
||||||
Email string `json:"email" validate:"required"`
|
|
||||||
Code string `json:"code" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BindEmailWithVerificationResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
UserId int64 `json:"user_id,omitempty"`
|
|
||||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
|
||||||
FamilyId int64 `json:"family_id,omitempty"`
|
|
||||||
OwnerUserId int64 `json:"owner_user_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BindInviteCodeRequest struct {
|
|
||||||
InviteCode string `json:"invite_code" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSubscribeStatusResponse struct {
|
|
||||||
DeviceStatus bool `json:"device_status"`
|
|
||||||
EmailStatus bool `json:"email_status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSubscribeStatusRequest struct {
|
|
||||||
Email string `json:"email" validate:"required,email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAccountResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDownloadLinkRequest struct {
|
|
||||||
InviteCode string `form:"invite_code,optional"`
|
|
||||||
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDownloadLinkResponse struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContactRequest struct {
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Email string `json:"email" validate:"required,email"`
|
|
||||||
OtherContact string `json:"other_contact,omitempty"`
|
|
||||||
Notes string `json:"notes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportLogMessageRequest struct {
|
|
||||||
Platform string `json:"platform" validate:"required"`
|
|
||||||
AppVersion string `json:"appVersion"`
|
|
||||||
OsName string `json:"osName"`
|
|
||||||
OsVersion string `json:"osVersion"`
|
|
||||||
DeviceId string `json:"deviceId"`
|
|
||||||
UserId int64 `json:"userId"`
|
|
||||||
SessionId string `json:"sessionId"`
|
|
||||||
Level uint8 `json:"level"`
|
|
||||||
ErrorCode string `json:"errorCode"`
|
|
||||||
Message string `json:"message" validate:"required"`
|
|
||||||
Stack string `json:"stack"`
|
|
||||||
Context map[string]interface{} `json:"context"`
|
|
||||||
OccurredAt int64 `json:"occurredAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportLogMessageResponse struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetErrorLogMessageListRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
Size int `form:"size"`
|
|
||||||
Platform string `form:"platform"`
|
|
||||||
Level uint8 `form:"level"`
|
|
||||||
UserId int64 `form:"user_id"`
|
|
||||||
DeviceId string `form:"device_id"`
|
|
||||||
ErrorCode string `form:"error_code"`
|
|
||||||
Keyword string `form:"keyword"`
|
|
||||||
Start int64 `form:"start"`
|
|
||||||
End int64 `form:"end"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorLogMessage struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
AppVersion string `json:"app_version"`
|
|
||||||
OsName string `json:"os_name"`
|
|
||||||
OsVersion string `json:"os_version"`
|
|
||||||
DeviceId string `json:"device_id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Level uint8 `json:"level"`
|
|
||||||
ErrorCode string `json:"error_code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetErrorLogMessageListResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []ErrorLogMessage `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetErrorLogMessageDetailResponse struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
AppVersion string `json:"app_version"`
|
|
||||||
OsName string `json:"os_name"`
|
|
||||||
OsVersion string `json:"os_version"`
|
|
||||||
DeviceId string `json:"device_id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Level uint8 `json:"level"`
|
|
||||||
ErrorCode string `json:"error_code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Stack string `json:"stack"`
|
|
||||||
Context map[string]interface{} `json:"context"`
|
|
||||||
ClientIP string `json:"client_ip"`
|
|
||||||
UserAgent string `json:"user_agent"`
|
|
||||||
Locale string `json:"locale"`
|
|
||||||
OccurredAt int64 `json:"occurred_at"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachAppleTransactionRequest struct {
|
|
||||||
SignedTransactionJWS string `json:"signed_transaction_jws" validate:"required"`
|
|
||||||
DurationDays int64 `json:"duration_days,omitempty"`
|
|
||||||
Tier string `json:"tier,omitempty"`
|
|
||||||
SubscribeId int64 `json:"subscribe_id,omitempty"`
|
|
||||||
OrderNo string `json:"order_no" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachAppleTransactionResponse struct {
|
|
||||||
ExpiresAt int64 `json:"expires_at"`
|
|
||||||
Tier string `json:"tier"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttachAppleTransactionByIdRequest struct {
|
|
||||||
TransactionId string `json:"transaction_id" validate:"required"`
|
|
||||||
OrderNo string `json:"order_no" validate:"required"`
|
|
||||||
Sandbox *bool `json:"sandbox,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RestoreAppleTransactionsRequest struct {
|
|
||||||
Transactions []string `json:"transactions" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAppleStatusResponse struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
ExpiresAt int64 `json:"expires_at"`
|
|
||||||
Tier string `json:"tier"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAgentRealtimeRequest struct{}
|
|
||||||
|
|
||||||
type GetAgentRealtimeResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Clicks int64 `json:"clicks"`
|
|
||||||
Views int64 `json:"views"`
|
|
||||||
Installs int64 `json:"installs"`
|
|
||||||
PaidCount int64 `json:"paid_count"`
|
|
||||||
GrowthRate string `json:"growth_rate"`
|
|
||||||
PaidGrowthRate string `json:"paid_growth_rate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAgentDownloadsRequest struct{}
|
|
||||||
|
|
||||||
type GetAgentDownloadsResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Platforms *PlatformDownloads `json:"platforms"`
|
|
||||||
ComparisonRate *string `json:"comparison_rate,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlatformDownloads struct {
|
|
||||||
IOS int64 `json:"ios"`
|
|
||||||
Android int64 `json:"android"`
|
|
||||||
Windows int64 `json:"windows"`
|
|
||||||
Mac int64 `json:"mac"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetUserInviteStatsRequest struct{}
|
|
||||||
|
|
||||||
type GetUserInviteStatsResponse struct {
|
|
||||||
FriendlyCount int64 `json:"friendly_count"`
|
|
||||||
HistoryCount int64 `json:"history_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetInviteSalesRequest struct {
|
|
||||||
Page int `form:"page" validate:"required"`
|
|
||||||
Size int `form:"size" validate:"required"`
|
|
||||||
StartTime int64 `form:"start_time,optional"`
|
|
||||||
EndTime int64 `form:"end_time,optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetInviteSalesResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []InvitedUserSale `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InvitedUserSale struct {
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
UpdatedAt int64 `json:"update_at"`
|
|
||||||
UserHash string `json:"user_hash"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
}
|
|
||||||
|
|||||||
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
|
InvalidAccess uint32 = 40005
|
||||||
InvalidCiphertext uint32 = 40006
|
InvalidCiphertext uint32 = 40006
|
||||||
SecretIsEmpty uint32 = 40007
|
SecretIsEmpty uint32 = 40007
|
||||||
|
SignatureMissing uint32 = 40008
|
||||||
|
SignatureExpired uint32 = 40009
|
||||||
|
SignatureInvalid uint32 = 40010
|
||||||
|
SignatureReplay uint32 = 40011
|
||||||
)
|
)
|
||||||
|
|
||||||
//coupon error
|
//coupon error
|
||||||
|
|||||||
@ -17,6 +17,10 @@ func init() {
|
|||||||
SecretIsEmpty: "Secret is empty",
|
SecretIsEmpty: "Secret is empty",
|
||||||
InvalidAccess: "Invalid access",
|
InvalidAccess: "Invalid access",
|
||||||
InvalidCiphertext: "Invalid ciphertext",
|
InvalidCiphertext: "Invalid ciphertext",
|
||||||
|
SignatureMissing: "Signature headers are missing",
|
||||||
|
SignatureExpired: "Signature is expired",
|
||||||
|
SignatureInvalid: "Signature is invalid",
|
||||||
|
SignatureReplay: "Signature nonce replay detected",
|
||||||
// Database error
|
// Database error
|
||||||
DatabaseQueryError: "Database query error",
|
DatabaseQueryError: "Database query error",
|
||||||
DatabaseUpdateError: "Database update error",
|
DatabaseUpdateError: "Database update error",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user