api_key_repo.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. package repository
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "database/sql"
  6. "encoding/hex"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "log/slog"
  11. "strings"
  12. "time"
  13. dbent "github.com/Wei-Shaw/sub2api/ent"
  14. "github.com/Wei-Shaw/sub2api/ent/apikey"
  15. "github.com/Wei-Shaw/sub2api/ent/group"
  16. "github.com/Wei-Shaw/sub2api/ent/schema/mixins"
  17. "github.com/Wei-Shaw/sub2api/ent/user"
  18. "github.com/Wei-Shaw/sub2api/internal/service"
  19. "github.com/lib/pq"
  20. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  21. "entgo.io/ent/dialect"
  22. entsql "entgo.io/ent/dialect/sql"
  23. )
  24. type apiKeyRepository struct {
  25. client *dbent.Client
  26. sql sqlExecutor
  27. }
  28. func NewAPIKeyRepository(client *dbent.Client, sqlDB *sql.DB) service.APIKeyRepository {
  29. return newAPIKeyRepositoryWithSQL(client, sqlDB)
  30. }
  31. func newAPIKeyRepositoryWithSQL(client *dbent.Client, sqlq sqlExecutor) *apiKeyRepository {
  32. return &apiKeyRepository{client: client, sql: sqlq}
  33. }
  34. func (r *apiKeyRepository) activeQuery() *dbent.APIKeyQuery {
  35. // 默认过滤已软删除记录,避免删除后仍被查询到。
  36. return r.client.APIKey.Query().Where(apikey.DeletedAtIsNil())
  37. }
  38. func (r *apiKeyRepository) Create(ctx context.Context, key *service.APIKey) error {
  39. builder := r.client.APIKey.Create().
  40. SetUserID(key.UserID).
  41. SetKey(key.Key).
  42. SetName(key.Name).
  43. SetStatus(key.Status).
  44. SetNillableGroupID(key.GroupID).
  45. SetNillableLastUsedAt(key.LastUsedAt).
  46. SetQuota(key.Quota).
  47. SetQuotaUsed(key.QuotaUsed).
  48. SetNillableExpiresAt(key.ExpiresAt).
  49. SetRateLimit5h(key.RateLimit5h).
  50. SetRateLimit1d(key.RateLimit1d).
  51. SetRateLimit7d(key.RateLimit7d)
  52. if len(key.IPWhitelist) > 0 {
  53. builder.SetIPWhitelist(key.IPWhitelist)
  54. }
  55. if len(key.IPBlacklist) > 0 {
  56. builder.SetIPBlacklist(key.IPBlacklist)
  57. }
  58. created, err := builder.Save(ctx)
  59. if err == nil {
  60. key.ID = created.ID
  61. key.LastUsedAt = created.LastUsedAt
  62. key.CreatedAt = created.CreatedAt
  63. key.UpdatedAt = created.UpdatedAt
  64. }
  65. return translatePersistenceError(err, nil, service.ErrAPIKeyExists)
  66. }
  67. func (r *apiKeyRepository) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
  68. m, err := r.activeQuery().
  69. Where(apikey.IDEQ(id)).
  70. WithUser().
  71. WithGroup().
  72. Only(ctx)
  73. if err != nil {
  74. if dbent.IsNotFound(err) {
  75. return nil, service.ErrAPIKeyNotFound
  76. }
  77. return nil, err
  78. }
  79. return apiKeyEntityToService(m), nil
  80. }
  81. // GetKeyAndOwnerID 根据 API Key ID 获取其 key 与所有者(用户)ID。
  82. // 相比 GetByID,此方法性能更优,因为:
  83. // - 使用 Select() 只查询必要字段,减少数据传输量
  84. // - 不加载完整的 API Key 实体及其关联数据(User、Group 等)
  85. // - 适用于删除等只需 key 与用户 ID 的场景
  86. func (r *apiKeyRepository) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
  87. m, err := r.activeQuery().
  88. Where(apikey.IDEQ(id)).
  89. Select(apikey.FieldKey, apikey.FieldKeyHash, apikey.FieldUserID).
  90. Only(ctx)
  91. if err != nil {
  92. if dbent.IsNotFound(err) {
  93. return "", 0, service.ErrAPIKeyNotFound
  94. }
  95. return "", 0, err
  96. }
  97. return (&service.APIKey{Key: m.Key, KeyHash: m.KeyHash}).AuthCacheInvalidationKey(), m.UserID, nil
  98. }
  99. func (r *apiKeyRepository) GetByKey(ctx context.Context, key string) (*service.APIKey, error) {
  100. m, err := r.activeQuery().
  101. Where(apikey.Or(apikey.KeyEQ(key), apikey.KeyHashEQ(legacyAPIKeyDigest(key)))).
  102. WithUser(func(q *dbent.UserQuery) {
  103. q.WithAllowedGroups(func(gq *dbent.GroupQuery) {
  104. gq.Select(group.FieldID)
  105. })
  106. }).
  107. WithGroup().
  108. Only(ctx)
  109. if err != nil {
  110. if dbent.IsNotFound(err) {
  111. return nil, service.ErrAPIKeyNotFound
  112. }
  113. return nil, err
  114. }
  115. return apiKeyEntityToService(m), nil
  116. }
  117. func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) {
  118. m, err := r.activeQuery().
  119. Where(apikey.Or(apikey.KeyEQ(key), apikey.KeyHashEQ(legacyAPIKeyDigest(key)))).
  120. Select(
  121. apikey.FieldID,
  122. apikey.FieldKeyHash,
  123. apikey.FieldKeyPrefix,
  124. apikey.FieldUserID,
  125. apikey.FieldGroupID,
  126. apikey.FieldName,
  127. apikey.FieldStatus,
  128. apikey.FieldIPWhitelist,
  129. apikey.FieldIPBlacklist,
  130. apikey.FieldQuota,
  131. apikey.FieldQuotaUsed,
  132. apikey.FieldExpiresAt,
  133. apikey.FieldRateLimit5h,
  134. apikey.FieldRateLimit1d,
  135. apikey.FieldRateLimit7d,
  136. ).
  137. WithUser(func(q *dbent.UserQuery) {
  138. q.Select(
  139. user.FieldID,
  140. user.FieldEmail,
  141. user.FieldUsername,
  142. user.FieldStatus,
  143. user.FieldRole,
  144. user.FieldBalance,
  145. user.FieldConcurrency,
  146. user.FieldBalanceNotifyEnabled,
  147. user.FieldRestrictPublicGroups,
  148. user.FieldBalanceNotifyThresholdType,
  149. user.FieldBalanceNotifyThreshold,
  150. user.FieldBalanceNotifyExtraEmails,
  151. user.FieldTotalRecharged,
  152. user.FieldSignupSource,
  153. user.FieldLastLoginAt,
  154. user.FieldLastActiveAt,
  155. user.FieldRpmLimit,
  156. )
  157. q.WithAllowedGroups(func(gq *dbent.GroupQuery) {
  158. gq.Select(group.FieldID)
  159. })
  160. }).
  161. WithGroup(func(q *dbent.GroupQuery) {
  162. q.Select(
  163. group.FieldID,
  164. group.FieldName,
  165. group.FieldPlatform,
  166. group.FieldIsExclusive,
  167. group.FieldSecurityPolicyEnabled,
  168. group.FieldSecurityPolicyMode,
  169. group.FieldSecurityPolicyEmailEnabled,
  170. group.FieldStatus,
  171. group.FieldSubscriptionType,
  172. group.FieldRateMultiplier,
  173. group.FieldDailyLimitUsd,
  174. group.FieldWeeklyLimitUsd,
  175. group.FieldMonthlyLimitUsd,
  176. group.FieldAllowImageGeneration,
  177. group.FieldAllowBatchImageGeneration,
  178. group.FieldImageRateIndependent,
  179. group.FieldImageRateMultiplier,
  180. group.FieldImagePrice1k,
  181. group.FieldImagePrice2k,
  182. group.FieldImagePrice4k,
  183. group.FieldVideoRateIndependent,
  184. group.FieldVideoRateMultiplier,
  185. group.FieldVideoPrice480p,
  186. group.FieldVideoPrice720p,
  187. group.FieldVideoPrice1080p,
  188. group.FieldVideoModelPrices,
  189. group.FieldWebSearchPricePerCall,
  190. group.FieldSearchPricePer1k,
  191. group.FieldAudioRealtimePricePerMin,
  192. group.FieldAudioTtsPricePerMillionChars,
  193. group.FieldAudioSttPricePerHour,
  194. group.FieldLongContextPricingEnabled,
  195. group.FieldModelPricing,
  196. group.FieldClaudeCodeOnly,
  197. group.FieldFallbackGroupID,
  198. group.FieldFallbackGroupIDOnInvalidRequest,
  199. group.FieldModelRoutingEnabled,
  200. group.FieldModelRouting,
  201. group.FieldMcpXMLInject,
  202. group.FieldSupportedModelScopes,
  203. group.FieldAllowMessagesDispatch,
  204. group.FieldAllowLive,
  205. group.FieldForceOpenaiFast,
  206. group.FieldFreeOpenaiFast,
  207. group.FieldDefaultMappedModel,
  208. group.FieldMessagesDispatchModelConfig,
  209. group.FieldModelAllowlist,
  210. group.FieldCodexModelsManifestConfig,
  211. group.FieldRpmLimit,
  212. group.FieldMaxReasoningEffort,
  213. group.FieldMaxReasoningEffortOverLimit,
  214. group.FieldReasoningEffortMappings,
  215. group.FieldPeakRateEnabled,
  216. group.FieldPeakStart,
  217. group.FieldPeakEnd,
  218. group.FieldPeakRateMultiplier,
  219. // 分组利润控制:认证快照是调度门 enable 判定的直接来源,
  220. // 漏选会让门静默失效;新增快照分组字段时必须同步本投影,
  221. // 集成测试对账兜底。
  222. group.FieldProfitControlEnabled,
  223. group.FieldProfitMinMargin,
  224. group.FieldProfitSafetyBuffer,
  225. )
  226. }).
  227. Only(ctx)
  228. if err != nil {
  229. if dbent.IsNotFound(err) {
  230. return nil, service.ErrAPIKeyNotFound
  231. }
  232. return nil, err
  233. }
  234. return apiKeyEntityToService(m), nil
  235. }
  236. func (r *apiKeyRepository) Update(ctx context.Context, key *service.APIKey, fields service.APIKeyUpdateFields) error {
  237. if needsManagedResourceGuard(ctx) {
  238. return withManagedResourceWrite(ctx, r.client, "api_keys", key.ID, 0, func(next context.Context) error { return r.Update(next, key, fields) })
  239. }
  240. // 空掩码代表调用方不改任何列,直接返回,避免产生一次无意义的整行写。
  241. if fields.IsEmpty() {
  242. return nil
  243. }
  244. // 使用原子操作:将软删除检查与更新合并到同一语句,避免竞态条件。
  245. // 之前的实现先检查 Exist 再 UpdateOneID,若在两步之间发生软删除,
  246. // 则会更新已删除的记录。
  247. // 这里选择 Update().Where(),确保只有未软删除记录能被更新。
  248. // 同时显式设置 updated_at,避免二次查询带来的并发可见性问题。
  249. client := clientFromContext(ctx, r.client)
  250. now := time.Now()
  251. builder := client.APIKey.Update().
  252. Where(apikey.IDEQ(key.ID), apikey.DeletedAtIsNil()).
  253. SetUpdatedAt(now)
  254. if fields.Name {
  255. builder.SetName(key.Name)
  256. }
  257. if fields.Status {
  258. builder.SetStatus(key.Status)
  259. }
  260. if fields.Quota {
  261. builder.SetQuota(key.Quota)
  262. }
  263. if fields.QuotaUsed {
  264. builder.SetQuotaUsed(key.QuotaUsed)
  265. }
  266. if fields.RateLimits {
  267. builder.
  268. SetRateLimit5h(key.RateLimit5h).
  269. SetRateLimit1d(key.RateLimit1d).
  270. SetRateLimit7d(key.RateLimit7d)
  271. }
  272. if fields.RateLimitUsage {
  273. builder.
  274. SetUsage5h(key.Usage5h).
  275. SetUsage1d(key.Usage1d).
  276. SetUsage7d(key.Usage7d)
  277. // Rate limit window start times
  278. if key.Window5hStart != nil {
  279. builder.SetWindow5hStart(*key.Window5hStart)
  280. } else {
  281. builder.ClearWindow5hStart()
  282. }
  283. if key.Window1dStart != nil {
  284. builder.SetWindow1dStart(*key.Window1dStart)
  285. } else {
  286. builder.ClearWindow1dStart()
  287. }
  288. if key.Window7dStart != nil {
  289. builder.SetWindow7dStart(*key.Window7dStart)
  290. } else {
  291. builder.ClearWindow7dStart()
  292. }
  293. }
  294. if fields.GroupID {
  295. if key.GroupID != nil {
  296. builder.SetGroupID(*key.GroupID)
  297. } else {
  298. builder.ClearGroupID()
  299. }
  300. }
  301. // Expiration time
  302. if fields.ExpiresAt {
  303. if key.ExpiresAt != nil {
  304. builder.SetExpiresAt(*key.ExpiresAt)
  305. } else {
  306. builder.ClearExpiresAt()
  307. }
  308. }
  309. // IP 限制字段
  310. if fields.IPRules {
  311. if len(key.IPWhitelist) > 0 {
  312. builder.SetIPWhitelist(key.IPWhitelist)
  313. } else {
  314. builder.ClearIPWhitelist()
  315. }
  316. if len(key.IPBlacklist) > 0 {
  317. builder.SetIPBlacklist(key.IPBlacklist)
  318. } else {
  319. builder.ClearIPBlacklist()
  320. }
  321. }
  322. affected, err := builder.Save(ctx)
  323. if err != nil {
  324. return err
  325. }
  326. if affected == 0 {
  327. // 更新影响行数为 0,说明记录不存在或已被软删除。
  328. return service.ErrAPIKeyNotFound
  329. }
  330. // 使用同一时间戳回填,避免并发删除导致二次查询失败。
  331. key.UpdatedAt = now
  332. return nil
  333. }
  334. func (r *apiKeyRepository) Delete(ctx context.Context, id int64) error {
  335. // 存在唯一键约束 生成tombstone key 用来释放原key,长度远小于 128,满足 schema 限制
  336. tombstoneKey := fmt.Sprintf("__deleted__%d__%d", id, time.Now().UnixNano())
  337. // 显式软删除:避免依赖 Hook 行为,确保 deleted_at 一定被设置。
  338. affected, err := r.client.APIKey.Update().
  339. Where(apikey.IDEQ(id), apikey.DeletedAtIsNil()).
  340. SetKey(tombstoneKey).
  341. SetDeletedAt(time.Now()).
  342. Save(ctx)
  343. if err != nil {
  344. if dbent.IsNotFound(err) {
  345. return service.ErrAPIKeyNotFound
  346. }
  347. return err
  348. }
  349. if affected == 0 {
  350. exists, err := r.client.APIKey.Query().
  351. Where(apikey.IDEQ(id)).
  352. Exist(mixins.SkipSoftDelete(ctx))
  353. if err != nil {
  354. return err
  355. }
  356. if exists {
  357. return nil
  358. }
  359. return service.ErrAPIKeyNotFound
  360. }
  361. return nil
  362. }
  363. // DeleteWithAudit keeps the legacy method name for rolling-upgrade compatibility.
  364. // It atomically tombstones and soft-deletes the key without retaining credential
  365. // material. Tombstoning releases the unique key value for safe reuse.
  366. func (r *apiKeyRepository) DeleteWithAudit(ctx context.Context, id int64) error {
  367. tombstoneKey := fmt.Sprintf("__deleted__%d__%d", id, time.Now().UnixNano())
  368. if existingTx := dbent.TxFromContext(ctx); existingTx != nil {
  369. return r.deleteWithTombstone(ctx, existingTx.Client(), id, tombstoneKey)
  370. }
  371. tx, err := r.client.Tx(ctx)
  372. if err != nil && !errors.Is(err, dbent.ErrTxStarted) {
  373. return err
  374. }
  375. exec := r.client
  376. if err == nil {
  377. defer func() { _ = tx.Rollback() }()
  378. exec = tx.Client()
  379. }
  380. if err := r.deleteWithTombstone(ctx, exec, id, tombstoneKey); err != nil {
  381. return err
  382. }
  383. if tx != nil {
  384. return tx.Commit()
  385. }
  386. return nil
  387. }
  388. func (r *apiKeyRepository) deleteWithTombstone(ctx context.Context, exec *dbent.Client, id int64, tombstoneKey string) error {
  389. res, err := exec.ExecContext(ctx, `
  390. UPDATE api_keys
  391. SET key = $1, deleted_at = NOW(), updated_at = NOW()
  392. WHERE id = $2 AND deleted_at IS NULL`, tombstoneKey, id)
  393. if err != nil {
  394. return err
  395. }
  396. affected, err := res.RowsAffected()
  397. if err != nil {
  398. return err
  399. }
  400. if affected == 0 {
  401. // 并发/重复删除:记录已存在(已软删)则幂等返回 nil(defer 回滚空事务),否则 NotFound。
  402. exists, existErr := r.client.APIKey.Query().
  403. Where(apikey.IDEQ(id)).
  404. Exist(mixins.SkipSoftDelete(ctx))
  405. if existErr != nil {
  406. return existErr
  407. }
  408. if exists {
  409. return nil
  410. }
  411. return service.ErrAPIKeyNotFound
  412. }
  413. return nil
  414. }
  415. func (r *apiKeyRepository) apiKeyListByUserIDQuery(userID int64, filters service.APIKeyListFilters) *dbent.APIKeyQuery {
  416. q := r.activeQuery().Where(apikey.UserIDEQ(userID))
  417. if filters.Search != "" {
  418. q = q.Where(apikey.Or(
  419. apikey.NameContainsFold(filters.Search),
  420. apikey.KeyContainsFold(filters.Search),
  421. ))
  422. }
  423. if filters.Status != "" {
  424. q = q.Where(apikey.StatusEQ(filters.Status))
  425. }
  426. if filters.GroupID != nil {
  427. if *filters.GroupID == 0 {
  428. q = q.Where(apikey.GroupIDIsNil())
  429. } else {
  430. q = q.Where(apikey.GroupIDEQ(*filters.GroupID))
  431. }
  432. }
  433. return q
  434. }
  435. func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
  436. q := r.apiKeyListByUserIDQuery(userID, filters)
  437. total, err := q.Count(ctx)
  438. if err != nil {
  439. return nil, nil, err
  440. }
  441. keysQuery := q.
  442. WithGroup().
  443. Offset(params.Offset()).
  444. Limit(params.Limit())
  445. for _, order := range apiKeyListOrder(params) {
  446. keysQuery = keysQuery.Order(order)
  447. }
  448. keys, err := keysQuery.All(ctx)
  449. if err != nil {
  450. return nil, nil, err
  451. }
  452. outKeys := make([]service.APIKey, 0, len(keys))
  453. for i := range keys {
  454. outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
  455. }
  456. if err := r.attachLastUsedIPs(ctx, outKeys); err != nil {
  457. return nil, nil, err
  458. }
  459. return outKeys, paginationResultFromTotal(int64(total), params), nil
  460. }
  461. func (r *apiKeyRepository) ListAllByUserID(ctx context.Context, userID int64, filters service.APIKeyListFilters) ([]service.APIKey, error) {
  462. keys, err := r.apiKeyListByUserIDQuery(userID, filters).
  463. WithGroup().
  464. Order(dbent.Asc(apikey.FieldID)).
  465. All(ctx)
  466. if err != nil {
  467. return nil, err
  468. }
  469. outKeys := make([]service.APIKey, 0, len(keys))
  470. for i := range keys {
  471. outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
  472. }
  473. if err := r.attachLastUsedIPs(ctx, outKeys); err != nil {
  474. return nil, err
  475. }
  476. return outKeys, nil
  477. }
  478. func (r *apiKeyRepository) attachLastUsedIPs(ctx context.Context, keys []service.APIKey) error {
  479. if len(keys) == 0 || r.sql == nil {
  480. return nil
  481. }
  482. apiKeyIDs := make([]int64, 0, len(keys))
  483. for i := range keys {
  484. apiKeyIDs = append(apiKeyIDs, keys[i].ID)
  485. }
  486. lastUsedIPs, err := r.latestUsageLogIPs(ctx, apiKeyIDs)
  487. if err != nil {
  488. return err
  489. }
  490. for i := range keys {
  491. if ip, ok := lastUsedIPs[keys[i].ID]; ok {
  492. keys[i].LastUsedIP = &ip
  493. }
  494. }
  495. return nil
  496. }
  497. func (r *apiKeyRepository) latestUsageLogIPs(ctx context.Context, apiKeyIDs []int64) (result map[int64]string, err error) {
  498. if len(apiKeyIDs) == 0 || r.sql == nil {
  499. return map[int64]string{}, nil
  500. }
  501. query, args := latestUsageLogIPsQuery(apiKeyIDs, r.client.Driver().Dialect())
  502. rows, err := r.sql.QueryContext(ctx, query, args...)
  503. if err != nil {
  504. return nil, err
  505. }
  506. defer func() {
  507. if closeErr := rows.Close(); closeErr != nil && err == nil {
  508. err = closeErr
  509. }
  510. }()
  511. out := make(map[int64]string, len(apiKeyIDs))
  512. for rows.Next() {
  513. var apiKeyID int64
  514. var ipAddress string
  515. if err := rows.Scan(&apiKeyID, &ipAddress); err != nil {
  516. return nil, err
  517. }
  518. out[apiKeyID] = ipAddress
  519. }
  520. if err := rows.Err(); err != nil {
  521. return nil, err
  522. }
  523. return out, nil
  524. }
  525. func latestUsageLogIPsQuery(apiKeyIDs []int64, dialectName string) (string, []any) {
  526. if dialectName == dialect.Postgres {
  527. // Keep each key lookup bounded to one ordered index probe instead of ranking its full history.
  528. return `
  529. SELECT requested.api_key_id, latest.ip_address
  530. FROM unnest($1::bigint[]) AS requested(api_key_id)
  531. CROSS JOIN LATERAL (
  532. SELECT ul.ip_address
  533. FROM usage_logs AS ul
  534. WHERE ul.api_key_id = requested.api_key_id
  535. AND ul.ip_address IS NOT NULL
  536. AND ul.ip_address <> ''
  537. ORDER BY ul.created_at DESC, ul.id DESC
  538. LIMIT 1
  539. ) AS latest`, []any{pq.Array(apiKeyIDs)}
  540. }
  541. placeholders := make([]string, len(apiKeyIDs))
  542. args := make([]any, len(apiKeyIDs))
  543. for i, id := range apiKeyIDs {
  544. placeholders[i] = "?"
  545. args[i] = id
  546. }
  547. return fmt.Sprintf(`
  548. SELECT api_key_id, ip_address
  549. FROM (
  550. SELECT api_key_id, ip_address,
  551. ROW_NUMBER() OVER (PARTITION BY api_key_id ORDER BY created_at DESC, id DESC) AS rn
  552. FROM usage_logs
  553. WHERE api_key_id IN (%s)
  554. AND ip_address IS NOT NULL
  555. AND ip_address <> ''
  556. ) ranked
  557. WHERE rn = 1`, strings.Join(placeholders, ", ")), args
  558. }
  559. func (r *apiKeyRepository) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
  560. if len(apiKeyIDs) == 0 {
  561. return []int64{}, nil
  562. }
  563. ids, err := r.client.APIKey.Query().
  564. Where(apikey.UserIDEQ(userID), apikey.IDIn(apiKeyIDs...), apikey.DeletedAtIsNil()).
  565. IDs(ctx)
  566. if err != nil {
  567. return nil, err
  568. }
  569. return ids, nil
  570. }
  571. func (r *apiKeyRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) {
  572. count, err := r.activeQuery().Where(apikey.UserIDEQ(userID)).Count(ctx)
  573. return int64(count), err
  574. }
  575. func (r *apiKeyRepository) ExistsByKey(ctx context.Context, key string) (bool, error) {
  576. count, err := r.activeQuery().Where(apikey.Or(apikey.KeyEQ(key), apikey.KeyHashEQ(legacyAPIKeyDigest(key)))).Count(ctx)
  577. return count > 0, err
  578. }
  579. func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) {
  580. q := r.activeQuery().Where(apikey.GroupIDEQ(groupID))
  581. total, err := q.Count(ctx)
  582. if err != nil {
  583. return nil, nil, err
  584. }
  585. keysQuery := q.
  586. WithUser().
  587. Offset(params.Offset()).
  588. Limit(params.Limit())
  589. for _, order := range apiKeyListOrder(params) {
  590. keysQuery = keysQuery.Order(order)
  591. }
  592. keys, err := keysQuery.All(ctx)
  593. if err != nil {
  594. return nil, nil, err
  595. }
  596. outKeys := make([]service.APIKey, 0, len(keys))
  597. for i := range keys {
  598. outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
  599. }
  600. return outKeys, paginationResultFromTotal(int64(total), params), nil
  601. }
  602. func apiKeyListOrder(params pagination.PaginationParams) []func(*entsql.Selector) {
  603. sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
  604. sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc)
  605. var field string
  606. switch sortBy {
  607. case "name":
  608. field = apikey.FieldName
  609. case "status":
  610. field = apikey.FieldStatus
  611. case "expires_at":
  612. field = apikey.FieldExpiresAt
  613. case "last_used_at":
  614. field = apikey.FieldLastUsedAt
  615. case "created_at":
  616. field = apikey.FieldCreatedAt
  617. case "id":
  618. field = apikey.FieldID
  619. default:
  620. field = apikey.FieldID
  621. }
  622. if sortOrder == pagination.SortOrderAsc {
  623. orders := []func(*entsql.Selector){dbent.Asc(field)}
  624. if field != apikey.FieldID {
  625. orders = append(orders, dbent.Asc(apikey.FieldID))
  626. }
  627. return orders
  628. }
  629. orders := []func(*entsql.Selector){dbent.Desc(field)}
  630. if field != apikey.FieldID {
  631. orders = append(orders, dbent.Desc(apikey.FieldID))
  632. }
  633. return orders
  634. }
  635. // SearchAPIKeys searches API keys by user ID and/or keyword (name)
  636. func (r *apiKeyRepository) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) {
  637. q := r.activeQuery()
  638. if userID > 0 {
  639. q = q.Where(apikey.UserIDEQ(userID))
  640. }
  641. if keyword != "" {
  642. q = q.Where(apikey.NameContainsFold(keyword))
  643. }
  644. keys, err := q.Limit(limit).Order(dbent.Desc(apikey.FieldID)).All(ctx)
  645. if err != nil {
  646. return nil, err
  647. }
  648. outKeys := make([]service.APIKey, 0, len(keys))
  649. for i := range keys {
  650. outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
  651. }
  652. return outKeys, nil
  653. }
  654. // ClearGroupIDByGroupID 将指定分组的所有 API Key 的 group_id 设为 nil
  655. func (r *apiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
  656. n, err := r.client.APIKey.Update().
  657. Where(apikey.GroupIDEQ(groupID), apikey.DeletedAtIsNil()).
  658. ClearGroupID().
  659. Save(ctx)
  660. return int64(n), err
  661. }
  662. // UpdateGroupIDByUserAndGroup 将用户下绑定 oldGroupID 的所有 Key 迁移到 newGroupID
  663. func (r *apiKeyRepository) UpdateGroupIDByUserAndGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (int64, error) {
  664. client := clientFromContext(ctx, r.client)
  665. n, err := client.APIKey.Update().
  666. Where(apikey.UserIDEQ(userID), apikey.GroupIDEQ(oldGroupID), apikey.DeletedAtIsNil()).
  667. SetGroupID(newGroupID).
  668. Save(ctx)
  669. return int64(n), err
  670. }
  671. // CountByGroupID 获取分组的 API Key 数量
  672. func (r *apiKeyRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
  673. count, err := r.activeQuery().Where(apikey.GroupIDEQ(groupID)).Count(ctx)
  674. return int64(count), err
  675. }
  676. func (r *apiKeyRepository) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
  677. rows, err := r.activeQuery().Where(apikey.UserIDEQ(userID)).Select(apikey.FieldKey, apikey.FieldKeyHash).All(ctx)
  678. if err != nil {
  679. return nil, err
  680. }
  681. keys := make([]string, 0, len(rows))
  682. for _, row := range rows {
  683. keys = append(keys, (&service.APIKey{Key: row.Key, KeyHash: row.KeyHash}).AuthCacheInvalidationKey())
  684. }
  685. return keys, nil
  686. }
  687. func (r *apiKeyRepository) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
  688. rows, err := r.activeQuery().Where(apikey.GroupIDEQ(groupID)).Select(apikey.FieldKey, apikey.FieldKeyHash).All(ctx)
  689. if err != nil {
  690. return nil, err
  691. }
  692. keys := make([]string, 0, len(rows))
  693. for _, row := range rows {
  694. keys = append(keys, (&service.APIKey{Key: row.Key, KeyHash: row.KeyHash}).AuthCacheInvalidationKey())
  695. }
  696. return keys, nil
  697. }
  698. // IncrementQuotaUsed 使用 Ent 原子递增 quota_used 字段并返回新值
  699. func (r *apiKeyRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
  700. updated, err := r.client.APIKey.UpdateOneID(id).
  701. Where(apikey.DeletedAtIsNil()).
  702. AddQuotaUsed(amount).
  703. Save(ctx)
  704. if err != nil {
  705. if dbent.IsNotFound(err) {
  706. return 0, service.ErrAPIKeyNotFound
  707. }
  708. return 0, err
  709. }
  710. return updated.QuotaUsed, nil
  711. }
  712. // IncrementQuotaUsedAndGetState atomically increments quota_used, conditionally marks the key
  713. // as quota_exhausted, and returns the latest quota state in one round trip.
  714. func (r *apiKeyRepository) IncrementQuotaUsedAndGetState(ctx context.Context, id int64, amount float64) (*service.APIKeyQuotaUsageState, error) {
  715. query := `
  716. UPDATE api_keys
  717. SET
  718. quota_used = quota_used + $1,
  719. status = CASE
  720. WHEN quota > 0 AND quota_used + $1 >= quota THEN $2
  721. ELSE status
  722. END,
  723. updated_at = NOW()
  724. WHERE id = $3 AND deleted_at IS NULL
  725. RETURNING quota_used, quota, COALESCE(NULLIF(key, ''), 'legacy-sha256:' || key_hash, ''), status
  726. `
  727. state := &service.APIKeyQuotaUsageState{}
  728. if err := scanSingleRow(ctx, r.sql, query, []any{amount, service.StatusAPIKeyQuotaExhausted, id}, &state.QuotaUsed, &state.Quota, &state.Key, &state.Status); err != nil {
  729. if err == sql.ErrNoRows {
  730. return nil, service.ErrAPIKeyNotFound
  731. }
  732. return nil, err
  733. }
  734. return state, nil
  735. }
  736. func (r *apiKeyRepository) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
  737. affected, err := r.client.APIKey.Update().
  738. Where(apikey.IDEQ(id), apikey.DeletedAtIsNil()).
  739. SetLastUsedAt(usedAt).
  740. SetUpdatedAt(usedAt).
  741. Save(ctx)
  742. if err != nil {
  743. return err
  744. }
  745. if affected == 0 {
  746. return service.ErrAPIKeyNotFound
  747. }
  748. return nil
  749. }
  750. // IncrementRateLimitUsage atomically increments all rate limit usage counters and initializes
  751. // window start times via COALESCE if not already set.
  752. func (r *apiKeyRepository) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
  753. _, err := r.sql.ExecContext(ctx, `
  754. UPDATE api_keys SET
  755. usage_5h = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN $1 ELSE usage_5h + $1 END,
  756. usage_1d = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN $1 ELSE usage_1d + $1 END,
  757. usage_7d = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN $1 ELSE usage_7d + $1 END,
  758. window_5h_start = CASE WHEN window_5h_start IS NULL OR window_5h_start + INTERVAL '5 hours' <= NOW() THEN NOW() ELSE window_5h_start END,
  759. window_1d_start = CASE WHEN window_1d_start IS NULL OR window_1d_start + INTERVAL '24 hours' <= NOW() THEN date_trunc('day', NOW()) ELSE window_1d_start END,
  760. window_7d_start = CASE WHEN window_7d_start IS NULL OR window_7d_start + INTERVAL '7 days' <= NOW() THEN date_trunc('day', NOW()) ELSE window_7d_start END,
  761. updated_at = NOW()
  762. WHERE id = $2 AND deleted_at IS NULL`,
  763. cost, id)
  764. return err
  765. }
  766. // ResetRateLimitWindows resets expired rate limit windows atomically.
  767. func (r *apiKeyRepository) ResetRateLimitWindows(ctx context.Context, id int64) error {
  768. _, err := r.sql.ExecContext(ctx, `
  769. UPDATE api_keys SET
  770. usage_5h = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN 0 ELSE usage_5h END,
  771. window_5h_start = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN NOW() ELSE window_5h_start END,
  772. usage_1d = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN 0 ELSE usage_1d END,
  773. window_1d_start = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN date_trunc('day', NOW()) ELSE window_1d_start END,
  774. usage_7d = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN 0 ELSE usage_7d END,
  775. window_7d_start = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN date_trunc('day', NOW()) ELSE window_7d_start END,
  776. updated_at = NOW()
  777. WHERE id = $1 AND deleted_at IS NULL`,
  778. id)
  779. return err
  780. }
  781. // GetRateLimitData returns the current rate limit usage and window start times for an API key.
  782. func (r *apiKeyRepository) GetRateLimitData(ctx context.Context, id int64) (result *service.APIKeyRateLimitData, err error) {
  783. rows, err := r.sql.QueryContext(ctx, `
  784. SELECT usage_5h, usage_1d, usage_7d, window_5h_start, window_1d_start, window_7d_start
  785. FROM api_keys
  786. WHERE id = $1 AND deleted_at IS NULL`,
  787. id)
  788. if err != nil {
  789. return nil, err
  790. }
  791. defer func() {
  792. if closeErr := rows.Close(); closeErr != nil && err == nil {
  793. err = closeErr
  794. }
  795. }()
  796. if !rows.Next() {
  797. return nil, service.ErrAPIKeyNotFound
  798. }
  799. data := &service.APIKeyRateLimitData{}
  800. if err := rows.Scan(&data.Usage5h, &data.Usage1d, &data.Usage7d, &data.Window5hStart, &data.Window1dStart, &data.Window7dStart); err != nil {
  801. return nil, err
  802. }
  803. return data, rows.Err()
  804. }
  805. func apiKeyEntityToService(m *dbent.APIKey) *service.APIKey {
  806. if m == nil {
  807. return nil
  808. }
  809. out := &service.APIKey{
  810. ID: m.ID,
  811. UserID: m.UserID,
  812. Key: m.Key,
  813. KeyHash: m.KeyHash,
  814. KeyPrefix: m.KeyPrefix,
  815. Name: m.Name,
  816. Status: m.Status,
  817. IPWhitelist: m.IPWhitelist,
  818. IPBlacklist: m.IPBlacklist,
  819. LastUsedAt: m.LastUsedAt,
  820. CreatedAt: m.CreatedAt,
  821. UpdatedAt: m.UpdatedAt,
  822. GroupID: m.GroupID,
  823. Quota: m.Quota,
  824. QuotaUsed: m.QuotaUsed,
  825. ExpiresAt: m.ExpiresAt,
  826. RateLimit5h: m.RateLimit5h,
  827. RateLimit1d: m.RateLimit1d,
  828. RateLimit7d: m.RateLimit7d,
  829. Usage5h: m.Usage5h,
  830. Usage1d: m.Usage1d,
  831. Usage7d: m.Usage7d,
  832. Window5hStart: m.Window5hStart,
  833. Window1dStart: m.Window1dStart,
  834. Window7dStart: m.Window7dStart,
  835. }
  836. if m.Edges.User != nil {
  837. out.User = userEntityToService(m.Edges.User)
  838. if allowed := m.Edges.User.Edges.AllowedGroups; len(allowed) > 0 {
  839. out.User.AllowedGroups = make([]int64, 0, len(allowed))
  840. for _, g := range allowed {
  841. if g != nil {
  842. out.User.AllowedGroups = append(out.User.AllowedGroups, g.ID)
  843. }
  844. }
  845. }
  846. }
  847. if m.Edges.Group != nil {
  848. out.Group = groupEntityToService(m.Edges.Group)
  849. }
  850. return out
  851. }
  852. func userEntityToService(u *dbent.User) *service.User {
  853. if u == nil {
  854. return nil
  855. }
  856. out := &service.User{
  857. ID: u.ID,
  858. Email: u.Email,
  859. Username: u.Username,
  860. Notes: u.Notes,
  861. PasswordHash: u.PasswordHash,
  862. Role: u.Role,
  863. Balance: u.Balance,
  864. FrozenBalance: u.FrozenBalance,
  865. Concurrency: u.Concurrency,
  866. Status: u.Status,
  867. SignupSource: u.SignupSource,
  868. LastLoginAt: u.LastLoginAt,
  869. LastActiveAt: u.LastActiveAt,
  870. TotpSecretEncrypted: u.TotpSecretEncrypted,
  871. TotpEnabled: u.TotpEnabled,
  872. TotpEnabledAt: u.TotpEnabledAt,
  873. BalanceNotifyEnabled: u.BalanceNotifyEnabled,
  874. RestrictPublicGroups: u.RestrictPublicGroups,
  875. BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
  876. BalanceNotifyThreshold: u.BalanceNotifyThreshold,
  877. TotalRecharged: u.TotalRecharged,
  878. RPMLimit: u.RpmLimit,
  879. CreatedAt: u.CreatedAt,
  880. UpdatedAt: u.UpdatedAt,
  881. DeletedAt: u.DeletedAt,
  882. }
  883. // Parse extra emails JSON (supports both old []string and new []NotifyEmailEntry format)
  884. if u.BalanceNotifyExtraEmails != "" && u.BalanceNotifyExtraEmails != "[]" {
  885. out.BalanceNotifyExtraEmails = service.ParseNotifyEmails(u.BalanceNotifyExtraEmails)
  886. }
  887. return out
  888. }
  889. func groupEntityToService(g *dbent.Group) *service.Group {
  890. if g == nil {
  891. return nil
  892. }
  893. var modelPricing []service.ChannelModelPricing
  894. if len(g.ModelPricing) > 0 {
  895. if err := json.Unmarshal(g.ModelPricing, &modelPricing); err != nil {
  896. slog.Warn("group model_pricing unmarshal failed; falling back to channel/builtin pricing",
  897. "group_id", g.ID, "error", err)
  898. modelPricing = nil
  899. }
  900. }
  901. return &service.Group{
  902. ID: g.ID,
  903. Name: g.Name,
  904. Description: derefString(g.Description),
  905. Platform: g.Platform,
  906. RateMultiplier: g.RateMultiplier,
  907. IsExclusive: g.IsExclusive,
  908. SecurityPolicyEnabled: g.SecurityPolicyEnabled,
  909. SecurityPolicyMode: g.SecurityPolicyMode,
  910. SecurityPolicyEmailEnabled: g.SecurityPolicyEmailEnabled,
  911. Status: g.Status,
  912. Hydrated: true,
  913. DuplicateOperationID: derefString(g.DuplicateOperationID),
  914. SubscriptionType: g.SubscriptionType,
  915. DailyLimitUSD: g.DailyLimitUsd,
  916. WeeklyLimitUSD: g.WeeklyLimitUsd,
  917. MonthlyLimitUSD: g.MonthlyLimitUsd,
  918. AllowImageGeneration: g.AllowImageGeneration,
  919. AllowBatchImageGeneration: g.AllowBatchImageGeneration,
  920. ImageRateIndependent: g.ImageRateIndependent,
  921. ImageRateMultiplier: g.ImageRateMultiplier,
  922. ImagePrice1K: g.ImagePrice1k,
  923. ImagePrice2K: g.ImagePrice2k,
  924. ImagePrice4K: g.ImagePrice4k,
  925. BatchImageDiscountMultiplier: g.BatchImageDiscountMultiplier,
  926. BatchImageHoldMultiplier: g.BatchImageHoldMultiplier,
  927. VideoRateIndependent: g.VideoRateIndependent,
  928. VideoRateMultiplier: g.VideoRateMultiplier,
  929. VideoPrice480P: g.VideoPrice480p,
  930. VideoPrice720P: g.VideoPrice720p,
  931. VideoPrice1080P: g.VideoPrice1080p,
  932. VideoModelPrices: service.NormalizeVideoModelPrices(g.VideoModelPrices),
  933. WebSearchPricePerCall: g.WebSearchPricePerCall,
  934. SearchPricePer1k: g.SearchPricePer1k,
  935. AudioRealtimePricePerMin: g.AudioRealtimePricePerMin,
  936. AudioTTSPricePerMillionChars: g.AudioTtsPricePerMillionChars,
  937. AudioSTTPricePerHour: g.AudioSttPricePerHour,
  938. LongContextPricingEnabled: g.LongContextPricingEnabled,
  939. ModelPricing: modelPricing,
  940. DefaultValidityDays: g.DefaultValidityDays,
  941. ClaudeCodeOnly: g.ClaudeCodeOnly,
  942. FallbackGroupID: g.FallbackGroupID,
  943. FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
  944. ModelRouting: g.ModelRouting,
  945. ModelRoutingEnabled: g.ModelRoutingEnabled,
  946. MCPXMLInject: g.McpXMLInject,
  947. SupportedModelScopes: g.SupportedModelScopes,
  948. SortOrder: g.SortOrder,
  949. AllowMessagesDispatch: g.AllowMessagesDispatch,
  950. AllowLive: g.AllowLive,
  951. ForceOpenAIFast: g.ForceOpenaiFast,
  952. FreeOpenAIFast: g.FreeOpenaiFast,
  953. RequireOAuthOnly: g.RequireOauthOnly,
  954. RequirePrivacySet: g.RequirePrivacySet,
  955. DefaultMappedModel: g.DefaultMappedModel,
  956. MessagesDispatchModelConfig: g.MessagesDispatchModelConfig,
  957. ModelAllowlist: service.GroupModelAllowlistFromDomain(g.ModelAllowlist),
  958. CodexModelsManifestConfig: g.CodexModelsManifestConfig,
  959. RPMLimit: g.RpmLimit,
  960. MaxReasoningEffort: g.MaxReasoningEffort,
  961. MaxReasoningEffortOverLimit: g.MaxReasoningEffortOverLimit,
  962. ReasoningEffortMappings: g.ReasoningEffortMappings,
  963. PeakRateEnabled: g.PeakRateEnabled,
  964. PeakStart: g.PeakStart,
  965. PeakEnd: g.PeakEnd,
  966. PeakRateMultiplier: g.PeakRateMultiplier,
  967. ProfitControlEnabled: g.ProfitControlEnabled,
  968. ProfitMinMargin: g.ProfitMinMargin,
  969. ProfitSafetyBuffer: g.ProfitSafetyBuffer,
  970. CreatedAt: g.CreatedAt,
  971. UpdatedAt: g.UpdatedAt,
  972. }
  973. }
  974. func derefString(s *string) string {
  975. if s == nil {
  976. return ""
  977. }
  978. return *s
  979. }
  980. func legacyAPIKeyDigest(key string) string {
  981. sum := sha256.Sum256([]byte(key))
  982. return hex.EncodeToString(sum[:])
  983. }