123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- package user
- import (
- "errors"
- "github.com/stripe/stripe-go/v74"
- "heckel.io/ntfy/log"
- "net/netip"
- "regexp"
- "strings"
- "time"
- )
- // User is a struct that represents a user
- type User struct {
- ID string
- Name string
- Hash string // password hash (bcrypt)
- Token string // Only set if token was used to log in
- Role Role
- Prefs *Prefs
- Tier *Tier
- Stats *Stats
- Billing *Billing
- SyncTopic string
- Deleted bool
- }
- // TierID returns the ID of the User.Tier, or an empty string if the user has no tier,
- // or if the user itself is nil.
- func (u *User) TierID() string {
- if u == nil || u.Tier == nil {
- return ""
- }
- return u.Tier.ID
- }
- // IsAdmin returns true if the user is an admin
- func (u *User) IsAdmin() bool {
- return u != nil && u.Role == RoleAdmin
- }
- // IsUser returns true if the user is a regular user, not an admin
- func (u *User) IsUser() bool {
- return u != nil && u.Role == RoleUser
- }
- // Auther is an interface for authentication and authorization
- type Auther interface {
- // Authenticate checks username and password and returns a user if correct. The method
- // returns in constant-ish time, regardless of whether the user exists or the password is
- // correct or incorrect.
- Authenticate(username, password string) (*User, error)
- // Authorize returns nil if the given user has access to the given topic using the desired
- // permission. The user param may be nil to signal an anonymous user.
- Authorize(user *User, topic string, perm Permission) error
- }
- // Token represents a user token, including expiry date
- type Token struct {
- Value string
- Label string
- LastAccess time.Time
- LastOrigin netip.Addr
- Expires time.Time
- }
- // TokenUpdate holds information about the last access time and origin IP address of a token
- type TokenUpdate struct {
- LastAccess time.Time
- LastOrigin netip.Addr
- }
- // Prefs represents a user's configuration settings
- type Prefs struct {
- Language *string `json:"language,omitempty"`
- Notification *NotificationPrefs `json:"notification,omitempty"`
- Subscriptions []*Subscription `json:"subscriptions,omitempty"`
- }
- // Tier represents a user's account type, including its account limits
- type Tier struct {
- ID string // Tier identifier (ti_...)
- Code string // Code of the tier
- Name string // Name of the tier
- MessageLimit int64 // Daily message limit
- MessageExpiryDuration time.Duration // Cache duration for messages
- EmailLimit int64 // Daily email limit
- ReservationLimit int64 // Number of topic reservations allowed by user
- AttachmentFileSizeLimit int64 // Max file size per file (bytes)
- AttachmentTotalSizeLimit int64 // Total file size for all files of this user (bytes)
- AttachmentExpiryDuration time.Duration // Duration after which attachments will be deleted
- AttachmentBandwidthLimit int64 // Daily bandwidth limit for the user
- StripeMonthlyPriceID string // Monthly price ID for paid tiers (price_...)
- StripeYearlyPriceID string // Yearly price ID for paid tiers (price_...)
- }
- // Context returns fields for the log
- func (t *Tier) Context() log.Context {
- return log.Context{
- "tier_id": t.ID,
- "tier_code": t.Code,
- "stripe_monthly_price_id": t.StripeMonthlyPriceID,
- "stripe_yearly_price_id": t.StripeYearlyPriceID,
- }
- }
- // Subscription represents a user's topic subscription
- type Subscription struct {
- BaseURL string `json:"base_url"`
- Topic string `json:"topic"`
- DisplayName *string `json:"display_name"`
- }
- // Context returns fields for the log
- func (s *Subscription) Context() log.Context {
- return log.Context{
- "base_url": s.BaseURL,
- "topic": s.Topic,
- }
- }
- // NotificationPrefs represents the user's notification settings
- type NotificationPrefs struct {
- Sound *string `json:"sound,omitempty"`
- MinPriority *int `json:"min_priority,omitempty"`
- DeleteAfter *int `json:"delete_after,omitempty"`
- }
- // Stats is a struct holding daily user statistics
- type Stats struct {
- Messages int64
- Emails int64
- }
- // Billing is a struct holding a user's billing information
- type Billing struct {
- StripeCustomerID string
- StripeSubscriptionID string
- StripeSubscriptionStatus stripe.SubscriptionStatus
- StripeSubscriptionInterval stripe.PriceRecurringInterval
- StripeSubscriptionPaidUntil time.Time
- StripeSubscriptionCancelAt time.Time
- }
- // Grant is a struct that represents an access control entry to a topic by a user
- type Grant struct {
- TopicPattern string // May include wildcard (*)
- Allow Permission
- }
- // Reservation is a struct that represents the ownership over a topic by a user
- type Reservation struct {
- Topic string
- Owner Permission
- Everyone Permission
- }
- // Permission represents a read or write permission to a topic
- type Permission uint8
- // Permissions to a topic
- const (
- PermissionDenyAll Permission = iota
- PermissionRead
- PermissionWrite
- PermissionReadWrite // 3!
- )
- // NewPermission is a helper to create a Permission based on read/write bool values
- func NewPermission(read, write bool) Permission {
- p := uint8(0)
- if read {
- p |= uint8(PermissionRead)
- }
- if write {
- p |= uint8(PermissionWrite)
- }
- return Permission(p)
- }
- // ParsePermission parses the string representation and returns a Permission
- func ParsePermission(s string) (Permission, error) {
- switch strings.ToLower(s) {
- case "read-write", "rw":
- return NewPermission(true, true), nil
- case "read-only", "read", "ro":
- return NewPermission(true, false), nil
- case "write-only", "write", "wo":
- return NewPermission(false, true), nil
- case "deny-all", "deny", "none":
- return NewPermission(false, false), nil
- default:
- return NewPermission(false, false), errors.New("invalid permission")
- }
- }
- // IsRead returns true if readable
- func (p Permission) IsRead() bool {
- return p&PermissionRead != 0
- }
- // IsWrite returns true if writable
- func (p Permission) IsWrite() bool {
- return p&PermissionWrite != 0
- }
- // IsReadWrite returns true if readable and writable
- func (p Permission) IsReadWrite() bool {
- return p.IsRead() && p.IsWrite()
- }
- // String returns a string representation of the permission
- func (p Permission) String() string {
- if p.IsReadWrite() {
- return "read-write"
- } else if p.IsRead() {
- return "read-only"
- } else if p.IsWrite() {
- return "write-only"
- }
- return "deny-all"
- }
- // Role represents a user's role, either admin or regular user
- type Role string
- // User roles
- const (
- RoleAdmin = Role("admin") // Some queries have these values hardcoded!
- RoleUser = Role("user")
- RoleAnonymous = Role("anonymous")
- )
- // Everyone is a special username representing anonymous users
- const (
- Everyone = "*"
- everyoneID = "u_everyone"
- )
- var (
- allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
- allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
- allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
- allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
- )
- // AllowedRole returns true if the given role can be used for new users
- func AllowedRole(role Role) bool {
- return role == RoleUser || role == RoleAdmin
- }
- // AllowedUsername returns true if the given username is valid
- func AllowedUsername(username string) bool {
- return allowedUsernameRegex.MatchString(username)
- }
- // AllowedTopic returns true if the given topic name is valid
- func AllowedTopic(topic string) bool {
- return allowedTopicRegex.MatchString(topic)
- }
- // AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
- func AllowedTopicPattern(topic string) bool {
- return allowedTopicPatternRegex.MatchString(topic)
- }
- // AllowedTier returns true if the given tier name is valid
- func AllowedTier(tier string) bool {
- return allowedTierRegex.MatchString(tier)
- }
- // Error constants used by the package
- var (
- ErrUnauthenticated = errors.New("unauthenticated")
- ErrUnauthorized = errors.New("unauthorized")
- ErrInvalidArgument = errors.New("invalid argument")
- ErrUserNotFound = errors.New("user not found")
- ErrTierNotFound = errors.New("tier not found")
- ErrTokenNotFound = errors.New("token not found")
- ErrTooManyReservations = errors.New("new tier has lower reservation limit")
- )
|