web_api_store.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. package state
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "time"
  9. )
  10. var (
  11. // ErrDupAPIKey is returned when attempting to insert a duplicate API key.
  12. ErrDupAPIKey = errors.New("API key already exists")
  13. // ErrNoAPIKey is returned when an API key is not found.
  14. ErrNoAPIKey = errors.New("API key not found")
  15. )
  16. // WebAPIKey represents a Web API authentication key.
  17. type WebAPIKey struct {
  18. DevID string `json:"dev_id"`
  19. DevKey string `json:"dev_key"`
  20. AppName string `json:"app_name"`
  21. CreatedAt time.Time `json:"created_at"`
  22. LastUsed *time.Time `json:"last_used,omitempty"`
  23. IsActive bool `json:"is_active"`
  24. RateLimit int `json:"rate_limit"`
  25. AllowedOrigins []string `json:"allowed_origins"`
  26. Capabilities []string `json:"capabilities"`
  27. }
  28. // WebAPIKeyUpdate represents fields that can be updated for an API key.
  29. type WebAPIKeyUpdate struct {
  30. AppName *string `json:"app_name,omitempty"`
  31. IsActive *bool `json:"is_active,omitempty"`
  32. RateLimit *int `json:"rate_limit,omitempty"`
  33. AllowedOrigins *[]string `json:"allowed_origins,omitempty"`
  34. Capabilities *[]string `json:"capabilities,omitempty"`
  35. }
  36. // CreateAPIKey inserts a new API key into the database.
  37. func (f SQLiteUserStore) CreateAPIKey(ctx context.Context, key WebAPIKey) error {
  38. originsJSON, err := json.Marshal(key.AllowedOrigins)
  39. if err != nil {
  40. return fmt.Errorf("failed to marshal allowed origins: %w", err)
  41. }
  42. capabilitiesJSON, err := json.Marshal(key.Capabilities)
  43. if err != nil {
  44. return fmt.Errorf("failed to marshal capabilities: %w", err)
  45. }
  46. q := `
  47. INSERT INTO web_api_keys (dev_id, dev_key, app_name, created_at, is_active, rate_limit, allowed_origins, capabilities)
  48. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  49. ON CONFLICT (dev_id) DO NOTHING
  50. `
  51. result, err := f.db.ExecContext(ctx,
  52. q,
  53. key.DevID,
  54. key.DevKey,
  55. key.AppName,
  56. key.CreatedAt.Unix(),
  57. key.IsActive,
  58. key.RateLimit,
  59. string(originsJSON),
  60. string(capabilitiesJSON),
  61. )
  62. if err != nil {
  63. return err
  64. }
  65. rowsAffected, err := result.RowsAffected()
  66. if err != nil {
  67. return err
  68. }
  69. if rowsAffected == 0 {
  70. return ErrDupAPIKey
  71. }
  72. return nil
  73. }
  74. // GetAPIKeyByDevKey retrieves an API key by its dev_key value.
  75. func (f *SQLiteUserStore) GetAPIKeyByDevKey(ctx context.Context, devKey string) (*WebAPIKey, error) {
  76. q := `
  77. SELECT dev_id, dev_key, app_name, created_at, last_used, is_active, rate_limit, allowed_origins, capabilities
  78. FROM web_api_keys
  79. WHERE dev_key = ? AND is_active = 1
  80. `
  81. var key WebAPIKey
  82. var createdAt, lastUsed sql.NullInt64
  83. var originsJSON, capabilitiesJSON string
  84. err := f.db.QueryRowContext(ctx, q, devKey).Scan(
  85. &key.DevID,
  86. &key.DevKey,
  87. &key.AppName,
  88. &createdAt,
  89. &lastUsed,
  90. &key.IsActive,
  91. &key.RateLimit,
  92. &originsJSON,
  93. &capabilitiesJSON,
  94. )
  95. if err == sql.ErrNoRows {
  96. return nil, ErrNoAPIKey
  97. }
  98. if err != nil {
  99. return nil, err
  100. }
  101. key.CreatedAt = time.Unix(createdAt.Int64, 0)
  102. if lastUsed.Valid {
  103. t := time.Unix(lastUsed.Int64, 0)
  104. key.LastUsed = &t
  105. }
  106. if err := json.Unmarshal([]byte(originsJSON), &key.AllowedOrigins); err != nil {
  107. return nil, fmt.Errorf("failed to unmarshal allowed origins: %w", err)
  108. }
  109. if err := json.Unmarshal([]byte(capabilitiesJSON), &key.Capabilities); err != nil {
  110. return nil, fmt.Errorf("failed to unmarshal capabilities: %w", err)
  111. }
  112. return &key, nil
  113. }
  114. // GetAPIKeyByDevID retrieves an API key by its dev_id value.
  115. func (f SQLiteUserStore) GetAPIKeyByDevID(ctx context.Context, devID string) (*WebAPIKey, error) {
  116. q := `
  117. SELECT dev_id, dev_key, app_name, created_at, last_used, is_active, rate_limit, allowed_origins, capabilities
  118. FROM web_api_keys
  119. WHERE dev_id = ?
  120. `
  121. var key WebAPIKey
  122. var createdAt, lastUsed sql.NullInt64
  123. var originsJSON, capabilitiesJSON string
  124. err := f.db.QueryRowContext(ctx, q, devID).Scan(
  125. &key.DevID,
  126. &key.DevKey,
  127. &key.AppName,
  128. &createdAt,
  129. &lastUsed,
  130. &key.IsActive,
  131. &key.RateLimit,
  132. &originsJSON,
  133. &capabilitiesJSON,
  134. )
  135. if err == sql.ErrNoRows {
  136. return nil, ErrNoAPIKey
  137. }
  138. if err != nil {
  139. return nil, err
  140. }
  141. key.CreatedAt = time.Unix(createdAt.Int64, 0)
  142. if lastUsed.Valid {
  143. t := time.Unix(lastUsed.Int64, 0)
  144. key.LastUsed = &t
  145. }
  146. if err := json.Unmarshal([]byte(originsJSON), &key.AllowedOrigins); err != nil {
  147. return nil, fmt.Errorf("failed to unmarshal allowed origins: %w", err)
  148. }
  149. if err := json.Unmarshal([]byte(capabilitiesJSON), &key.Capabilities); err != nil {
  150. return nil, fmt.Errorf("failed to unmarshal capabilities: %w", err)
  151. }
  152. return &key, nil
  153. }
  154. // ListAPIKeys retrieves all API keys from the database.
  155. func (f SQLiteUserStore) ListAPIKeys(ctx context.Context) ([]WebAPIKey, error) {
  156. q := `
  157. SELECT dev_id, dev_key, app_name, created_at, last_used, is_active, rate_limit, allowed_origins, capabilities
  158. FROM web_api_keys
  159. ORDER BY created_at DESC
  160. `
  161. rows, err := f.db.QueryContext(ctx, q)
  162. if err != nil {
  163. return nil, err
  164. }
  165. defer rows.Close()
  166. var keys []WebAPIKey
  167. for rows.Next() {
  168. var key WebAPIKey
  169. var createdAt, lastUsed sql.NullInt64
  170. var originsJSON, capabilitiesJSON string
  171. err := rows.Scan(
  172. &key.DevID,
  173. &key.DevKey,
  174. &key.AppName,
  175. &createdAt,
  176. &lastUsed,
  177. &key.IsActive,
  178. &key.RateLimit,
  179. &originsJSON,
  180. &capabilitiesJSON,
  181. )
  182. if err != nil {
  183. return nil, err
  184. }
  185. key.CreatedAt = time.Unix(createdAt.Int64, 0)
  186. if lastUsed.Valid {
  187. t := time.Unix(lastUsed.Int64, 0)
  188. key.LastUsed = &t
  189. }
  190. if err := json.Unmarshal([]byte(originsJSON), &key.AllowedOrigins); err != nil {
  191. return nil, fmt.Errorf("failed to unmarshal allowed origins: %w", err)
  192. }
  193. if err := json.Unmarshal([]byte(capabilitiesJSON), &key.Capabilities); err != nil {
  194. return nil, fmt.Errorf("failed to unmarshal capabilities: %w", err)
  195. }
  196. keys = append(keys, key)
  197. }
  198. if err = rows.Err(); err != nil {
  199. return nil, err
  200. }
  201. return keys, nil
  202. }
  203. // UpdateAPIKey updates an existing API key's fields.
  204. func (f SQLiteUserStore) UpdateAPIKey(ctx context.Context, devID string, updates WebAPIKeyUpdate) error {
  205. // Build dynamic UPDATE query based on provided fields
  206. var setClauses []string
  207. var args []interface{}
  208. if updates.AppName != nil {
  209. setClauses = append(setClauses, "app_name = ?")
  210. args = append(args, *updates.AppName)
  211. }
  212. if updates.IsActive != nil {
  213. setClauses = append(setClauses, "is_active = ?")
  214. args = append(args, *updates.IsActive)
  215. }
  216. if updates.RateLimit != nil {
  217. setClauses = append(setClauses, "rate_limit = ?")
  218. args = append(args, *updates.RateLimit)
  219. }
  220. if updates.AllowedOrigins != nil {
  221. originsJSON, err := json.Marshal(*updates.AllowedOrigins)
  222. if err != nil {
  223. return fmt.Errorf("failed to marshal allowed origins: %w", err)
  224. }
  225. setClauses = append(setClauses, "allowed_origins = ?")
  226. args = append(args, string(originsJSON))
  227. }
  228. if updates.Capabilities != nil {
  229. capabilitiesJSON, err := json.Marshal(*updates.Capabilities)
  230. if err != nil {
  231. return fmt.Errorf("failed to marshal capabilities: %w", err)
  232. }
  233. setClauses = append(setClauses, "capabilities = ?")
  234. args = append(args, string(capabilitiesJSON))
  235. }
  236. if len(setClauses) == 0 {
  237. return nil // No updates to perform
  238. }
  239. // Add WHERE clause argument
  240. args = append(args, devID)
  241. q := fmt.Sprintf(`
  242. UPDATE web_api_keys
  243. SET %s
  244. WHERE dev_id = ?
  245. `, joinStrings(setClauses, ", "))
  246. result, err := f.db.ExecContext(ctx, q, args...)
  247. if err != nil {
  248. return err
  249. }
  250. rowsAffected, err := result.RowsAffected()
  251. if err != nil {
  252. return err
  253. }
  254. if rowsAffected == 0 {
  255. return ErrNoAPIKey
  256. }
  257. return nil
  258. }
  259. // DeleteAPIKey removes an API key from the database.
  260. func (f SQLiteUserStore) DeleteAPIKey(ctx context.Context, devID string) error {
  261. q := `
  262. DELETE FROM web_api_keys WHERE dev_id = ?
  263. `
  264. result, err := f.db.ExecContext(ctx, q, devID)
  265. if err != nil {
  266. return err
  267. }
  268. rowsAffected, err := result.RowsAffected()
  269. if err != nil {
  270. return err
  271. }
  272. if rowsAffected == 0 {
  273. return ErrNoAPIKey
  274. }
  275. return nil
  276. }
  277. // UpdateLastUsed updates the last_used timestamp for an API key.
  278. func (f *SQLiteUserStore) UpdateLastUsed(ctx context.Context, devKey string) error {
  279. q := `
  280. UPDATE web_api_keys
  281. SET last_used = ?
  282. WHERE dev_key = ?
  283. `
  284. _, err := f.db.ExecContext(ctx, q, time.Now().Unix(), devKey)
  285. return err
  286. }
  287. // joinStrings is a helper function to join strings with a separator.
  288. func joinStrings(strs []string, sep string) string {
  289. if len(strs) == 0 {
  290. return ""
  291. }
  292. result := strs[0]
  293. for i := 1; i < len(strs); i++ {
  294. result += sep + strs[i]
  295. }
  296. return result
  297. }