// Command codegen is a CLI transition tool for batch-generating activation codes // before the admin panel (task #8) is ready. // // Usage: // // codegen -plan=pro -days=30 -count=100 -channel=manual \ // -note="telegram batch june" -dsn="root:pass@tcp(127.0.0.1:3306)/pangolin?parseTime=true" \ // [-out=codes.csv] // // The generated plaintext codes are written to stdout (or -out) as a CSV file. // They are NEVER logged or stored in the database. // The database receives only the SHA-256 hashes. // // Run `codegen -help` for full flag documentation. package main import ( "context" "flag" "fmt" "log" "os" "time" "github.com/wangjia/pangolin/server/internal/codes" "github.com/wangjia/pangolin/server/internal/db" ) func main() { plan := flag.String("plan", "", "Plan code: free | pro | team (required)") days := flag.Int("days", 0, "Duration in days the code grants (required, >0)") count := flag.Int("count", 0, "Number of codes to generate (required, >0)") channel := flag.String("channel", "manual", "Distribution channel: store | tg | line | manual") note := flag.String("note", "", "Human-readable batch note (optional)") dsn := flag.String("dsn", "", "MySQL DSN, e.g. user:pass@tcp(host:port)/dbname?parseTime=true (required, or set DB_DSN env)") out := flag.String("out", "", "Output CSV file path (default: stdout)") createdBy := flag.String("by", "cli", "Identifier of the operator creating this batch") flag.Parse() // Resolve DSN from flag or environment. dsnVal := *dsn if dsnVal == "" { dsnVal = os.Getenv("DB_DSN") } // Validation. if dsnVal == "" { log.Fatal("codegen: -dsn or DB_DSN is required") } if *plan == "" { log.Fatal("codegen: -plan is required") } if *days <= 0 { log.Fatal("codegen: -days must be > 0") } if *count <= 0 { log.Fatal("codegen: -count must be > 0") } planCode := codes.PlanCode(*plan) switch planCode { case codes.PlanFree, codes.PlanPro, codes.PlanTeam: default: log.Fatalf("codegen: unknown plan %q; must be free | pro | team", *plan) } ch := codes.BatchChannel(*channel) switch ch { case codes.ChannelStore, codes.ChannelTG, codes.ChannelLine, codes.ChannelManual: default: log.Fatalf("codegen: unknown channel %q; must be store | tg | line | manual", *channel) } // Open database. dbConn, err := db.Open(dsnVal) if err != nil { log.Fatalf("codegen: database: %v", err) } defer dbConn.Close() store := codes.NewStore(dbConn) // Service is created without Redis (batch generation doesn't need rate limiting). svc := codes.NewService(store, nil, 5, time.Hour) log.Printf("codegen: generating %d %s/%dd codes for channel=%s …", *count, planCode, *days, ch) result, err := svc.CreateBatch(context.Background(), codes.BatchRequest{ PlanCode: planCode, DurationDays: *days, Count: *count, Channel: ch, Note: *note, CreatedBy: *createdBy, }) if err != nil { log.Fatalf("codegen: CreateBatch: %v", err) } log.Printf("codegen: batch %d created; writing CSV …", result.BatchID) // Determine output writer. output := os.Stdout if *out != "" { f, err := os.Create(*out) if err != nil { log.Fatalf("codegen: open output file: %v", err) } defer f.Close() output = f } csvRows := make([]codes.CSVRow, len(result.Codes)) now := time.Now().UTC() for i, c := range result.Codes { csvRows[i] = codes.CSVRow{ Index: i + 1, Code: c, Plan: planCode, DurationDays: *days, BatchID: result.BatchID, Channel: ch, GeneratedAt: now, } } if err := codes.ExportCSV(output, csvRows); err != nil { log.Fatalf("codegen: ExportCSV: %v", err) } if *out != "" { fmt.Printf("codegen: %d codes written to %s\n", *count, *out) fmt.Printf("codegen: ⚠️ Treat this file as a secret — it contains plaintext activation codes.\n") } else { fmt.Fprintf(os.Stderr, "codegen: %d codes written to stdout\n", *count) fmt.Fprintf(os.Stderr, "codegen: ⚠️ Treat the output as a secret — it contains plaintext activation codes.\n") } }