types.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. package user
  2. import (
  3. "errors"
  4. "github.com/stripe/stripe-go/v74"
  5. "heckel.io/ntfy/log"
  6. "net/netip"
  7. "regexp"
  8. "strings"
  9. "time"
  10. )
  11. // User is a struct that represents a user
  12. type User struct {
  13. ID string
  14. Name string
  15. Hash string // password hash (bcrypt)
  16. Token string // Only set if token was used to log in
  17. Role Role
  18. Prefs *Prefs
  19. Tier *Tier
  20. Stats *Stats
  21. Billing *Billing
  22. SyncTopic string
  23. Deleted bool
  24. }
  25. // TierID returns the ID of the User.Tier, or an empty string if the user has no tier,
  26. // or if the user itself is nil.
  27. func (u *User) TierID() string {
  28. if u == nil || u.Tier == nil {
  29. return ""
  30. }
  31. return u.Tier.ID
  32. }
  33. // IsAdmin returns true if the user is an admin
  34. func (u *User) IsAdmin() bool {
  35. return u != nil && u.Role == RoleAdmin
  36. }
  37. // IsUser returns true if the user is a regular user, not an admin
  38. func (u *User) IsUser() bool {
  39. return u != nil && u.Role == RoleUser
  40. }
  41. // Auther is an interface for authentication and authorization
  42. type Auther interface {
  43. // Authenticate checks username and password and returns a user if correct. The method
  44. // returns in constant-ish time, regardless of whether the user exists or the password is
  45. // correct or incorrect.
  46. Authenticate(username, password string) (*User, error)
  47. // Authorize returns nil if the given user has access to the given topic using the desired
  48. // permission. The user param may be nil to signal an anonymous user.
  49. Authorize(user *User, topic string, perm Permission) error
  50. }
  51. // Token represents a user token, including expiry date
  52. type Token struct {
  53. Value string
  54. Label string
  55. LastAccess time.Time
  56. LastOrigin netip.Addr
  57. Expires time.Time
  58. }
  59. // TokenUpdate holds information about the last access time and origin IP address of a token
  60. type TokenUpdate struct {
  61. LastAccess time.Time
  62. LastOrigin netip.Addr
  63. }
  64. // Prefs represents a user's configuration settings
  65. type Prefs struct {
  66. Language *string `json:"language,omitempty"`
  67. Notification *NotificationPrefs `json:"notification,omitempty"`
  68. Subscriptions []*Subscription `json:"subscriptions,omitempty"`
  69. }
  70. // Tier represents a user's account type, including its account limits
  71. type Tier struct {
  72. ID string // Tier identifier (ti_...)
  73. Code string // Code of the tier
  74. Name string // Name of the tier
  75. MessageLimit int64 // Daily message limit
  76. MessageExpiryDuration time.Duration // Cache duration for messages
  77. EmailLimit int64 // Daily email limit
  78. ReservationLimit int64 // Number of topic reservations allowed by user
  79. AttachmentFileSizeLimit int64 // Max file size per file (bytes)
  80. AttachmentTotalSizeLimit int64 // Total file size for all files of this user (bytes)
  81. AttachmentExpiryDuration time.Duration // Duration after which attachments will be deleted
  82. AttachmentBandwidthLimit int64 // Daily bandwidth limit for the user
  83. StripePriceID string // Price ID for paid tiers (price_...)
  84. }
  85. // Context returns fields for the log
  86. func (t *Tier) Context() log.Context {
  87. return log.Context{
  88. "tier_id": t.ID,
  89. "tier_code": t.Code,
  90. "stripe_price_id": t.StripePriceID,
  91. }
  92. }
  93. // Subscription represents a user's topic subscription
  94. type Subscription struct {
  95. BaseURL string `json:"base_url"`
  96. Topic string `json:"topic"`
  97. DisplayName *string `json:"display_name"`
  98. }
  99. // Context returns fields for the log
  100. func (s *Subscription) Context() log.Context {
  101. return log.Context{
  102. "base_url": s.BaseURL,
  103. "topic": s.Topic,
  104. }
  105. }
  106. // NotificationPrefs represents the user's notification settings
  107. type NotificationPrefs struct {
  108. Sound *string `json:"sound,omitempty"`
  109. MinPriority *int `json:"min_priority,omitempty"`
  110. DeleteAfter *int `json:"delete_after,omitempty"`
  111. }
  112. // Stats is a struct holding daily user statistics
  113. type Stats struct {
  114. Messages int64
  115. Emails int64
  116. }
  117. // Billing is a struct holding a user's billing information
  118. type Billing struct {
  119. StripeCustomerID string
  120. StripeSubscriptionID string
  121. StripeSubscriptionStatus stripe.SubscriptionStatus
  122. StripeSubscriptionPaidUntil time.Time
  123. StripeSubscriptionCancelAt time.Time
  124. }
  125. // Grant is a struct that represents an access control entry to a topic by a user
  126. type Grant struct {
  127. TopicPattern string // May include wildcard (*)
  128. Allow Permission
  129. }
  130. // Reservation is a struct that represents the ownership over a topic by a user
  131. type Reservation struct {
  132. Topic string
  133. Owner Permission
  134. Everyone Permission
  135. }
  136. // Permission represents a read or write permission to a topic
  137. type Permission uint8
  138. // Permissions to a topic
  139. const (
  140. PermissionDenyAll Permission = iota
  141. PermissionRead
  142. PermissionWrite
  143. PermissionReadWrite // 3!
  144. )
  145. // NewPermission is a helper to create a Permission based on read/write bool values
  146. func NewPermission(read, write bool) Permission {
  147. p := uint8(0)
  148. if read {
  149. p |= uint8(PermissionRead)
  150. }
  151. if write {
  152. p |= uint8(PermissionWrite)
  153. }
  154. return Permission(p)
  155. }
  156. // ParsePermission parses the string representation and returns a Permission
  157. func ParsePermission(s string) (Permission, error) {
  158. switch strings.ToLower(s) {
  159. case "read-write", "rw":
  160. return NewPermission(true, true), nil
  161. case "read-only", "read", "ro":
  162. return NewPermission(true, false), nil
  163. case "write-only", "write", "wo":
  164. return NewPermission(false, true), nil
  165. case "deny-all", "deny", "none":
  166. return NewPermission(false, false), nil
  167. default:
  168. return NewPermission(false, false), errors.New("invalid permission")
  169. }
  170. }
  171. // IsRead returns true if readable
  172. func (p Permission) IsRead() bool {
  173. return p&PermissionRead != 0
  174. }
  175. // IsWrite returns true if writable
  176. func (p Permission) IsWrite() bool {
  177. return p&PermissionWrite != 0
  178. }
  179. // IsReadWrite returns true if readable and writable
  180. func (p Permission) IsReadWrite() bool {
  181. return p.IsRead() && p.IsWrite()
  182. }
  183. // String returns a string representation of the permission
  184. func (p Permission) String() string {
  185. if p.IsReadWrite() {
  186. return "read-write"
  187. } else if p.IsRead() {
  188. return "read-only"
  189. } else if p.IsWrite() {
  190. return "write-only"
  191. }
  192. return "deny-all"
  193. }
  194. // Role represents a user's role, either admin or regular user
  195. type Role string
  196. // User roles
  197. const (
  198. RoleAdmin = Role("admin") // Some queries have these values hardcoded!
  199. RoleUser = Role("user")
  200. RoleAnonymous = Role("anonymous")
  201. )
  202. // Everyone is a special username representing anonymous users
  203. const (
  204. Everyone = "*"
  205. everyoneID = "u_everyone"
  206. )
  207. var (
  208. allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
  209. allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
  210. allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
  211. allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
  212. )
  213. // AllowedRole returns true if the given role can be used for new users
  214. func AllowedRole(role Role) bool {
  215. return role == RoleUser || role == RoleAdmin
  216. }
  217. // AllowedUsername returns true if the given username is valid
  218. func AllowedUsername(username string) bool {
  219. return allowedUsernameRegex.MatchString(username)
  220. }
  221. // AllowedTopic returns true if the given topic name is valid
  222. func AllowedTopic(topic string) bool {
  223. return allowedTopicRegex.MatchString(topic)
  224. }
  225. // AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
  226. func AllowedTopicPattern(topic string) bool {
  227. return allowedTopicPatternRegex.MatchString(topic)
  228. }
  229. // AllowedTier returns true if the given tier name is valid
  230. func AllowedTier(tier string) bool {
  231. return allowedTierRegex.MatchString(tier)
  232. }
  233. // Error constants used by the package
  234. var (
  235. ErrUnauthenticated = errors.New("unauthenticated")
  236. ErrUnauthorized = errors.New("unauthorized")
  237. ErrInvalidArgument = errors.New("invalid argument")
  238. ErrUserNotFound = errors.New("user not found")
  239. ErrTierNotFound = errors.New("tier not found")
  240. ErrTokenNotFound = errors.New("token not found")
  241. ErrTooManyReservations = errors.New("new tier has lower reservation limit")
  242. )