| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- // webapi_keygen generates and manages Web API keys for the RAS Web AIM API.
- // Usage: go run ./cmd/webapi_keygen [command] [options]
- package main
- import (
- "context"
- "crypto/rand"
- "encoding/hex"
- "encoding/json"
- "flag"
- "fmt"
- "os"
- "strings"
- "text/tabwriter"
- "time"
- "github.com/google/uuid"
- "github.com/joho/godotenv"
- "github.com/mk6i/retro-aim-server/state"
- )
- const (
- keyLength = 32 // 256 bits of entropy
- )
- func main() {
- // Load environment configuration
- if err := godotenv.Load("config/settings.env"); err != nil {
- fmt.Printf("Config file not found, using environment variables\n")
- }
- if len(os.Args) < 2 {
- printUsage()
- os.Exit(1)
- }
- command := os.Args[1]
- args := os.Args[2:]
- switch command {
- case "generate", "gen":
- handleGenerate(args)
- case "list", "ls":
- handleList(args)
- case "revoke", "delete", "rm":
- handleRevoke(args)
- case "activate":
- handleActivate(args)
- case "update":
- handleUpdate(args)
- case "show":
- handleShow(args)
- case "help", "-h", "--help":
- printUsage()
- default:
- fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
- printUsage()
- os.Exit(1)
- }
- }
- func printUsage() {
- fmt.Println("Web API Key Generator for RAS")
- fmt.Println("\nUsage: webapi_keygen <command> [options]")
- fmt.Println("\nCommands:")
- fmt.Println(" generate, gen Generate a new API key")
- fmt.Println(" list, ls List all API keys")
- fmt.Println(" show Show details of a specific key")
- fmt.Println(" revoke, delete Deactivate an API key")
- fmt.Println(" activate Reactivate an API key")
- fmt.Println(" update Update API key settings")
- fmt.Println(" help Show this help message")
- fmt.Println("\nGenerate Options:")
- fmt.Println(" --app-name Application name (required)")
- fmt.Println(" --origins Comma-separated list of allowed origins")
- fmt.Println(" --rate-limit Requests per minute (default: 60)")
- fmt.Println(" --capabilities Comma-separated list of capabilities")
- fmt.Println("\nUpdate Options:")
- fmt.Println(" --dev-id Developer ID to update (required)")
- fmt.Println(" --app-name New application name")
- fmt.Println(" --origins New comma-separated list of allowed origins")
- fmt.Println(" --rate-limit New requests per minute limit")
- fmt.Println(" --capabilities New comma-separated list of capabilities")
- fmt.Println("\nExamples:")
- fmt.Println(" webapi_keygen generate --app-name \"My Web Client\" --origins \"https://example.com,https://app.example.com\"")
- fmt.Println(" webapi_keygen list")
- fmt.Println(" webapi_keygen show --dev-id dev_abc123")
- fmt.Println(" webapi_keygen revoke --dev-id dev_abc123")
- fmt.Println(" webapi_keygen update --dev-id dev_abc123 --rate-limit 120")
- }
- func handleGenerate(args []string) {
- fs := flag.NewFlagSet("generate", flag.ExitOnError)
- appName := fs.String("app-name", "", "Application name (required)")
- originsStr := fs.String("origins", "", "Comma-separated list of allowed origins")
- rateLimit := fs.Int("rate-limit", 60, "Requests per minute")
- capabilitiesStr := fs.String("capabilities", "", "Comma-separated list of capabilities")
- if err := fs.Parse(args); err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
- os.Exit(1)
- }
- if *appName == "" {
- fmt.Fprintln(os.Stderr, "Error: --app-name is required")
- os.Exit(1)
- }
- // Parse origins and capabilities
- var origins []string
- if *originsStr != "" {
- origins = parseCSV(*originsStr)
- }
- var capabilities []string
- if *capabilitiesStr != "" {
- capabilities = parseCSV(*capabilitiesStr)
- }
- // Generate secure random key
- keyBytes := make([]byte, keyLength)
- if _, err := rand.Read(keyBytes); err != nil {
- fmt.Fprintf(os.Stderr, "Error generating key: %v\n", err)
- os.Exit(1)
- }
- devKey := hex.EncodeToString(keyBytes)
- // Generate dev_id
- devID := fmt.Sprintf("dev_%s", uuid.New().String())
- // Create the API key record
- apiKey := state.WebAPIKey{
- DevID: devID,
- DevKey: devKey,
- AppName: *appName,
- CreatedAt: time.Now(),
- IsActive: true,
- RateLimit: *rateLimit,
- AllowedOrigins: origins,
- Capabilities: capabilities,
- }
- // Connect to database and insert the key
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- if err := store.CreateAPIKey(ctx, apiKey); err != nil {
- fmt.Fprintf(os.Stderr, "Error creating API key: %v\n", err)
- os.Exit(1)
- }
- // Output the generated key details
- fmt.Println("Successfully generated Web API key:")
- fmt.Println("=====================================")
- fmt.Printf("Developer ID: %s\n", devID)
- fmt.Printf("API Key: %s\n", devKey)
- fmt.Printf("App Name: %s\n", *appName)
- fmt.Printf("Rate Limit: %d requests/minute\n", *rateLimit)
- if len(origins) > 0 {
- fmt.Printf("Origins: %s\n", strings.Join(origins, ", "))
- }
- if len(capabilities) > 0 {
- fmt.Printf("Capabilities: %s\n", strings.Join(capabilities, ", "))
- }
- fmt.Println("=====================================")
- fmt.Println("\nIMPORTANT: Save the API key securely. It cannot be retrieved later.")
- }
- func handleList(args []string) {
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- keys, err := store.ListAPIKeys(ctx)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error listing API keys: %v\n", err)
- os.Exit(1)
- }
- if len(keys) == 0 {
- fmt.Println("No API keys found.")
- return
- }
- // Create a tabwriter for formatted output
- w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
- fmt.Fprintln(w, "DEV ID\tAPP NAME\tACTIVE\tRATE LIMIT\tCREATED\tLAST USED")
- fmt.Fprintln(w, "------\t--------\t------\t----------\t-------\t---------")
- for _, key := range keys {
- lastUsed := "Never"
- if key.LastUsed != nil {
- lastUsed = key.LastUsed.Format("2006-01-02 15:04")
- }
- fmt.Fprintf(w, "%s\t%s\t%v\t%d/min\t%s\t%s\n",
- truncateString(key.DevID, 20),
- truncateString(key.AppName, 20),
- key.IsActive,
- key.RateLimit,
- key.CreatedAt.Format("2006-01-02"),
- lastUsed,
- )
- }
- w.Flush()
- }
- func handleShow(args []string) {
- fs := flag.NewFlagSet("show", flag.ExitOnError)
- devID := fs.String("dev-id", "", "Developer ID (required)")
- if err := fs.Parse(args); err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
- os.Exit(1)
- }
- if *devID == "" {
- fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
- os.Exit(1)
- }
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- key, err := store.GetAPIKeyByDevID(ctx, *devID)
- if err != nil {
- if err == state.ErrNoAPIKey {
- fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
- } else {
- fmt.Fprintf(os.Stderr, "Error retrieving API key: %v\n", err)
- }
- os.Exit(1)
- }
- // Output detailed key information
- fmt.Println("Web API Key Details:")
- fmt.Println("=====================================")
- fmt.Printf("Developer ID: %s\n", key.DevID)
- fmt.Printf("App Name: %s\n", key.AppName)
- fmt.Printf("Active: %v\n", key.IsActive)
- fmt.Printf("Rate Limit: %d requests/minute\n", key.RateLimit)
- fmt.Printf("Created: %s\n", key.CreatedAt.Format("2006-01-02 15:04:05"))
- if key.LastUsed != nil {
- fmt.Printf("Last Used: %s\n", key.LastUsed.Format("2006-01-02 15:04:05"))
- } else {
- fmt.Println("Last Used: Never")
- }
- if len(key.AllowedOrigins) > 0 {
- fmt.Printf("Origins: %s\n", strings.Join(key.AllowedOrigins, ", "))
- } else {
- fmt.Println("Origins: All origins allowed")
- }
- if len(key.Capabilities) > 0 {
- fmt.Printf("Capabilities: %s\n", strings.Join(key.Capabilities, ", "))
- } else {
- fmt.Println("Capabilities: All capabilities enabled")
- }
- fmt.Println("=====================================")
- }
- func handleRevoke(args []string) {
- fs := flag.NewFlagSet("revoke", flag.ExitOnError)
- devID := fs.String("dev-id", "", "Developer ID to revoke (required)")
- if err := fs.Parse(args); err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
- os.Exit(1)
- }
- if *devID == "" {
- fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
- os.Exit(1)
- }
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- isActive := false
- update := state.WebAPIKeyUpdate{
- IsActive: &isActive,
- }
- if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
- if err == state.ErrNoAPIKey {
- fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
- } else {
- fmt.Fprintf(os.Stderr, "Error revoking API key: %v\n", err)
- }
- os.Exit(1)
- }
- fmt.Printf("Successfully revoked API key: %s\n", *devID)
- }
- func handleActivate(args []string) {
- fs := flag.NewFlagSet("activate", flag.ExitOnError)
- devID := fs.String("dev-id", "", "Developer ID to activate (required)")
- if err := fs.Parse(args); err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
- os.Exit(1)
- }
- if *devID == "" {
- fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
- os.Exit(1)
- }
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- isActive := true
- update := state.WebAPIKeyUpdate{
- IsActive: &isActive,
- }
- if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
- if err == state.ErrNoAPIKey {
- fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
- } else {
- fmt.Fprintf(os.Stderr, "Error activating API key: %v\n", err)
- }
- os.Exit(1)
- }
- fmt.Printf("Successfully activated API key: %s\n", *devID)
- }
- func handleUpdate(args []string) {
- fs := flag.NewFlagSet("update", flag.ExitOnError)
- devID := fs.String("dev-id", "", "Developer ID to update (required)")
- appName := fs.String("app-name", "", "New application name")
- originsStr := fs.String("origins", "", "New comma-separated list of allowed origins")
- rateLimit := fs.Int("rate-limit", -1, "New requests per minute limit")
- capabilitiesStr := fs.String("capabilities", "", "New comma-separated list of capabilities")
- if err := fs.Parse(args); err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
- os.Exit(1)
- }
- if *devID == "" {
- fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
- os.Exit(1)
- }
- update := state.WebAPIKeyUpdate{}
- if *appName != "" {
- update.AppName = appName
- }
- if *originsStr != "" {
- origins := parseCSV(*originsStr)
- update.AllowedOrigins = &origins
- }
- if *rateLimit > 0 {
- update.RateLimit = rateLimit
- }
- if *capabilitiesStr != "" {
- capabilities := parseCSV(*capabilitiesStr)
- update.Capabilities = &capabilities
- }
- // Check if any updates were provided
- updateJSON, _ := json.Marshal(update)
- if string(updateJSON) == "{}" {
- fmt.Fprintln(os.Stderr, "Error: No update fields provided")
- os.Exit(1)
- }
- store, err := connectToStore()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
- os.Exit(1)
- }
- ctx := context.Background()
- if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
- if err == state.ErrNoAPIKey {
- fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
- } else {
- fmt.Fprintf(os.Stderr, "Error updating API key: %v\n", err)
- }
- os.Exit(1)
- }
- fmt.Printf("Successfully updated API key: %s\n", *devID)
- }
- func connectToStore() (*state.SQLiteUserStore, error) {
- dbPath := os.Getenv("DB_PATH")
- if dbPath == "" {
- dbPath = "oscar.sqlite"
- }
- return state.NewSQLiteUserStore(dbPath)
- }
- func parseCSV(input string) []string {
- if input == "" {
- return []string{}
- }
- parts := strings.Split(input, ",")
- result := make([]string, 0, len(parts))
- for _, part := range parts {
- trimmed := strings.TrimSpace(part)
- if trimmed != "" {
- result = append(result, trimmed)
- }
- }
- return result
- }
- func truncateString(s string, maxLen int) string {
- if len(s) <= maxLen {
- return s
- }
- return s[:maxLen-3] + "..."
- }
|