user_profile_identity_repo.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "hash/fnv"
  7. "reflect"
  8. "sort"
  9. "strings"
  10. "sync"
  11. "time"
  12. "unsafe"
  13. "entgo.io/ent/dialect"
  14. entsql "entgo.io/ent/dialect/sql"
  15. dbent "github.com/Wei-Shaw/sub2api/ent"
  16. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  17. "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
  18. "github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
  19. dbpredicate "github.com/Wei-Shaw/sub2api/ent/predicate"
  20. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  21. "github.com/Wei-Shaw/sub2api/internal/service"
  22. )
  23. var (
  24. ErrAuthIdentityOwnershipConflict = infraerrors.Conflict(
  25. "AUTH_IDENTITY_OWNERSHIP_CONFLICT",
  26. "auth identity already belongs to another user",
  27. )
  28. ErrAuthIdentityChannelOwnershipConflict = infraerrors.Conflict(
  29. "AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT",
  30. "auth identity channel already belongs to another user",
  31. )
  32. ErrAuthIdentityChannelProviderMismatch = infraerrors.BadRequest(
  33. "AUTH_IDENTITY_CHANNEL_PROVIDER_MISMATCH",
  34. "auth identity channel provider must match canonical identity",
  35. )
  36. )
  37. type ProviderGrantReason string
  38. const (
  39. ProviderGrantReasonSignup ProviderGrantReason = "signup"
  40. ProviderGrantReasonFirstBind ProviderGrantReason = "first_bind"
  41. )
  42. type AuthIdentityKey struct {
  43. ProviderType string
  44. ProviderKey string
  45. ProviderSubject string
  46. }
  47. type AuthIdentityChannelKey struct {
  48. ProviderType string
  49. ProviderKey string
  50. Channel string
  51. ChannelAppID string
  52. ChannelSubject string
  53. }
  54. type CreateAuthIdentityInput struct {
  55. UserID int64
  56. Canonical AuthIdentityKey
  57. Channel *AuthIdentityChannelKey
  58. Issuer *string
  59. VerifiedAt *time.Time
  60. Metadata map[string]any
  61. ChannelMetadata map[string]any
  62. }
  63. type BindAuthIdentityInput = CreateAuthIdentityInput
  64. type CreateAuthIdentityResult struct {
  65. Identity *dbent.AuthIdentity
  66. Channel *dbent.AuthIdentityChannel
  67. }
  68. func (r *CreateAuthIdentityResult) IdentityRef() AuthIdentityKey {
  69. if r == nil || r.Identity == nil {
  70. return AuthIdentityKey{}
  71. }
  72. return AuthIdentityKey{
  73. ProviderType: r.Identity.ProviderType,
  74. ProviderKey: r.Identity.ProviderKey,
  75. ProviderSubject: r.Identity.ProviderSubject,
  76. }
  77. }
  78. func (r *CreateAuthIdentityResult) ChannelRef() *AuthIdentityChannelKey {
  79. if r == nil || r.Channel == nil {
  80. return nil
  81. }
  82. return &AuthIdentityChannelKey{
  83. ProviderType: r.Channel.ProviderType,
  84. ProviderKey: r.Channel.ProviderKey,
  85. Channel: r.Channel.Channel,
  86. ChannelAppID: r.Channel.ChannelAppID,
  87. ChannelSubject: r.Channel.ChannelSubject,
  88. }
  89. }
  90. type UserAuthIdentityLookup struct {
  91. User *dbent.User
  92. Identity *dbent.AuthIdentity
  93. Channel *dbent.AuthIdentityChannel
  94. }
  95. type ProviderGrantRecordInput struct {
  96. UserID int64
  97. ProviderType string
  98. GrantReason ProviderGrantReason
  99. }
  100. type IdentityAdoptionDecisionInput struct {
  101. PendingAuthSessionID int64
  102. IdentityID *int64
  103. AdoptDisplayName bool
  104. AdoptAvatar bool
  105. }
  106. type sqlQueryExecutor interface {
  107. ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
  108. QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
  109. }
  110. var repositoryScopedKeyLocks = newScopedKeyLockRegistry()
  111. type scopedKeyLockRegistry struct {
  112. mu sync.Mutex
  113. locks map[string]*scopedKeyLockEntry
  114. }
  115. type scopedKeyLockEntry struct {
  116. mu sync.Mutex
  117. refs int
  118. }
  119. func newScopedKeyLockRegistry() *scopedKeyLockRegistry {
  120. return &scopedKeyLockRegistry{
  121. locks: make(map[string]*scopedKeyLockEntry),
  122. }
  123. }
  124. func (r *scopedKeyLockRegistry) lock(keys ...string) func() {
  125. normalized := normalizeLockKeys(keys...)
  126. if len(normalized) == 0 {
  127. return func() {}
  128. }
  129. entries := make([]*scopedKeyLockEntry, 0, len(normalized))
  130. r.mu.Lock()
  131. for _, key := range normalized {
  132. entry := r.locks[key]
  133. if entry == nil {
  134. entry = &scopedKeyLockEntry{}
  135. r.locks[key] = entry
  136. }
  137. entry.refs++
  138. entries = append(entries, entry)
  139. }
  140. r.mu.Unlock()
  141. for _, entry := range entries {
  142. entry.mu.Lock()
  143. }
  144. return func() {
  145. for i := len(entries) - 1; i >= 0; i-- {
  146. entries[i].mu.Unlock()
  147. }
  148. r.mu.Lock()
  149. defer r.mu.Unlock()
  150. for idx, key := range normalized {
  151. entry := entries[idx]
  152. entry.refs--
  153. if entry.refs == 0 {
  154. delete(r.locks, key)
  155. }
  156. }
  157. }
  158. }
  159. func normalizeLockKeys(keys ...string) []string {
  160. if len(keys) == 0 {
  161. return nil
  162. }
  163. deduped := make(map[string]struct{}, len(keys))
  164. for _, key := range keys {
  165. trimmed := strings.TrimSpace(key)
  166. if trimmed == "" {
  167. continue
  168. }
  169. deduped[trimmed] = struct{}{}
  170. }
  171. if len(deduped) == 0 {
  172. return nil
  173. }
  174. normalized := make([]string, 0, len(deduped))
  175. for key := range deduped {
  176. normalized = append(normalized, key)
  177. }
  178. sort.Strings(normalized)
  179. return normalized
  180. }
  181. func advisoryLockHash(key string) int64 {
  182. hasher := fnv.New64a()
  183. _, _ = hasher.Write([]byte(key))
  184. return int64(hasher.Sum64())
  185. }
  186. func lockRepositoryScopedKeys(ctx context.Context, client *dbent.Client, exec sqlQueryExecutor, keys ...string) (func(), error) {
  187. release := repositoryScopedKeyLocks.lock(keys...)
  188. normalized := normalizeLockKeys(keys...)
  189. if len(normalized) == 0 || client == nil || exec == nil || client.Driver().Dialect() != dialect.Postgres {
  190. return release, nil
  191. }
  192. for _, key := range normalized {
  193. rows, err := exec.QueryContext(ctx, "SELECT pg_advisory_xact_lock($1)", advisoryLockHash(key))
  194. if err != nil {
  195. release()
  196. return nil, err
  197. }
  198. _ = rows.Close()
  199. }
  200. return release, nil
  201. }
  202. func (r *userRepository) WithUserProfileIdentityTx(ctx context.Context, fn func(txCtx context.Context) error) error {
  203. if dbent.TxFromContext(ctx) != nil {
  204. return fn(ctx)
  205. }
  206. tx, err := r.client.Tx(ctx)
  207. if err != nil {
  208. return err
  209. }
  210. defer func() { _ = tx.Rollback() }()
  211. txCtx := dbent.NewTxContext(ctx, tx)
  212. if err := fn(txCtx); err != nil {
  213. return err
  214. }
  215. return tx.Commit()
  216. }
  217. func (r *userRepository) CreateAuthIdentity(ctx context.Context, input CreateAuthIdentityInput) (*CreateAuthIdentityResult, error) {
  218. if err := validateAuthIdentityChannelProviderMatch(input.Canonical, input.Channel); err != nil {
  219. return nil, err
  220. }
  221. client := clientFromContext(ctx, r.client)
  222. create := client.AuthIdentity.Create().
  223. SetUserID(input.UserID).
  224. SetProviderType(strings.TrimSpace(input.Canonical.ProviderType)).
  225. SetProviderKey(strings.TrimSpace(input.Canonical.ProviderKey)).
  226. SetProviderSubject(strings.TrimSpace(input.Canonical.ProviderSubject)).
  227. SetMetadata(copyMetadata(input.Metadata)).
  228. SetNillableIssuer(input.Issuer).
  229. SetNillableVerifiedAt(input.VerifiedAt)
  230. identity, err := create.Save(ctx)
  231. if err != nil {
  232. return nil, err
  233. }
  234. var channel *dbent.AuthIdentityChannel
  235. if input.Channel != nil {
  236. channel, err = client.AuthIdentityChannel.Create().
  237. SetIdentityID(identity.ID).
  238. SetProviderType(strings.TrimSpace(input.Channel.ProviderType)).
  239. SetProviderKey(strings.TrimSpace(input.Channel.ProviderKey)).
  240. SetChannel(strings.TrimSpace(input.Channel.Channel)).
  241. SetChannelAppID(strings.TrimSpace(input.Channel.ChannelAppID)).
  242. SetChannelSubject(strings.TrimSpace(input.Channel.ChannelSubject)).
  243. SetMetadata(copyMetadata(input.ChannelMetadata)).
  244. Save(ctx)
  245. if err != nil {
  246. return nil, err
  247. }
  248. }
  249. return &CreateAuthIdentityResult{Identity: identity, Channel: channel}, nil
  250. }
  251. func (r *userRepository) GetUserByCanonicalIdentity(ctx context.Context, key AuthIdentityKey) (*UserAuthIdentityLookup, error) {
  252. identity, err := clientFromContext(ctx, r.client).AuthIdentity.Query().
  253. Where(
  254. authidentity.ProviderTypeEQ(strings.TrimSpace(key.ProviderType)),
  255. authidentity.ProviderKeyEQ(strings.TrimSpace(key.ProviderKey)),
  256. authidentity.ProviderSubjectEQ(strings.TrimSpace(key.ProviderSubject)),
  257. ).
  258. WithUser().
  259. Only(ctx)
  260. if err != nil {
  261. return nil, err
  262. }
  263. return &UserAuthIdentityLookup{
  264. User: identity.Edges.User,
  265. Identity: identity,
  266. }, nil
  267. }
  268. func (r *userRepository) GetUserByChannelIdentity(ctx context.Context, key AuthIdentityChannelKey) (*UserAuthIdentityLookup, error) {
  269. channel, err := clientFromContext(ctx, r.client).AuthIdentityChannel.Query().
  270. Where(
  271. authidentitychannel.ProviderTypeEQ(strings.TrimSpace(key.ProviderType)),
  272. authidentitychannel.ProviderKeyEQ(strings.TrimSpace(key.ProviderKey)),
  273. authidentitychannel.ChannelEQ(strings.TrimSpace(key.Channel)),
  274. authidentitychannel.ChannelAppIDEQ(strings.TrimSpace(key.ChannelAppID)),
  275. authidentitychannel.ChannelSubjectEQ(strings.TrimSpace(key.ChannelSubject)),
  276. ).
  277. WithIdentity(func(q *dbent.AuthIdentityQuery) {
  278. q.WithUser()
  279. }).
  280. Only(ctx)
  281. if err != nil {
  282. return nil, err
  283. }
  284. return &UserAuthIdentityLookup{
  285. User: channel.Edges.Identity.Edges.User,
  286. Identity: channel.Edges.Identity,
  287. Channel: channel,
  288. }, nil
  289. }
  290. func (r *userRepository) ListUserAuthIdentities(ctx context.Context, userID int64) ([]service.UserAuthIdentityRecord, error) {
  291. identities, err := clientFromContext(ctx, r.client).AuthIdentity.Query().
  292. Where(authidentity.UserIDEQ(userID)).
  293. All(ctx)
  294. if err != nil {
  295. return nil, err
  296. }
  297. records := make([]service.UserAuthIdentityRecord, 0, len(identities))
  298. for _, identity := range identities {
  299. if identity == nil {
  300. continue
  301. }
  302. records = append(records, service.UserAuthIdentityRecord{
  303. ProviderType: strings.TrimSpace(identity.ProviderType),
  304. ProviderKey: strings.TrimSpace(identity.ProviderKey),
  305. ProviderSubject: strings.TrimSpace(identity.ProviderSubject),
  306. VerifiedAt: identity.VerifiedAt,
  307. Issuer: identity.Issuer,
  308. Metadata: copyMetadata(identity.Metadata),
  309. CreatedAt: identity.CreatedAt,
  310. UpdatedAt: identity.UpdatedAt,
  311. })
  312. }
  313. return records, nil
  314. }
  315. func (r *userRepository) UnbindUserAuthProvider(ctx context.Context, userID int64, provider string) error {
  316. provider = strings.ToLower(strings.TrimSpace(provider))
  317. if provider == "" || provider == "email" {
  318. return service.ErrIdentityProviderInvalid
  319. }
  320. return r.WithUserProfileIdentityTx(ctx, func(txCtx context.Context) error {
  321. client := clientFromContext(txCtx, r.client)
  322. identityIDs, err := client.AuthIdentity.Query().
  323. Where(
  324. authidentity.UserIDEQ(userID),
  325. authidentity.ProviderTypeEQ(provider),
  326. ).
  327. IDs(txCtx)
  328. if err != nil {
  329. return err
  330. }
  331. if len(identityIDs) == 0 {
  332. return nil
  333. }
  334. if _, err := client.IdentityAdoptionDecision.Update().
  335. Where(identityadoptiondecision.IdentityIDIn(identityIDs...)).
  336. ClearIdentityID().
  337. Save(txCtx); err != nil {
  338. return err
  339. }
  340. if _, err := client.AuthIdentityChannel.Delete().
  341. Where(authidentitychannel.IdentityIDIn(identityIDs...)).
  342. Exec(txCtx); err != nil {
  343. return err
  344. }
  345. _, err = client.AuthIdentity.Delete().
  346. Where(
  347. authidentity.UserIDEQ(userID),
  348. authidentity.ProviderTypeEQ(provider),
  349. ).
  350. Exec(txCtx)
  351. return err
  352. })
  353. }
  354. func (r *userRepository) BindAuthIdentityToUser(ctx context.Context, input BindAuthIdentityInput) (*CreateAuthIdentityResult, error) {
  355. if err := validateAuthIdentityChannelProviderMatch(input.Canonical, input.Channel); err != nil {
  356. return nil, err
  357. }
  358. var result *CreateAuthIdentityResult
  359. err := r.WithUserProfileIdentityTx(ctx, func(txCtx context.Context) error {
  360. client := clientFromContext(txCtx, r.client)
  361. canonical := input.Canonical
  362. identityRecords, err := client.AuthIdentity.Query().
  363. Where(
  364. authidentity.ProviderTypeEQ(strings.TrimSpace(canonical.ProviderType)),
  365. authidentity.ProviderKeyIn(compatibleIdentityProviderKeys(canonical.ProviderType, canonical.ProviderKey)...),
  366. authidentity.ProviderSubjectEQ(strings.TrimSpace(canonical.ProviderSubject)),
  367. ).
  368. All(txCtx)
  369. if err != nil {
  370. return err
  371. }
  372. identity := selectOwnedCompatibleIdentity(identityRecords, input.UserID)
  373. if identity == nil && hasCompatibleIdentityConflict(identityRecords, input.UserID) {
  374. return ErrAuthIdentityOwnershipConflict
  375. }
  376. if identity == nil {
  377. identity, err = client.AuthIdentity.Create().
  378. SetUserID(input.UserID).
  379. SetProviderType(strings.TrimSpace(canonical.ProviderType)).
  380. SetProviderKey(strings.TrimSpace(canonical.ProviderKey)).
  381. SetProviderSubject(strings.TrimSpace(canonical.ProviderSubject)).
  382. SetMetadata(copyMetadata(input.Metadata)).
  383. SetNillableIssuer(input.Issuer).
  384. SetNillableVerifiedAt(input.VerifiedAt).
  385. Save(txCtx)
  386. if err != nil {
  387. return err
  388. }
  389. } else {
  390. targetProviderKey := canonicalizeCompatibleIdentityProviderKey(canonical.ProviderType, identity.ProviderKey, canonical.ProviderKey)
  391. update := client.AuthIdentity.UpdateOneID(identity.ID)
  392. if targetProviderKey != "" && !strings.EqualFold(targetProviderKey, identity.ProviderKey) {
  393. update = update.SetProviderKey(targetProviderKey)
  394. }
  395. if input.Metadata != nil {
  396. update = update.SetMetadata(copyMetadata(input.Metadata))
  397. }
  398. if input.Issuer != nil {
  399. update = update.SetIssuer(strings.TrimSpace(*input.Issuer))
  400. }
  401. if input.VerifiedAt != nil {
  402. update = update.SetVerifiedAt(*input.VerifiedAt)
  403. }
  404. identity, err = update.Save(txCtx)
  405. if err != nil {
  406. return err
  407. }
  408. }
  409. var channel *dbent.AuthIdentityChannel
  410. if input.Channel != nil {
  411. channelRecords, err := client.AuthIdentityChannel.Query().
  412. Where(
  413. authidentitychannel.ProviderTypeEQ(strings.TrimSpace(input.Channel.ProviderType)),
  414. authidentitychannel.ProviderKeyIn(compatibleIdentityProviderKeys(input.Channel.ProviderType, input.Channel.ProviderKey)...),
  415. authidentitychannel.ChannelEQ(strings.TrimSpace(input.Channel.Channel)),
  416. authidentitychannel.ChannelAppIDEQ(strings.TrimSpace(input.Channel.ChannelAppID)),
  417. authidentitychannel.ChannelSubjectEQ(strings.TrimSpace(input.Channel.ChannelSubject)),
  418. ).
  419. WithIdentity().
  420. All(txCtx)
  421. if err != nil {
  422. return err
  423. }
  424. channel = selectOwnedCompatibleChannel(channelRecords, input.UserID)
  425. if channel == nil && hasCompatibleChannelConflict(channelRecords, input.UserID) {
  426. return ErrAuthIdentityChannelOwnershipConflict
  427. }
  428. if channel == nil {
  429. channel, err = client.AuthIdentityChannel.Create().
  430. SetIdentityID(identity.ID).
  431. SetProviderType(strings.TrimSpace(input.Channel.ProviderType)).
  432. SetProviderKey(strings.TrimSpace(input.Channel.ProviderKey)).
  433. SetChannel(strings.TrimSpace(input.Channel.Channel)).
  434. SetChannelAppID(strings.TrimSpace(input.Channel.ChannelAppID)).
  435. SetChannelSubject(strings.TrimSpace(input.Channel.ChannelSubject)).
  436. SetMetadata(copyMetadata(input.ChannelMetadata)).
  437. Save(txCtx)
  438. if err != nil {
  439. return err
  440. }
  441. } else {
  442. targetProviderKey := canonicalizeCompatibleIdentityProviderKey(input.Channel.ProviderType, channel.ProviderKey, input.Channel.ProviderKey)
  443. update := client.AuthIdentityChannel.UpdateOneID(channel.ID).
  444. SetIdentityID(identity.ID)
  445. if targetProviderKey != "" && !strings.EqualFold(targetProviderKey, channel.ProviderKey) {
  446. update = update.SetProviderKey(targetProviderKey)
  447. }
  448. if input.ChannelMetadata != nil {
  449. update = update.SetMetadata(copyMetadata(input.ChannelMetadata))
  450. }
  451. channel, err = update.Save(txCtx)
  452. if err != nil {
  453. return err
  454. }
  455. }
  456. }
  457. result = &CreateAuthIdentityResult{Identity: identity, Channel: channel}
  458. return nil
  459. })
  460. if err != nil {
  461. return nil, err
  462. }
  463. return result, nil
  464. }
  465. func compatibleIdentityProviderKeys(providerType, providerKey string) []string {
  466. providerType = strings.TrimSpace(strings.ToLower(providerType))
  467. providerKey = strings.TrimSpace(providerKey)
  468. if providerKey == "" {
  469. return []string{providerKey}
  470. }
  471. if providerType != "wechat" {
  472. return []string{providerKey}
  473. }
  474. keys := []string{providerKey}
  475. if !strings.EqualFold(providerKey, "wechat-main") {
  476. keys = append(keys, "wechat-main")
  477. }
  478. if !strings.EqualFold(providerKey, "wechat") {
  479. keys = append(keys, "wechat")
  480. }
  481. return keys
  482. }
  483. func canonicalizeCompatibleIdentityProviderKey(providerType, existingKey, requestedKey string) string {
  484. providerType = strings.TrimSpace(strings.ToLower(providerType))
  485. existingKey = strings.TrimSpace(existingKey)
  486. requestedKey = strings.TrimSpace(requestedKey)
  487. if providerType != "wechat" {
  488. if requestedKey != "" {
  489. return requestedKey
  490. }
  491. return existingKey
  492. }
  493. if strings.EqualFold(existingKey, "wechat") || strings.EqualFold(existingKey, "wechat-main") || strings.EqualFold(requestedKey, "wechat-main") {
  494. return "wechat-main"
  495. }
  496. if requestedKey != "" {
  497. return requestedKey
  498. }
  499. return existingKey
  500. }
  501. func compatibleIdentityProviderKeyRank(providerType, providerKey string) int {
  502. providerType = strings.TrimSpace(strings.ToLower(providerType))
  503. providerKey = strings.TrimSpace(providerKey)
  504. if providerType != "wechat" {
  505. return 0
  506. }
  507. switch {
  508. case strings.EqualFold(providerKey, "wechat-main"):
  509. return 0
  510. case strings.EqualFold(providerKey, "wechat"):
  511. return 2
  512. default:
  513. return 1
  514. }
  515. }
  516. func selectOwnedCompatibleIdentity(records []*dbent.AuthIdentity, userID int64) *dbent.AuthIdentity {
  517. var selected *dbent.AuthIdentity
  518. for _, record := range records {
  519. if record.UserID != userID {
  520. continue
  521. }
  522. if selected == nil || compatibleIdentityProviderKeyRank(record.ProviderType, record.ProviderKey) < compatibleIdentityProviderKeyRank(selected.ProviderType, selected.ProviderKey) {
  523. selected = record
  524. }
  525. }
  526. return selected
  527. }
  528. func hasCompatibleIdentityConflict(records []*dbent.AuthIdentity, userID int64) bool {
  529. for _, record := range records {
  530. if record.UserID != userID {
  531. return true
  532. }
  533. }
  534. return false
  535. }
  536. func selectOwnedCompatibleChannel(records []*dbent.AuthIdentityChannel, userID int64) *dbent.AuthIdentityChannel {
  537. var selected *dbent.AuthIdentityChannel
  538. for _, record := range records {
  539. if record.Edges.Identity == nil || record.Edges.Identity.UserID != userID {
  540. continue
  541. }
  542. if selected == nil || compatibleIdentityProviderKeyRank(record.ProviderType, record.ProviderKey) < compatibleIdentityProviderKeyRank(selected.ProviderType, selected.ProviderKey) {
  543. selected = record
  544. }
  545. }
  546. return selected
  547. }
  548. func hasCompatibleChannelConflict(records []*dbent.AuthIdentityChannel, userID int64) bool {
  549. for _, record := range records {
  550. if record.Edges.Identity != nil && record.Edges.Identity.UserID != userID {
  551. return true
  552. }
  553. }
  554. return false
  555. }
  556. func (r *userRepository) RecordProviderGrant(ctx context.Context, input ProviderGrantRecordInput) (bool, error) {
  557. exec := txAwareSQLExecutor(ctx, r.sql, r.client)
  558. if exec == nil {
  559. return false, fmt.Errorf("sql executor is not configured")
  560. }
  561. result, err := exec.ExecContext(ctx, `
  562. INSERT INTO user_provider_default_grants (user_id, provider_type, grant_reason)
  563. VALUES ($1, $2, $3)
  564. ON CONFLICT (user_id, provider_type, grant_reason) DO NOTHING`,
  565. input.UserID,
  566. strings.TrimSpace(input.ProviderType),
  567. string(input.GrantReason),
  568. )
  569. if err != nil {
  570. return false, err
  571. }
  572. affected, err := result.RowsAffected()
  573. if err != nil {
  574. return false, err
  575. }
  576. return affected > 0, nil
  577. }
  578. func (r *userRepository) UpsertIdentityAdoptionDecision(ctx context.Context, input IdentityAdoptionDecisionInput) (*dbent.IdentityAdoptionDecision, error) {
  579. var result *dbent.IdentityAdoptionDecision
  580. err := r.WithUserProfileIdentityTx(ctx, func(txCtx context.Context) error {
  581. client := clientFromContext(txCtx, r.client)
  582. releaseLocks, err := lockRepositoryScopedKeys(
  583. txCtx,
  584. client,
  585. txAwareSQLExecutor(txCtx, r.sql, r.client),
  586. identityAdoptionDecisionLockKeys(input.PendingAuthSessionID, input.IdentityID)...,
  587. )
  588. if err != nil {
  589. return err
  590. }
  591. defer releaseLocks()
  592. if input.IdentityID != nil && *input.IdentityID > 0 {
  593. if _, err := client.IdentityAdoptionDecision.Update().
  594. Where(
  595. identityadoptiondecision.IdentityIDEQ(*input.IdentityID),
  596. dbpredicate.IdentityAdoptionDecision(func(s *entsql.Selector) {
  597. col := s.C(identityadoptiondecision.FieldPendingAuthSessionID)
  598. s.Where(entsql.Or(
  599. entsql.IsNull(col),
  600. entsql.NEQ(col, input.PendingAuthSessionID),
  601. ))
  602. }),
  603. ).
  604. ClearIdentityID().
  605. Save(txCtx); err != nil {
  606. return err
  607. }
  608. }
  609. create := client.IdentityAdoptionDecision.Create().
  610. SetPendingAuthSessionID(input.PendingAuthSessionID).
  611. SetAdoptDisplayName(input.AdoptDisplayName).
  612. SetAdoptAvatar(input.AdoptAvatar).
  613. SetDecidedAt(time.Now().UTC())
  614. if input.IdentityID != nil && *input.IdentityID > 0 {
  615. create = create.SetIdentityID(*input.IdentityID)
  616. }
  617. decisionID, err := create.
  618. OnConflictColumns(identityadoptiondecision.FieldPendingAuthSessionID).
  619. UpdateNewValues().
  620. ID(txCtx)
  621. if err != nil {
  622. return err
  623. }
  624. result, err = client.IdentityAdoptionDecision.Get(txCtx, decisionID)
  625. return err
  626. })
  627. if err != nil {
  628. return nil, err
  629. }
  630. return result, nil
  631. }
  632. func identityAdoptionDecisionLockKeys(pendingAuthSessionID int64, identityID *int64) []string {
  633. keys := []string{fmt.Sprintf("identity-adoption:pending:%d", pendingAuthSessionID)}
  634. if identityID != nil && *identityID > 0 {
  635. keys = append(keys, fmt.Sprintf("identity-adoption:identity:%d", *identityID))
  636. }
  637. return keys
  638. }
  639. func (r *userRepository) GetIdentityAdoptionDecisionByPendingAuthSessionID(ctx context.Context, pendingAuthSessionID int64) (*dbent.IdentityAdoptionDecision, error) {
  640. return clientFromContext(ctx, r.client).IdentityAdoptionDecision.Query().
  641. Where(identityadoptiondecision.PendingAuthSessionIDEQ(pendingAuthSessionID)).
  642. Only(ctx)
  643. }
  644. func (r *userRepository) UpdateUserLastLoginAt(ctx context.Context, userID int64, loginAt time.Time) error {
  645. _, err := clientFromContext(ctx, r.client).User.UpdateOneID(userID).
  646. SetLastLoginAt(loginAt).
  647. Save(ctx)
  648. return err
  649. }
  650. func (r *userRepository) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
  651. _, err := clientFromContext(ctx, r.client).User.UpdateOneID(userID).
  652. SetLastActiveAt(activeAt).
  653. Save(ctx)
  654. return err
  655. }
  656. func (r *userRepository) GetUserAvatar(ctx context.Context, userID int64) (*service.UserAvatar, error) {
  657. exec, err := r.userProfileIdentitySQL(ctx)
  658. if err != nil {
  659. return nil, err
  660. }
  661. rows, err := exec.QueryContext(ctx, `
  662. SELECT storage_provider, storage_key, url, content_type, byte_size, sha256
  663. FROM user_avatars
  664. WHERE user_id = $1`, userID)
  665. if err != nil {
  666. return nil, err
  667. }
  668. defer func() { _ = rows.Close() }()
  669. if !rows.Next() {
  670. return nil, rows.Err()
  671. }
  672. var avatar service.UserAvatar
  673. if err := rows.Scan(
  674. &avatar.StorageProvider,
  675. &avatar.StorageKey,
  676. &avatar.URL,
  677. &avatar.ContentType,
  678. &avatar.ByteSize,
  679. &avatar.SHA256,
  680. ); err != nil {
  681. return nil, err
  682. }
  683. if err := rows.Err(); err != nil {
  684. return nil, err
  685. }
  686. return &avatar, nil
  687. }
  688. func (r *userRepository) UpsertUserAvatar(ctx context.Context, userID int64, input service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
  689. exec, err := r.userProfileIdentitySQL(ctx)
  690. if err != nil {
  691. return nil, err
  692. }
  693. _, err = exec.ExecContext(ctx, `
  694. INSERT INTO user_avatars (user_id, storage_provider, storage_key, url, content_type, byte_size, sha256, updated_at)
  695. VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
  696. ON CONFLICT (user_id) DO UPDATE SET
  697. storage_provider = EXCLUDED.storage_provider,
  698. storage_key = EXCLUDED.storage_key,
  699. url = EXCLUDED.url,
  700. content_type = EXCLUDED.content_type,
  701. byte_size = EXCLUDED.byte_size,
  702. sha256 = EXCLUDED.sha256,
  703. updated_at = NOW()`,
  704. userID,
  705. strings.TrimSpace(input.StorageProvider),
  706. strings.TrimSpace(input.StorageKey),
  707. strings.TrimSpace(input.URL),
  708. strings.TrimSpace(input.ContentType),
  709. input.ByteSize,
  710. strings.TrimSpace(input.SHA256),
  711. )
  712. if err != nil {
  713. return nil, err
  714. }
  715. return &service.UserAvatar{
  716. StorageProvider: strings.TrimSpace(input.StorageProvider),
  717. StorageKey: strings.TrimSpace(input.StorageKey),
  718. URL: strings.TrimSpace(input.URL),
  719. ContentType: strings.TrimSpace(input.ContentType),
  720. ByteSize: input.ByteSize,
  721. SHA256: strings.TrimSpace(input.SHA256),
  722. }, nil
  723. }
  724. func (r *userRepository) DeleteUserAvatar(ctx context.Context, userID int64) error {
  725. exec, err := r.userProfileIdentitySQL(ctx)
  726. if err != nil {
  727. return err
  728. }
  729. _, err = exec.ExecContext(ctx, `DELETE FROM user_avatars WHERE user_id = $1`, userID)
  730. return err
  731. }
  732. func copyMetadata(in map[string]any) map[string]any {
  733. if len(in) == 0 {
  734. return map[string]any{}
  735. }
  736. out := make(map[string]any, len(in))
  737. for k, v := range in {
  738. out[k] = v
  739. }
  740. return out
  741. }
  742. func validateAuthIdentityChannelProviderMatch(canonical AuthIdentityKey, channel *AuthIdentityChannelKey) error {
  743. if channel == nil {
  744. return nil
  745. }
  746. canonicalProviderType := strings.TrimSpace(canonical.ProviderType)
  747. canonicalProviderKey := strings.TrimSpace(canonical.ProviderKey)
  748. channelProviderType := strings.TrimSpace(channel.ProviderType)
  749. channelProviderKey := strings.TrimSpace(channel.ProviderKey)
  750. if canonicalProviderType != channelProviderType || canonicalProviderKey != channelProviderKey {
  751. return ErrAuthIdentityChannelProviderMismatch
  752. }
  753. return nil
  754. }
  755. func txAwareSQLExecutor(ctx context.Context, fallback sqlExecutor, client *dbent.Client) sqlQueryExecutor {
  756. if tx := dbent.TxFromContext(ctx); tx != nil {
  757. if exec := sqlExecutorFromEntClient(tx.Client()); exec != nil {
  758. return exec
  759. }
  760. }
  761. if fallback != nil {
  762. return fallback
  763. }
  764. return sqlExecutorFromEntClient(client)
  765. }
  766. func (r *userRepository) userProfileIdentitySQL(ctx context.Context) (sqlQueryExecutor, error) {
  767. exec := txAwareSQLExecutor(ctx, r.sql, r.client)
  768. if exec == nil {
  769. return nil, fmt.Errorf("sql executor is not configured")
  770. }
  771. return exec, nil
  772. }
  773. func sqlExecutorFromEntClient(client *dbent.Client) sqlQueryExecutor {
  774. if client == nil {
  775. return nil
  776. }
  777. clientValue := reflect.ValueOf(client).Elem()
  778. configValue := clientValue.FieldByName("config")
  779. driverValue := configValue.FieldByName("driver")
  780. if !driverValue.IsValid() {
  781. return nil
  782. }
  783. driver := reflect.NewAt(driverValue.Type(), unsafe.Pointer(driverValue.UnsafeAddr())).Elem().Interface()
  784. exec, ok := driver.(sqlQueryExecutor)
  785. if !ok {
  786. return nil
  787. }
  788. return exec
  789. }