ntfy/user/types.go

283 lines
8.4 KiB
Go
Raw Normal View History

2022-12-26 06:29:55 +03:00
package user
import (
"errors"
2023-01-16 07:29:46 +03:00
"github.com/stripe/stripe-go/v74"
2023-02-11 18:49:37 +03:00
"heckel.io/ntfy/log"
2023-01-29 04:29:06 +03:00
"net/netip"
2022-12-26 06:29:55 +03:00
"regexp"
2023-02-12 06:14:09 +03:00
"strings"
2022-12-28 21:46:18 +03:00
"time"
2022-12-26 06:29:55 +03:00
)
// User is a struct that represents a user
type User struct {
ID string
2023-01-10 05:53:21 +03:00
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
2023-01-14 14:43:44 +03:00
Billing *Billing
2023-01-10 05:53:21 +03:00
SyncTopic string
2023-01-23 06:21:30 +03:00
Deleted bool
2022-12-26 06:29:55 +03:00
}
// 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
}
2023-02-10 05:51:12 +03:00
// IsAdmin returns true if the user is an admin
func (u *User) IsAdmin() bool {
2023-02-02 23:19:37 +03:00
return u != nil && u.Role == RoleAdmin
}
2023-02-10 05:51:12 +03:00
// IsUser returns true if the user is a regular user, not an admin
func (u *User) IsUser() bool {
2023-02-09 06:57:10 +03:00
return u != nil && u.Role == RoleUser
2023-02-02 23:19:37 +03:00
}
2022-12-29 06:16:11 +03:00
// 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
2022-12-26 06:29:55 +03:00
type Token struct {
2023-01-29 04:29:06 +03:00
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
2022-12-26 06:29:55 +03:00
}
2022-12-29 06:16:11 +03:00
// Prefs represents a user's configuration settings
2022-12-26 06:29:55 +03:00
type Prefs struct {
Language *string `json:"language,omitempty"`
2022-12-26 06:29:55 +03:00
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
2023-01-27 06:57:18 +03:00
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
2023-02-22 06:44:30 +03:00
StripeMonthlyPriceID string // Monthly price ID for paid tiers (price_...)
StripeYearlyPriceID string // Yearly price ID for paid tiers (price_...)
2022-12-26 06:29:55 +03:00
}
2023-02-11 22:13:10 +03:00
// Context returns fields for the log
2023-02-11 18:49:37 +03:00
func (t *Tier) Context() log.Context {
return log.Context{
2023-02-22 06:44:30 +03:00
"tier_id": t.ID,
"tier_code": t.Code,
"stripe_monthly_price_id": t.StripeMonthlyPriceID,
"stripe_yearly_price_id": t.StripeYearlyPriceID,
2023-02-11 18:49:37 +03:00
}
}
2022-12-29 06:16:11 +03:00
// Subscription represents a user's topic subscription
2022-12-26 06:29:55 +03:00
type Subscription struct {
BaseURL string `json:"base_url"`
Topic string `json:"topic"`
DisplayName *string `json:"display_name"`
2022-12-26 06:29:55 +03:00
}
2023-02-12 22:09:44 +03:00
// Context returns fields for the log
func (s *Subscription) Context() log.Context {
return log.Context{
"base_url": s.BaseURL,
"topic": s.Topic,
}
}
2022-12-29 06:16:11 +03:00
// NotificationPrefs represents the user's notification settings
2022-12-26 06:29:55 +03:00
type NotificationPrefs struct {
Sound *string `json:"sound,omitempty"`
MinPriority *int `json:"min_priority,omitempty"`
DeleteAfter *int `json:"delete_after,omitempty"`
2022-12-26 06:29:55 +03:00
}
2022-12-29 06:16:11 +03:00
// Stats is a struct holding daily user statistics
2022-12-26 06:29:55 +03:00
type Stats struct {
Messages int64
Emails int64
}
2023-01-14 14:43:44 +03:00
// Billing is a struct holding a user's billing information
type Billing struct {
2023-01-16 07:29:46 +03:00
StripeCustomerID string
StripeSubscriptionID string
StripeSubscriptionStatus stripe.SubscriptionStatus
2023-02-22 06:44:30 +03:00
StripeSubscriptionInterval stripe.PriceRecurringInterval
2023-01-16 07:29:46 +03:00
StripeSubscriptionPaidUntil time.Time
2023-01-16 18:35:12 +03:00
StripeSubscriptionCancelAt time.Time
2023-01-14 14:43:44 +03:00
}
// Grant is a struct that represents an access control entry to a topic by a user
2022-12-26 06:29:55 +03:00
type Grant struct {
TopicPattern string // May include wildcard (*)
Allow Permission
2023-01-03 04:08:37 +03:00
}
// Reservation is a struct that represents the ownership over a topic by a user
type Reservation struct {
Topic string
Owner Permission
Everyone Permission
2022-12-26 06:29:55 +03:00
}
// Permission represents a read or write permission to a topic
type Permission uint8
2022-12-26 06:29:55 +03:00
// Permissions to a topic
const (
PermissionDenyAll Permission = iota
PermissionRead
PermissionWrite
PermissionReadWrite // 3!
2022-12-26 06:29:55 +03:00
)
2023-01-03 06:28:43 +03:00
// 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)
}
2023-01-03 06:28:43 +03:00
// ParsePermission parses the string representation and returns a Permission
func ParsePermission(s string) (Permission, error) {
2023-02-12 06:14:09 +03:00
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")
}
}
2023-01-03 06:28:43 +03:00
// IsRead returns true if readable
func (p Permission) IsRead() bool {
return p&PermissionRead != 0
}
2023-01-03 06:28:43 +03:00
// IsWrite returns true if writable
func (p Permission) IsWrite() bool {
return p&PermissionWrite != 0
}
2023-01-03 06:28:43 +03:00
// IsReadWrite returns true if readable and writable
func (p Permission) IsReadWrite() bool {
return p.IsRead() && p.IsWrite()
}
2023-01-03 06:28:43 +03:00
// 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"
}
2022-12-26 06:29:55 +03:00
// Role represents a user's role, either admin or regular user
type Role string
// User roles
const (
2022-12-28 21:28:28 +03:00
RoleAdmin = Role("admin") // Some queries have these values hardcoded!
2022-12-26 06:29:55 +03:00
RoleUser = Role("user")
RoleAnonymous = Role("anonymous")
)
// Everyone is a special username representing anonymous users
const (
2023-01-23 06:21:30 +03:00
Everyone = "*"
everyoneID = "u_everyone"
2022-12-26 06:29:55 +03:00
)
var (
allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
2023-01-01 23:21:43 +03:00
allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
2022-12-26 06:29:55 +03:00
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
2023-01-09 23:40:46 +03:00
allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
2022-12-26 06:29:55 +03:00
)
// 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)
}
2023-01-01 23:21:43 +03:00
// AllowedTopic returns true if the given topic name is valid
2023-01-09 23:40:46 +03:00
func AllowedTopic(topic string) bool {
return allowedTopicRegex.MatchString(topic)
2023-01-01 23:21:43 +03:00
}
2022-12-26 06:29:55 +03:00
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
2023-01-09 23:40:46 +03:00
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)
2022-12-26 06:29:55 +03:00
}
// 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")
2023-01-28 07:10:59 +03:00
ErrTokenNotFound = errors.New("token not found")
ErrTooManyReservations = errors.New("new tier has lower reservation limit")
2023-01-16 07:29:46 +03:00
)