api_token.go 3.6 KB

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