ops_auth_store.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package postgres
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "time"
  7. "github.com/jackc/pgx/v5"
  8. )
  9. type OpsUser struct {
  10. ID string
  11. PublicID string
  12. CountryCode string
  13. Mobile string
  14. DisplayName string
  15. Status string
  16. LastLoginAt *time.Time
  17. }
  18. type OpsRole struct {
  19. ID string
  20. RoleCode string
  21. DisplayName string
  22. RoleRank int
  23. }
  24. type CreateOpsUserParams struct {
  25. PublicID string
  26. CountryCode string
  27. Mobile string
  28. DisplayName string
  29. Status string
  30. }
  31. type CreateOpsRefreshTokenParams struct {
  32. OpsUserID string
  33. DeviceKey string
  34. TokenHash string
  35. ExpiresAt time.Time
  36. }
  37. type OpsRefreshTokenRecord struct {
  38. ID string
  39. OpsUserID string
  40. DeviceKey *string
  41. ExpiresAt time.Time
  42. IsRevoked bool
  43. }
  44. func (s *Store) CountOpsUsers(ctx context.Context) (int, error) {
  45. row := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM ops_users WHERE status <> 'deleted'`)
  46. var count int
  47. if err := row.Scan(&count); err != nil {
  48. return 0, fmt.Errorf("count ops users: %w", err)
  49. }
  50. return count, nil
  51. }
  52. func (s *Store) GetOpsUserByMobile(ctx context.Context, db queryRower, countryCode, mobile string) (*OpsUser, error) {
  53. row := db.QueryRow(ctx, `
  54. SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
  55. FROM ops_users
  56. WHERE country_code = $1 AND mobile = $2
  57. LIMIT 1
  58. `, countryCode, mobile)
  59. return scanOpsUser(row)
  60. }
  61. func (s *Store) GetOpsUserByID(ctx context.Context, db queryRower, opsUserID string) (*OpsUser, error) {
  62. row := db.QueryRow(ctx, `
  63. SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
  64. FROM ops_users
  65. WHERE id = $1
  66. `, opsUserID)
  67. return scanOpsUser(row)
  68. }
  69. func (s *Store) CreateOpsUser(ctx context.Context, tx Tx, params CreateOpsUserParams) (*OpsUser, error) {
  70. row := tx.QueryRow(ctx, `
  71. INSERT INTO ops_users (ops_user_public_id, country_code, mobile, display_name, status)
  72. VALUES ($1, $2, $3, $4, $5)
  73. RETURNING id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
  74. `, params.PublicID, params.CountryCode, params.Mobile, params.DisplayName, params.Status)
  75. return scanOpsUser(row)
  76. }
  77. func (s *Store) TouchOpsUserLogin(ctx context.Context, tx Tx, opsUserID string) error {
  78. _, err := tx.Exec(ctx, `
  79. UPDATE ops_users
  80. SET last_login_at = NOW()
  81. WHERE id = $1
  82. `, opsUserID)
  83. if err != nil {
  84. return fmt.Errorf("touch ops user last login: %w", err)
  85. }
  86. return nil
  87. }
  88. func (s *Store) GetOpsRoleByCode(ctx context.Context, tx Tx, roleCode string) (*OpsRole, error) {
  89. row := tx.QueryRow(ctx, `
  90. SELECT id, role_code, display_name, role_rank
  91. FROM ops_roles
  92. WHERE role_code = $1
  93. AND status = 'active'
  94. LIMIT 1
  95. `, roleCode)
  96. return scanOpsRole(row)
  97. }
  98. func (s *Store) AssignOpsRole(ctx context.Context, tx Tx, opsUserID, roleID string) error {
  99. _, err := tx.Exec(ctx, `
  100. INSERT INTO ops_user_roles (ops_user_id, ops_role_id, status)
  101. VALUES ($1, $2, 'active')
  102. ON CONFLICT (ops_user_id, ops_role_id) DO NOTHING
  103. `, opsUserID, roleID)
  104. if err != nil {
  105. return fmt.Errorf("assign ops role: %w", err)
  106. }
  107. return nil
  108. }
  109. func (s *Store) GetPrimaryOpsRole(ctx context.Context, db queryRower, opsUserID string) (*OpsRole, error) {
  110. row := db.QueryRow(ctx, `
  111. SELECT r.id, r.role_code, r.display_name, r.role_rank
  112. FROM ops_user_roles ur
  113. JOIN ops_roles r ON r.id = ur.ops_role_id
  114. WHERE ur.ops_user_id = $1
  115. AND ur.status = 'active'
  116. AND r.status = 'active'
  117. ORDER BY r.role_rank DESC, r.created_at ASC
  118. LIMIT 1
  119. `, opsUserID)
  120. return scanOpsRole(row)
  121. }
  122. func (s *Store) CreateOpsRefreshToken(ctx context.Context, tx Tx, params CreateOpsRefreshTokenParams) (string, error) {
  123. row := tx.QueryRow(ctx, `
  124. INSERT INTO ops_refresh_tokens (ops_user_id, device_key, token_hash, expires_at)
  125. VALUES ($1, NULLIF($2, ''), $3, $4)
  126. RETURNING id
  127. `, params.OpsUserID, params.DeviceKey, params.TokenHash, params.ExpiresAt)
  128. var id string
  129. if err := row.Scan(&id); err != nil {
  130. return "", fmt.Errorf("create ops refresh token: %w", err)
  131. }
  132. return id, nil
  133. }
  134. func (s *Store) GetOpsRefreshTokenForUpdate(ctx context.Context, tx Tx, tokenHash string) (*OpsRefreshTokenRecord, error) {
  135. row := tx.QueryRow(ctx, `
  136. SELECT id, ops_user_id, device_key, expires_at, revoked_at IS NOT NULL
  137. FROM ops_refresh_tokens
  138. WHERE token_hash = $1
  139. FOR UPDATE
  140. `, tokenHash)
  141. var record OpsRefreshTokenRecord
  142. err := row.Scan(&record.ID, &record.OpsUserID, &record.DeviceKey, &record.ExpiresAt, &record.IsRevoked)
  143. if errors.Is(err, pgx.ErrNoRows) {
  144. return nil, nil
  145. }
  146. if err != nil {
  147. return nil, fmt.Errorf("query ops refresh token for update: %w", err)
  148. }
  149. return &record, nil
  150. }
  151. func (s *Store) RotateOpsRefreshToken(ctx context.Context, tx Tx, oldTokenID, newTokenID string) error {
  152. _, err := tx.Exec(ctx, `
  153. UPDATE ops_refresh_tokens
  154. SET revoked_at = NOW(), replaced_by_token_id = $2
  155. WHERE id = $1
  156. `, oldTokenID, newTokenID)
  157. if err != nil {
  158. return fmt.Errorf("rotate ops refresh token: %w", err)
  159. }
  160. return nil
  161. }
  162. func (s *Store) RevokeOpsRefreshToken(ctx context.Context, tokenHash string) error {
  163. _, err := s.pool.Exec(ctx, `
  164. UPDATE ops_refresh_tokens
  165. SET revoked_at = COALESCE(revoked_at, NOW())
  166. WHERE token_hash = $1
  167. `, tokenHash)
  168. if err != nil {
  169. return fmt.Errorf("revoke ops refresh token: %w", err)
  170. }
  171. return nil
  172. }
  173. func scanOpsUser(row pgx.Row) (*OpsUser, error) {
  174. var item OpsUser
  175. err := row.Scan(&item.ID, &item.PublicID, &item.CountryCode, &item.Mobile, &item.DisplayName, &item.Status, &item.LastLoginAt)
  176. if errors.Is(err, pgx.ErrNoRows) {
  177. return nil, nil
  178. }
  179. if err != nil {
  180. return nil, fmt.Errorf("scan ops user: %w", err)
  181. }
  182. return &item, nil
  183. }
  184. func scanOpsRole(row pgx.Row) (*OpsRole, error) {
  185. var item OpsRole
  186. err := row.Scan(&item.ID, &item.RoleCode, &item.DisplayName, &item.RoleRank)
  187. if errors.Is(err, pgx.ErrNoRows) {
  188. return nil, nil
  189. }
  190. if err != nil {
  191. return nil, fmt.Errorf("scan ops role: %w", err)
  192. }
  193. return &item, nil
  194. }