api_token.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package service
  2. import (
  3. "crypto/subtle"
  4. "errors"
  5. "strings"
  6. "github.com/mhsanaei/3x-ui/v3/database"
  7. "github.com/mhsanaei/3x-ui/v3/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/util/common"
  9. "github.com/mhsanaei/3x-ui/v3/util/crypto"
  10. "github.com/mhsanaei/3x-ui/v3/util/random"
  11. )
  12. type ApiTokenService struct{}
  13. const apiTokenLength = 48
  14. type ApiTokenView struct {
  15. Id int `json:"id"`
  16. Name string `json:"name"`
  17. Token string `json:"token,omitempty"`
  18. Enabled bool `json:"enabled"`
  19. CreatedAt int64 `json:"createdAt"`
  20. }
  21. // toView builds the metadata view returned by List. It never carries the
  22. // token value: only a SHA-256 hash is stored, and the plaintext is shown
  23. // exactly once at creation time.
  24. func toView(t *model.ApiToken) *ApiTokenView {
  25. return &ApiTokenView{
  26. Id: t.Id,
  27. Name: t.Name,
  28. Enabled: t.Enabled,
  29. CreatedAt: t.CreatedAt,
  30. }
  31. }
  32. func (s *ApiTokenService) List() ([]*ApiTokenView, error) {
  33. db := database.GetDB()
  34. var rows []*model.ApiToken
  35. if err := db.Model(model.ApiToken{}).Order("id asc").Find(&rows).Error; err != nil {
  36. return nil, err
  37. }
  38. out := make([]*ApiTokenView, 0, len(rows))
  39. for _, r := range rows {
  40. out = append(out, toView(r))
  41. }
  42. return out, nil
  43. }
  44. func (s *ApiTokenService) Create(name string) (*ApiTokenView, error) {
  45. name = strings.TrimSpace(name)
  46. if name == "" {
  47. return nil, common.NewError("token name is required")
  48. }
  49. if len(name) > 64 {
  50. return nil, common.NewError("token name must be 64 characters or fewer")
  51. }
  52. db := database.GetDB()
  53. var count int64
  54. if err := db.Model(model.ApiToken{}).Where("name = ?", name).Count(&count).Error; err != nil {
  55. return nil, err
  56. }
  57. if count > 0 {
  58. return nil, common.NewError("a token with that name already exists")
  59. }
  60. plaintext := random.Seq(apiTokenLength)
  61. row := &model.ApiToken{
  62. Name: name,
  63. Token: crypto.HashTokenSHA256(plaintext),
  64. Enabled: true,
  65. }
  66. if err := db.Create(row).Error; err != nil {
  67. return nil, err
  68. }
  69. view := toView(row)
  70. view.Token = plaintext
  71. return view, nil
  72. }
  73. func (s *ApiTokenService) Delete(id int) error {
  74. if id <= 0 {
  75. return common.NewError("invalid token id")
  76. }
  77. db := database.GetDB()
  78. return db.Where("id = ?", id).Delete(model.ApiToken{}).Error
  79. }
  80. func (s *ApiTokenService) SetEnabled(id int, enabled bool) error {
  81. if id <= 0 {
  82. return common.NewError("invalid token id")
  83. }
  84. db := database.GetDB()
  85. res := db.Model(model.ApiToken{}).Where("id = ?", id).Update("enabled", enabled)
  86. if res.Error != nil {
  87. return res.Error
  88. }
  89. if res.RowsAffected == 0 {
  90. return errors.New("token not found")
  91. }
  92. return nil
  93. }
  94. // Match returns true when the presented bearer token matches any enabled
  95. // row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented
  96. // value is hashed before a constant-time compare per row keeps a remote
  97. // attacker from timing the comparison byte-by-byte.
  98. func (s *ApiTokenService) Match(presented string) bool {
  99. if presented == "" {
  100. return false
  101. }
  102. db := database.GetDB()
  103. var rows []*model.ApiToken
  104. if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
  105. return false
  106. }
  107. presentedHash := []byte(crypto.HashTokenSHA256(presented))
  108. matched := false
  109. for _, r := range rows {
  110. if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
  111. matched = true
  112. }
  113. }
  114. return matched
  115. }