package engine import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "time" "golang.org/x/crypto/bcrypt" ) // APIKey represents an API key in the database type APIKey struct { ID int `json:"id"` Name string `json:"name"` UserID int `json:"user_id"` CreatedAt time.Time `json:"created_at"` LastUsed *time.Time `json:"last_used,omitempty"` Revoked bool `json:"revoked"` } // MarshalJSON emits APIKey with unix timestamps. func (k APIKey) MarshalJSON() ([]byte, error) { type keyJSON struct { ID int `json:"id"` Name string `json:"name"` UserID int `json:"user_id"` CreatedAt int64 `json:"created_at"` LastUsed *int64 `json:"last_used,omitempty"` Revoked bool `json:"revoked"` } var lastUsed *int64 if k.LastUsed != nil { v := k.LastUsed.Unix() lastUsed = &v } return json.Marshal(keyJSON{ ID: k.ID, Name: k.Name, UserID: k.UserID, CreatedAt: k.CreatedAt.Unix(), LastUsed: lastUsed, Revoked: k.Revoked, }) } // UnmarshalJSON parses APIKey from JSON with unix timestamps. func (k *APIKey) UnmarshalJSON(data []byte) error { type keyJSON struct { ID int `json:"id"` Name string `json:"name"` UserID int `json:"user_id"` CreatedAt int64 `json:"created_at"` LastUsed *int64 `json:"last_used,omitempty"` Revoked bool `json:"revoked"` } var raw keyJSON if err := json.Unmarshal(data, &raw); err != nil { return err } k.ID = raw.ID k.Name = raw.Name k.UserID = raw.UserID k.CreatedAt = time.Unix(raw.CreatedAt, 0) k.Revoked = raw.Revoked if raw.LastUsed != nil { t := time.Unix(*raw.LastUsed, 0) k.LastUsed = &t } return nil } // GenerateAPIKey creates a new API key for the given name func GenerateAPIKey(name string) (string, error) { db := GetDB() if db == nil { return "", fmt.Errorf("database not initialized") } // Generate random key: oak_ + 32 random bytes (base64 encoded) keyBytes := make([]byte, 32) if _, err := rand.Read(keyBytes); err != nil { return "", fmt.Errorf("failed to generate random key: %w", err) } key := "oak_" + base64.URLEncoding.EncodeToString(keyBytes) // Hash the key for storage hashedKey, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash key: %w", err) } // Store in database (user_id defaults to 1 for shared user) _, err = db.Exec(` INSERT INTO api_keys (key, name, user_id, created_at) VALUES (?, ?, 1, ?) `, string(hashedKey), name, timeNow().Unix()) if err != nil { return "", fmt.Errorf("failed to store API key: %w", err) } return key, nil } // ValidateAPIKey checks if an API key is valid and updates last_used timestamp func ValidateAPIKey(key string) (bool, int, error) { db := GetDB() if db == nil { return false, 0, fmt.Errorf("database not initialized") } // Get all non-revoked keys rows, err := db.Query(` SELECT id, key, user_id, revoked FROM api_keys WHERE revoked = 0 `) if err != nil { return false, 0, fmt.Errorf("failed to query API keys: %w", err) } defer rows.Close() // Check each key (bcrypt comparison) for rows.Next() { var id, userID int var hashedKey string var revoked bool if err := rows.Scan(&id, &hashedKey, &userID, &revoked); err != nil { continue } // Compare with bcrypt if err := bcrypt.CompareHashAndPassword([]byte(hashedKey), []byte(key)); err == nil { // Valid key found - update last_used now := timeNow().Unix() _, _ = db.Exec("UPDATE api_keys SET last_used = ? WHERE id = ?", now, id) return true, userID, nil } } return false, 0, nil } // ListAPIKeys returns all API keys for a user (without the actual key value) func ListAPIKeys(userID int) ([]*APIKey, error) { db := GetDB() if db == nil { return nil, fmt.Errorf("database not initialized") } rows, err := db.Query(` SELECT id, name, user_id, created_at, last_used, revoked FROM api_keys WHERE user_id = ? ORDER BY created_at DESC `, userID) if err != nil { return nil, fmt.Errorf("failed to query API keys: %w", err) } defer rows.Close() var keys []*APIKey for rows.Next() { key := &APIKey{} var createdAt, lastUsed int64 var lastUsedNull *int64 err := rows.Scan(&key.ID, &key.Name, &key.UserID, &createdAt, &lastUsedNull, &key.Revoked) if err != nil { return nil, fmt.Errorf("failed to scan API key: %w", err) } key.CreatedAt = time.Unix(createdAt, 0) if lastUsedNull != nil { lastUsed = *lastUsedNull t := time.Unix(lastUsed, 0) key.LastUsed = &t } keys = append(keys, key) } return keys, nil } // RevokeAPIKey marks an API key as revoked func RevokeAPIKey(keyID int) error { db := GetDB() if db == nil { return fmt.Errorf("database not initialized") } result, err := db.Exec("UPDATE api_keys SET revoked = 1 WHERE id = ?", keyID) if err != nil { return fmt.Errorf("failed to revoke API key: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return fmt.Errorf("API key not found") } return nil }