From f3c4febcb55689f735f648311e6757761e355963 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:50:12 +0000 Subject: [PATCH] feat: implement API key authentication for iOS client security - Added secure API key generation using crypto/rand (256-bit keys) - Implemented authentication middleware with X-API-Key header validation - Protected all endpoints except health check and API key generation - Added master key system for controlled API key generation - Created comprehensive iOS implementation guide in CLAUDE.md with: - Keychain storage examples for secure key management - Network request examples with authentication headers - Biometric authentication integration - Security best practices following Apple's latest standards - Added API key persistence in api_keys.json - Included comprehensive test coverage for authentication - Updated README with authentication documentation Co-authored-by: Ben Christensen --- CLAUDE.md | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 47 +++++++++- auth.go | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ auth_test.go | 210 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 24 +++++- 5 files changed, 735 insertions(+), 12 deletions(-) create mode 100644 auth.go create mode 100644 auth_test.go diff --git a/CLAUDE.md b/CLAUDE.md index e94ebea..33a45bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,12 +17,239 @@ The OGS Notifications Server is a Go application that monitors Online-Go.com gam 5. **Send notification**: Consolidated message for all qualifying games 6. **Update timestamp**: Set `last_notification_time` to current unix timestamp +## API Key Authentication (NEW!) + +All protected endpoints now require API key authentication following Apple's security best practices. + +### Obtaining an API Key +``` +POST /generate-api-key +Content-Type: application/json + +{ + "user_id": "your_ogs_user_id", + "master_key": "master_key_from_server_admin", + "description": "iOS App - iPhone 15 Pro" +} +``` + +**Response:** +```json +{ + "api_key": "64-character-hex-api-key", + "user_id": "1783478", + "created_at": "2025-09-22T14:30:00Z", + "description": "iOS App - iPhone 15 Pro" +} +``` + +### Using the API Key + +All protected endpoints require the API key in the `X-API-Key` header: +``` +X-API-Key: your-64-character-api-key +``` + +### iOS Implementation Guidelines + +#### Secure Storage with Keychain + +**IMPORTANT**: Never store API keys in UserDefaults, plist files, or hardcode them. Use iOS Keychain for secure storage. + +```swift +import Security + +class APIKeyManager { + static let shared = APIKeyManager() + private let keychainKey = "com.ogs.notifications.apikey" + private let keychainAccessGroup = "your.app.group" // Optional for app groups + + // Save API key to Keychain + func saveAPIKey(_ apiKey: String) -> Bool { + guard let data = apiKey.data(using: .utf8) else { return false } + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keychainKey, + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + ] + + // Delete any existing key + SecItemDelete(query as CFDictionary) + + // Add new key + let status = SecItemAdd(query as CFDictionary, nil) + return status == errSecSuccess + } + + // Retrieve API key from Keychain + func getAPIKey() -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keychainKey, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var dataTypeRef: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) + + guard status == errSecSuccess, + let data = dataTypeRef as? Data, + let apiKey = String(data: data, encoding: .utf8) else { + return nil + } + + return apiKey + } + + // Delete API key from Keychain + func deleteAPIKey() -> Bool { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keychainKey + ] + + let status = SecItemDelete(query as CFDictionary) + return status == errSecSuccess || status == errSecItemNotFound + } +} +``` + +#### Network Requests with Authentication + +```swift +class OGSNotificationService { + private let baseURL = "https://your-server.com" + + func makeAuthenticatedRequest(endpoint: String, method: String = "GET", body: Data? = nil) async throws -> Data { + guard let apiKey = APIKeyManager.shared.getAPIKey() else { + throw APIError.noAPIKey + } + + guard let url = URL(string: "\(baseURL)\(endpoint)") else { + throw APIError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = method + request.setValue(apiKey, forHTTPHeaderField: "X-API-Key") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let body = body { + request.httpBody = body + } + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse + } + + switch httpResponse.statusCode { + case 200...299: + return data + case 401: + throw APIError.unauthorized + case 403: + throw APIError.forbidden + default: + throw APIError.serverError(httpResponse.statusCode) + } + } + + // Example: Register device + func registerDevice(userID: String, deviceToken: String) async throws { + let payload = [ + "user_id": userID, + "device_token": deviceToken + ] + + let jsonData = try JSONSerialization.data(withJSONObject: payload) + _ = try await makeAuthenticatedRequest(endpoint: "/register", method: "POST", body: jsonData) + } + + // Example: Get diagnostics + func getDiagnostics(for userID: String) async throws -> UserDiagnostics { + let data = try await makeAuthenticatedRequest(endpoint: "/diagnostics/\(userID)") + return try JSONDecoder().decode(UserDiagnostics.self, from: data) + } +} + +enum APIError: LocalizedError { + case noAPIKey + case invalidURL + case invalidResponse + case unauthorized + case forbidden + case serverError(Int) + + var errorDescription: String? { + switch self { + case .noAPIKey: + return "No API key found. Please log in again." + case .unauthorized: + return "Invalid API key. Please log in again." + case .forbidden: + return "Access forbidden. Please check your permissions." + case .serverError(let code): + return "Server error (\(code)). Please try again." + default: + return "An error occurred. Please try again." + } + } +} +``` + +#### Initial Setup Flow + +1. **First Launch**: Check Keychain for existing API key +2. **No API Key Found**: Show login/setup screen +3. **User Provides Credentials**: + - For new users: Admin generates API key via `/generate-api-key` endpoint + - Provide API key to user through secure channel +4. **Store API Key**: Save to Keychain using `APIKeyManager` +5. **Register Device**: Call `/register` endpoint with API key +6. **Begin Monitoring**: Server starts checking for turns + +#### Security Best Practices + +1. **Never expose the master key**: Keep it secure on the server +2. **Use HTTPS only**: Never send API keys over unencrypted connections +3. **Implement key rotation**: Allow users to regenerate keys if compromised +4. **Add biometric protection**: Use Face ID/Touch ID for additional security +5. **Clear on logout**: Delete API key from Keychain when user logs out +6. **Handle 401 errors**: Prompt re-authentication on unauthorized responses + +```swift +// Biometric protection example +import LocalAuthentication + +func authenticateWithBiometrics(completion: @escaping (Bool) -> Void) { + let context = LAContext() + var error: NSError? + + if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, + localizedReason: "Authenticate to access OGS notifications") { success, _ in + DispatchQueue.main.async { + completion(success) + } + } + } else { + completion(false) + } +} +``` + ## Server Endpoints for iOS Integration -### Device Registration +### Device Registration (Protected) ``` POST /register Content-Type: application/json +X-API-Key: your-api-key { "user_id": "your_ogs_user_id", @@ -34,24 +261,27 @@ Content-Type: application/json - **User ID**: Can be obtained from OGS profile URL (e.g., `https://online-go.com/user/view/1783478`) - **Server Behavior**: Stores device token and begins monitoring this user every 30 seconds -### Manual Check (Optional) +### Manual Check (Protected) ``` GET /check/:user_id +X-API-Key: your-api-key ``` - **Purpose**: Manually trigger turn checking (primarily for testing) - **Note**: Not needed in normal operation since server auto-checks every 30 seconds -### User Diagnostics +### User Diagnostics (Protected) ``` GET /diagnostics/:user_id +X-API-Key: your-api-key ``` - **Purpose**: Get comprehensive diagnostics for debugging and user information display - **iOS Implementation**: Call this to show user status, game information, and server health - **Returns**: JSON with user status, monitored games, last notification time, and server info -### Find Users by Device Token (New!) +### Find Users by Device Token (Protected) ``` GET /users-by-token/:device_token +X-API-Key: your-api-key ``` - **Purpose**: Find all user IDs registered to a specific device token - **iOS Implementation**: Call this on app launch to discover which OGS users are monitored on this device diff --git a/README.md b/README.md index 9755720..7f781a2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A Go server that monitors [Online-Go.com](https://online-go.com) games and sends - **Deep Linking**: Notifications include both web URLs and iOS app URLs for seamless game access - **Smart Notifications**: Combines multiple new turns into a single notification to avoid spam - **Persistent Tracking**: Remembers which moves you've already been notified about +- **API Key Authentication**: Secure API key-based authentication for all endpoints ## Quick Start @@ -42,6 +43,7 @@ A Go server that monitors [Online-Go.com](https://online-go.com) games and sends - `APNS_DEVELOPMENT`: Set to `true` for development, `false` for production - `CHECK_INTERVAL_MINUTES`: How often to check for new turns (default: 3) - `ENVIRONMENT`: Deployment environment name (optional, defaults to "none") + - `MASTER_API_KEY`: Master key for generating API keys (auto-generated if not set) ### Running the Server @@ -56,13 +58,47 @@ The server will: - Begin checking all registered users every few minutes - Send push notifications automatically when new turns are detected +## API Authentication + +All protected endpoints require API key authentication using the `X-API-Key` header. + +### Generate API Key + +To generate an API key for a user: + +```bash +POST /generate-api-key +Content-Type: application/json + +{ + "user_id": "your_ogs_user_id", + "master_key": "master_key_from_server_logs", + "description": "iOS App - Device Name" +} +``` + +Response: +```json +{ + "api_key": "64-character-hex-api-key", + "user_id": "1783478", + "created_at": "2025-09-22T14:30:00Z", + "description": "iOS App - Device Name" +} +``` + +**Note**: The master key is logged when the server starts. Set `MASTER_API_KEY` environment variable to use a persistent key. + ## API Endpoints -### Register a Device Token +All endpoints below require the `X-API-Key` header for authentication. + +### Register a Device Token (Protected) ```bash POST /register Content-Type: application/json +X-API-Key: your-api-key { "user_id": "your_ogs_user_id", @@ -70,26 +106,29 @@ Content-Type: application/json } ``` -### Manual Turn Check (Optional) +### Manual Turn Check (Protected) ```bash GET /check/:user_id +X-API-Key: your-api-key ``` Returns JSON with current game status and sends notifications if needed. -### User Diagnostics +### User Diagnostics (Protected) ```bash GET /diagnostics/:user_id +X-API-Key: your-api-key ``` Returns comprehensive user status including device registration, monitored games, and last notification time. -### Find Users by Device Token +### Find Users by Device Token (Protected) ```bash GET /users-by-token/:device_token +X-API-Key: your-api-key ``` Returns all user IDs that are registered to a specific device token. Useful for iOS apps to discover which OGS users are monitored on the current device. diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..b97415b --- /dev/null +++ b/auth.go @@ -0,0 +1,228 @@ +package main + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "sync" + "time" +) + +type APIKey struct { + Key string `json:"key"` + UserID string `json:"user_id"` + CreatedAt time.Time `json:"created_at"` + LastUsed time.Time `json:"last_used"` + Description string `json:"description"` +} + +type APIKeyStorage struct { + mu sync.RWMutex + keys map[string]*APIKey // key -> APIKey + userKeys map[string]string // userID -> key (one key per user for simplicity) +} + +var apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), +} + +// generateAPIKey creates a cryptographically secure random API key +func generateAPIKey() (string, error) { + bytes := make([]byte, 32) // 256 bits of entropy + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("failed to generate random key: %v", err) + } + return hex.EncodeToString(bytes), nil +} + +// createAPIKey generates a new API key for a user +func createAPIKey(userID string, description string) (*APIKey, error) { + key, err := generateAPIKey() + if err != nil { + return nil, err + } + + apiKey := &APIKey{ + Key: key, + UserID: userID, + CreatedAt: time.Now(), + LastUsed: time.Now(), + Description: description, + } + + apiKeyStorage.mu.Lock() + defer apiKeyStorage.mu.Unlock() + + // Revoke existing key if one exists + if existingKey, exists := apiKeyStorage.userKeys[userID]; exists { + delete(apiKeyStorage.keys, existingKey) + } + + apiKeyStorage.keys[key] = apiKey + apiKeyStorage.userKeys[userID] = key + + saveAPIKeys() + + log.Printf("Created new API key for user %s: %s", userID, description) + return apiKey, nil +} + +// validateAPIKey checks if an API key is valid and updates last used time +func validateAPIKey(key string) (*APIKey, bool) { + apiKeyStorage.mu.Lock() + defer apiKeyStorage.mu.Unlock() + + apiKey, exists := apiKeyStorage.keys[key] + if !exists { + return nil, false + } + + // Update last used time + apiKey.LastUsed = time.Now() + saveAPIKeys() + + return apiKey, true +} + +// requireAPIKey is middleware that validates API key for protected endpoints +func requireAPIKey(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + log.Printf("Request to %s from %s without API key", r.URL.Path, r.RemoteAddr) + http.Error(w, "API key required", http.StatusUnauthorized) + return + } + + key, valid := validateAPIKey(apiKey) + if !valid { + log.Printf("Invalid API key attempt for %s from %s", r.URL.Path, r.RemoteAddr) + http.Error(w, "Invalid API key", http.StatusUnauthorized) + return + } + + log.Printf("Authenticated request to %s from user %s", r.URL.Path, key.UserID) + + // Add user ID to request context for use in handlers + r.Header.Set("X-User-ID", key.UserID) + + next(w, r) + } +} + +// generateAPIKeyHandler creates a new API key for a user +func generateAPIKeyHandler(w http.ResponseWriter, r *http.Request) { + var request struct { + UserID string `json:"user_id"` + MasterKey string `json:"master_key"` + Description string `json:"description"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Validate master key for API key generation + masterKey := os.Getenv("MASTER_API_KEY") + if masterKey == "" { + // Generate and log a master key if not set + newMasterKey, _ := generateAPIKey() + log.Printf("WARNING: No MASTER_API_KEY set. Generated temporary key: %s", newMasterKey) + log.Printf("Set this as MASTER_API_KEY environment variable to persist it") + masterKey = newMasterKey + os.Setenv("MASTER_API_KEY", masterKey) + } + + // Use constant-time comparison to prevent timing attacks + if subtle.ConstantTimeCompare([]byte(request.MasterKey), []byte(masterKey)) != 1 { + log.Printf("Invalid master key attempt from %s", r.RemoteAddr) + http.Error(w, "Invalid master key", http.StatusUnauthorized) + return + } + + if request.UserID == "" { + http.Error(w, "user_id is required", http.StatusBadRequest) + return + } + + if request.Description == "" { + request.Description = "iOS App API Key" + } + + apiKey, err := createAPIKey(request.UserID, request.Description) + if err != nil { + log.Printf("Failed to create API key: %v", err) + http.Error(w, "Failed to create API key", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "api_key": apiKey.Key, + "user_id": apiKey.UserID, + "created_at": apiKey.CreatedAt, + "description": apiKey.Description, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// loadAPIKeys loads API keys from storage +func loadAPIKeys() { + apiKeyStorage.mu.Lock() + defer apiKeyStorage.mu.Unlock() + + data, err := os.ReadFile("api_keys.json") + if err != nil { + log.Println("No existing api_keys.json file, starting fresh") + return + } + + var keys []*APIKey + if err := json.Unmarshal(data, &keys); err != nil { + log.Printf("Error loading api_keys.json: %v", err) + return + } + + for _, key := range keys { + apiKeyStorage.keys[key.Key] = key + apiKeyStorage.userKeys[key.UserID] = key.Key + } + + log.Printf("Loaded %d API keys", len(keys)) +} + +// saveAPIKeys saves API keys to storage +func saveAPIKeys() { + // Convert map to slice for JSON storage + var keys []*APIKey + for _, key := range apiKeyStorage.keys { + keys = append(keys, key) + } + + data, err := json.MarshalIndent(keys, "", " ") + if err != nil { + log.Printf("Error marshaling API keys: %v", err) + return + } + + if err := os.WriteFile("api_keys.json", data, 0600); err != nil { + log.Printf("Error saving api_keys.json: %v", err) + } +} + +// getUserAPIKey returns the API key for a user (for diagnostics) +func getUserAPIKey(userID string) (string, bool) { + apiKeyStorage.mu.RLock() + defer apiKeyStorage.mu.RUnlock() + + key, exists := apiKeyStorage.userKeys[userID] + return key, exists +} \ No newline at end of file diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..1ce8645 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,210 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestGenerateAPIKey(t *testing.T) { + // Test API key generation + key, err := generateAPIKey() + if err != nil { + t.Fatalf("Failed to generate API key: %v", err) + } + + // Check key length (32 bytes = 64 hex characters) + if len(key) != 64 { + t.Errorf("Expected API key length 64, got %d", len(key)) + } + + // Check that keys are unique + key2, err := generateAPIKey() + if err != nil { + t.Fatalf("Failed to generate second API key: %v", err) + } + + if key == key2 { + t.Error("Generated API keys should be unique") + } +} + +func TestCreateAndValidateAPIKey(t *testing.T) { + // Clear storage for test + apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), + } + + // Create API key + userID := "testuser123" + description := "Test API Key" + apiKey, err := createAPIKey(userID, description) + if err != nil { + t.Fatalf("Failed to create API key: %v", err) + } + + // Validate the created key + validatedKey, valid := validateAPIKey(apiKey.Key) + if !valid { + t.Error("API key should be valid") + } + + if validatedKey.UserID != userID { + t.Errorf("Expected user ID %s, got %s", userID, validatedKey.UserID) + } + + // Test invalid key + _, valid = validateAPIKey("invalid-key") + if valid { + t.Error("Invalid API key should not validate") + } + + // Clean up + os.Remove("api_keys.json") +} + +func TestRequireAPIKeyMiddleware(t *testing.T) { + // Clear storage for test + apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), + } + + // Create test API key + userID := "testuser456" + apiKey, _ := createAPIKey(userID, "Test Key") + + // Create a test handler + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Success")) + }) + + // Wrap with middleware + protectedHandler := requireAPIKey(testHandler) + + // Test without API key + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + protectedHandler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("Expected status 401, got %d", w.Code) + } + + // Test with invalid API key + req = httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-API-Key", "invalid-key") + w = httptest.NewRecorder() + protectedHandler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("Expected status 401 for invalid key, got %d", w.Code) + } + + // Test with valid API key + req = httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-API-Key", apiKey.Key) + w = httptest.NewRecorder() + protectedHandler(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 for valid key, got %d", w.Code) + } + + // Clean up + os.Remove("api_keys.json") +} + +func TestGenerateAPIKeyHandler(t *testing.T) { + // Set master key for testing + testMasterKey := "test-master-key-123" + os.Setenv("MASTER_API_KEY", testMasterKey) + defer os.Unsetenv("MASTER_API_KEY") + + // Clear storage for test + apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), + } + + // Test with invalid master key + payload := map[string]string{ + "user_id": "789", + "master_key": "wrong-key", + "description": "Test", + } + jsonPayload, _ := json.Marshal(payload) + req := httptest.NewRequest("POST", "/generate-api-key", bytes.NewBuffer(jsonPayload)) + w := httptest.NewRecorder() + generateAPIKeyHandler(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("Expected status 401 for invalid master key, got %d", w.Code) + } + + // Test with valid master key + payload["master_key"] = testMasterKey + jsonPayload, _ = json.Marshal(payload) + req = httptest.NewRequest("POST", "/generate-api-key", bytes.NewBuffer(jsonPayload)) + w = httptest.NewRecorder() + generateAPIKeyHandler(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 for valid request, got %d", w.Code) + } + + // Check response + var response map[string]interface{} + json.NewDecoder(w.Body).Decode(&response) + + if response["user_id"] != "789" { + t.Errorf("Expected user_id 789, got %v", response["user_id"]) + } + + if response["api_key"] == nil || response["api_key"] == "" { + t.Error("Response should contain api_key") + } + + // Clean up + os.Remove("api_keys.json") +} + +func TestLoadAndSaveAPIKeys(t *testing.T) { + // Clear storage for test + apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), + } + + // Create some test keys + key1, _ := createAPIKey("user1", "Key 1") + key2, _ := createAPIKey("user2", "Key 2") + + // Save keys + saveAPIKeys() + + // Clear storage + apiKeyStorage = &APIKeyStorage{ + keys: make(map[string]*APIKey), + userKeys: make(map[string]string), + } + + // Load keys + loadAPIKeys() + + // Validate loaded keys + _, valid1 := validateAPIKey(key1.Key) + _, valid2 := validateAPIKey(key2.Key) + + if !valid1 || !valid2 { + t.Error("Keys should be valid after loading") + } + + // Clean up + os.Remove("api_keys.json") +} \ No newline at end of file diff --git a/main.go b/main.go index 3ac0554..9452aa2 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,7 @@ var apnsClient *apns2.Client func main() { loadStorage() + loadAPIKeys() initAPNS() // Start periodic checking in background @@ -107,14 +108,29 @@ func main() { r := mux.NewRouter() - r.HandleFunc("/check/{userID}", checkUserTurn).Methods("GET") - r.HandleFunc("/register", registerDevice).Methods("POST") - r.HandleFunc("/users-by-token/{deviceToken}", getUsersByDeviceToken).Methods("GET") + // Public endpoints (no authentication required) r.HandleFunc("/health", healthCheck).Methods("GET") - r.HandleFunc("/diagnostics/{userID}", getUserDiagnostics).Methods("GET") + r.HandleFunc("/generate-api-key", generateAPIKeyHandler).Methods("POST") + + // Protected endpoints (require API key) + r.HandleFunc("/check/{userID}", requireAPIKey(checkUserTurn)).Methods("GET") + r.HandleFunc("/register", requireAPIKey(registerDevice)).Methods("POST") + r.HandleFunc("/users-by-token/{deviceToken}", requireAPIKey(getUsersByDeviceToken)).Methods("GET") + r.HandleFunc("/diagnostics/{userID}", requireAPIKey(getUserDiagnostics)).Methods("GET") log.Println("Server starting on :8080") log.Println("Automatic turn checking enabled") + + // Log master key info on startup + masterKey := os.Getenv("MASTER_API_KEY") + if masterKey == "" { + newMasterKey, _ := generateAPIKey() + log.Printf("WARNING: No MASTER_API_KEY set. Generated temporary key: %s", newMasterKey) + log.Printf("Set this as MASTER_API_KEY environment variable to persist it") + os.Setenv("MASTER_API_KEY", newMasterKey) + } else { + log.Println("Master API key is configured") + } log.Fatal(http.ListenAndServe(":8080", r)) }