mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 13:02:24 +01:00 
			
		
		
		
	Restructure limits
This commit is contained in:
		
							parent
							
								
									6598ce2fe4
								
							
						
					
					
						commit
						84785b7a60
					
				
					 7 changed files with 105 additions and 74 deletions
				
			
		auth
server
web/src/components
|  | @ -84,7 +84,7 @@ const ( | |||
| type Plan struct { | ||||
| 	Code                     string `json:"name"` | ||||
| 	Upgradable               bool   `json:"upgradable"` | ||||
| 	MessageLimit             int64  `json:"messages_limit"` | ||||
| 	MessagesLimit            int64  `json:"messages_limit"` | ||||
| 	EmailsLimit              int64  `json:"emails_limit"` | ||||
| 	AttachmentFileSizeLimit  int64  `json:"attachment_file_size_limit"` | ||||
| 	AttachmentTotalSizeLimit int64  `json:"attachment_total_size_limit"` | ||||
|  |  | |||
|  | @ -110,7 +110,7 @@ const ( | |||
| 	selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` | ||||
| ) | ||||
| 
 | ||||
| // SQLiteAuthManager is an implementation of Manager and Manager. It stores users and access control list | ||||
| // SQLiteAuthManager is an implementation of Manager. It stores users and access control list | ||||
| // in a SQLite database. | ||||
| type SQLiteAuthManager struct { | ||||
| 	db           *sql.DB | ||||
|  | @ -355,7 +355,7 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | |||
| 		user.Plan = &Plan{ | ||||
| 			Code:                     planCode.String, | ||||
| 			Upgradable:               true, // FIXME | ||||
| 			MessageLimit:             messagesLimit.Int64, | ||||
| 			MessagesLimit:            messagesLimit.Int64, | ||||
| 			EmailsLimit:              emailsLimit.Int64, | ||||
| 			AttachmentFileSizeLimit:  attachmentFileSizeLimit.Int64, | ||||
| 			AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, | ||||
|  |  | |||
|  | @ -773,7 +773,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, | |||
| 	contentLengthStr := r.Header.Get("Content-Length") | ||||
| 	if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below | ||||
| 		contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) | ||||
| 		if err == nil && (contentLength > visitorStats.VisitorAttachmentBytesRemaining || contentLength > s.config.AttachmentFileSizeLimit) { | ||||
| 		if err == nil && (contentLength > visitorStats.AttachmentTotalSizeRemaining || contentLength > s.config.AttachmentFileSizeLimit) { | ||||
| 			return errHTTPEntityTooLargeAttachmentTooLarge | ||||
| 		} | ||||
| 	} | ||||
|  | @ -791,7 +791,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, | |||
| 	if m.Message == "" { | ||||
| 		m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) | ||||
| 	} | ||||
| 	m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.VisitorAttachmentBytesRemaining)) | ||||
| 	m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.AttachmentTotalSizeRemaining)) | ||||
| 	if err == util.ErrLimitReached { | ||||
| 		return errHTTPEntityTooLargeAttachmentTooLarge | ||||
| 	} else if err != nil { | ||||
|  |  | |||
|  | @ -40,7 +40,21 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis | |||
| 		return err | ||||
| 	} | ||||
| 	response := &apiAccountSettingsResponse{ | ||||
| 		Usage: &apiAccountStats{}, | ||||
| 		Stats: &apiAccountStats{ | ||||
| 			Messages:                     stats.Messages, | ||||
| 			MessagesRemaining:            stats.MessagesRemaining, | ||||
| 			Emails:                       stats.Emails, | ||||
| 			EmailsRemaining:              stats.EmailsRemaining, | ||||
| 			AttachmentTotalSize:          stats.AttachmentTotalSize, | ||||
| 			AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining, | ||||
| 		}, | ||||
| 		Limits: &apiAccountLimits{ | ||||
| 			Basis:               stats.Basis, | ||||
| 			Messages:            stats.MessagesLimit, | ||||
| 			Emails:              stats.EmailsLimit, | ||||
| 			AttachmentTotalSize: stats.AttachmentTotalSizeLimit, | ||||
| 			AttachmentFileSize:  stats.AttachmentFileSizeLimit, | ||||
| 		}, | ||||
| 	} | ||||
| 	if v.user != nil { | ||||
| 		response.Username = v.user.Name | ||||
|  | @ -57,62 +71,31 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis | |||
| 			} | ||||
| 		} | ||||
| 		if v.user.Plan != nil { | ||||
| 			response.Usage.Basis = "account" | ||||
| 			response.Plan = &apiAccountSettingsPlan{ | ||||
| 			response.Plan = &apiAccountPlan{ | ||||
| 				Code:       v.user.Plan.Code, | ||||
| 				Upgradable: v.user.Plan.Upgradable, | ||||
| 			} | ||||
| 			response.Limits = &apiAccountLimits{ | ||||
| 				MessagesLimit:            v.user.Plan.MessageLimit, | ||||
| 				EmailsLimit:              v.user.Plan.EmailsLimit, | ||||
| 				AttachmentFileSizeLimit:  v.user.Plan.AttachmentFileSizeLimit, | ||||
| 				AttachmentTotalSizeLimit: v.user.Plan.AttachmentTotalSizeLimit, | ||||
| 			} | ||||
| 		} else { | ||||
| 			if v.user.Role == auth.RoleAdmin { | ||||
| 				response.Usage.Basis = "account" | ||||
| 				response.Plan = &apiAccountSettingsPlan{ | ||||
| 				response.Plan = &apiAccountPlan{ | ||||
| 					Code:       string(auth.PlanUnlimited), | ||||
| 					Upgradable: false, | ||||
| 				} | ||||
| 				response.Limits = &apiAccountLimits{ | ||||
| 					MessagesLimit:            0, | ||||
| 					EmailsLimit:              0, | ||||
| 					AttachmentFileSizeLimit:  0, | ||||
| 					AttachmentTotalSizeLimit: 0, | ||||
| 				} | ||||
| 			} else { | ||||
| 				response.Usage.Basis = "ip" | ||||
| 				response.Plan = &apiAccountSettingsPlan{ | ||||
| 				response.Plan = &apiAccountPlan{ | ||||
| 					Code:       string(auth.PlanDefault), | ||||
| 					Upgradable: true, | ||||
| 				} | ||||
| 				response.Limits = &apiAccountLimits{ | ||||
| 					MessagesLimit:            int64(s.config.VisitorRequestLimitBurst), | ||||
| 					EmailsLimit:              int64(s.config.VisitorEmailLimitBurst), | ||||
| 					AttachmentFileSizeLimit:  s.config.AttachmentFileSizeLimit, | ||||
| 					AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		response.Username = auth.Everyone | ||||
| 		response.Role = string(auth.RoleAnonymous) | ||||
| 		response.Usage.Basis = "ip" | ||||
| 		response.Plan = &apiAccountSettingsPlan{ | ||||
| 		response.Plan = &apiAccountPlan{ | ||||
| 			Code:       string(auth.PlanNone), | ||||
| 			Upgradable: true, | ||||
| 		} | ||||
| 		response.Limits = &apiAccountLimits{ | ||||
| 			MessagesLimit:            int64(s.config.VisitorRequestLimitBurst), | ||||
| 			EmailsLimit:              int64(s.config.VisitorEmailLimitBurst), | ||||
| 			AttachmentFileSizeLimit:  s.config.AttachmentFileSizeLimit, | ||||
| 			AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, | ||||
| 		} | ||||
| 	} | ||||
| 	response.Usage.Messages = stats.Messages | ||||
| 	response.Usage.Emails = stats.Emails | ||||
| 	response.Usage.AttachmentsSize = stats.AttachmentBytes | ||||
| 	if err := json.NewEncoder(w).Encode(response); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -224,23 +224,26 @@ type apiAccountTokenResponse struct { | |||
| 	Token string `json:"token"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountSettingsPlan struct { | ||||
| type apiAccountPlan struct { | ||||
| 	Code       string `json:"code"` | ||||
| 	Upgradable bool   `json:"upgradable"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountLimits struct { | ||||
| 	MessagesLimit            int64 `json:"messages"` | ||||
| 	EmailsLimit              int64 `json:"emails"` | ||||
| 	AttachmentFileSizeLimit  int64 `json:"attachment_file_size"` | ||||
| 	AttachmentTotalSizeLimit int64 `json:"attachment_total_size"` | ||||
| 	Basis               string `json:"basis"` // "ip", "role" or "plan" | ||||
| 	Messages            int64  `json:"messages"` | ||||
| 	Emails              int64  `json:"emails"` | ||||
| 	AttachmentTotalSize int64  `json:"attachment_total_size"` | ||||
| 	AttachmentFileSize  int64  `json:"attachment_file_size"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountStats struct { | ||||
| 	Basis           string `json:"basis"` // "ip" or "account" | ||||
| 	Messages        int64  `json:"messages"` | ||||
| 	Emails          int64  `json:"emails"` | ||||
| 	AttachmentsSize int64  `json:"attachments_size"` | ||||
| 	Messages                     int64 `json:"messages"` | ||||
| 	MessagesRemaining            int64 `json:"messages_remaining"` | ||||
| 	Emails                       int64 `json:"emails"` | ||||
| 	EmailsRemaining              int64 `json:"emails_remaining"` | ||||
| 	AttachmentTotalSize          int64 `json:"attachment_total_size"` | ||||
| 	AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountSettingsResponse struct { | ||||
|  | @ -249,7 +252,7 @@ type apiAccountSettingsResponse struct { | |||
| 	Language      string                      `json:"language,omitempty"` | ||||
| 	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"` | ||||
| 	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"` | ||||
| 	Plan          *apiAccountSettingsPlan     `json:"plan,omitempty"` | ||||
| 	Plan          *apiAccountPlan             `json:"plan,omitempty"` | ||||
| 	Limits        *apiAccountLimits           `json:"limits,omitempty"` | ||||
| 	Usage         *apiAccountStats            `json:"usage,omitempty"` | ||||
| 	Stats         *apiAccountStats            `json:"stats,omitempty"` | ||||
| } | ||||
|  |  | |||
|  | @ -40,17 +40,27 @@ type visitor struct { | |||
| } | ||||
| 
 | ||||
| type visitorStats struct { | ||||
| 	Messages        int64 | ||||
| 	Emails          int64 | ||||
| 	AttachmentBytes int64 | ||||
| 	Basis                        string // "ip", "role" or "plan" | ||||
| 	Messages                     int64 | ||||
| 	MessagesLimit                int64 | ||||
| 	MessagesRemaining            int64 | ||||
| 	Emails                       int64 | ||||
| 	EmailsLimit                  int64 | ||||
| 	EmailsRemaining              int64 | ||||
| 	AttachmentTotalSize          int64 | ||||
| 	AttachmentTotalSizeLimit     int64 | ||||
| 	AttachmentTotalSizeRemaining int64 | ||||
| 	AttachmentFileSizeLimit      int64 | ||||
| } | ||||
| 
 | ||||
| func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor { | ||||
| 	var requestLimiter *rate.Limiter | ||||
| 	var requestLimiter, emailsLimiter *rate.Limiter | ||||
| 	if user != nil && user.Plan != nil { | ||||
| 		requestLimiter = rate.NewLimiter(rate.Limit(user.Plan.MessageLimit)*rate.Every(24*time.Hour), conf.VisitorRequestLimitBurst) | ||||
| 		requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst) | ||||
| 		emailsLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.EmailsLimit), conf.VisitorEmailLimitBurst) | ||||
| 	} else { | ||||
| 		requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst) | ||||
| 		emailsLimiter = rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst) | ||||
| 	} | ||||
| 	return &visitor{ | ||||
| 		config:              conf, | ||||
|  | @ -60,7 +70,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *a | |||
| 		messages:            0, // TODO | ||||
| 		emails:              0, // TODO | ||||
| 		requestLimiter:      requestLimiter, | ||||
| 		emailsLimiter:       rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), | ||||
| 		emailsLimiter:       emailsLimiter, | ||||
| 		subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), | ||||
| 		bandwidthLimiter:    util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), | ||||
| 		firebase:            time.Unix(0, 0), | ||||
|  | @ -147,9 +157,46 @@ func (v *visitor) Stats() (*visitorStats, error) { | |||
| 	} | ||||
| 	v.mu.Lock() | ||||
| 	defer v.mu.Unlock() | ||||
| 	return &visitorStats{ | ||||
| 		Messages:        v.messages, | ||||
| 		Emails:          v.emails, | ||||
| 		AttachmentBytes: attachmentsBytesUsed, | ||||
| 	}, nil | ||||
| 	stats := &visitorStats{} | ||||
| 	if v.user != nil && v.user.Role == auth.RoleAdmin { | ||||
| 		stats.Basis = "role" | ||||
| 		stats.MessagesLimit = 0 | ||||
| 		stats.EmailsLimit = 0 | ||||
| 		stats.AttachmentTotalSizeLimit = 0 | ||||
| 		stats.AttachmentFileSizeLimit = 0 | ||||
| 	} else if v.user != nil && v.user.Plan != nil { | ||||
| 		stats.Basis = "plan" | ||||
| 		stats.MessagesLimit = v.user.Plan.MessagesLimit | ||||
| 		stats.EmailsLimit = v.user.Plan.EmailsLimit | ||||
| 		stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit | ||||
| 		stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit | ||||
| 	} else { | ||||
| 		stats.Basis = "ip" | ||||
| 		stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) | ||||
| 		stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) | ||||
| 		stats.AttachmentTotalSizeLimit = v.config.AttachmentTotalSizeLimit | ||||
| 		stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit | ||||
| 	} | ||||
| 	stats.Messages = v.messages | ||||
| 	stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.MessagesLimit) | ||||
| 	stats.Emails = v.emails | ||||
| 	stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.EmailsRemaining) | ||||
| 	stats.AttachmentTotalSize = attachmentsBytesUsed | ||||
| 	stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize) | ||||
| 	return stats, nil | ||||
| } | ||||
| 
 | ||||
| func zeroIfNegative(value int64) int64 { | ||||
| 	if value < 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
| 
 | ||||
| func replenishDurationToDailyLimit(duration time.Duration) int64 { | ||||
| 	return int64(24 * time.Hour / duration) | ||||
| } | ||||
| 
 | ||||
| func dailyLimitToRate(limit int64) rate.Limit { | ||||
| 	return rate.Limit(limit) * rate.Every(24*time.Hour) | ||||
| } | ||||
|  |  | |||
|  | @ -60,8 +60,6 @@ const Stats = () => { | |||
|         return <></>; // TODO loading | ||||
|     } | ||||
|     const accountType = account.plan.code ?? "none"; | ||||
|     const limits = account.limits; | ||||
|     const usage = account.usage; | ||||
|     const normalize = (value, max) => (value / max * 100); | ||||
|     return ( | ||||
|         <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}> | ||||
|  | @ -78,24 +76,24 @@ const Stats = () => { | |||
|                 </Pref> | ||||
|                 <Pref labelId={"messages"} title={t("Published messages")}> | ||||
|                     <div> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{usage.messages}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{limits.messages > 0 ? t("of {{limit}}", { limit: limits.messages }) : t("Unlimited")}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("of {{limit}}", { limit: account.limits.messages }) : t("Unlimited")}</Typography> | ||||
|                     </div> | ||||
|                     <LinearProgress variant="determinate" value={limits.messages > 0 ? normalize(usage.messages, limits.messages) : 100} /> | ||||
|                     <LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} /> | ||||
|                 </Pref> | ||||
|                 <Pref labelId={"emails"} title={t("Emails sent")}> | ||||
|                     <div> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{usage.emails}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{limits.emails > 0 ? t("of {{limit}}", { limit: limits.emails }) : t("Unlimited")}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("of {{limit}}", { limit: account.limits.emails }) : t("Unlimited")}</Typography> | ||||
|                     </div> | ||||
|                     <LinearProgress variant="determinate" value={limits.emails > 0 ? normalize(usage.emails, limits.emails) : 100} /> | ||||
|                     <LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} /> | ||||
|                 </Pref> | ||||
|                 <Pref labelId={"attachments"} title={t("Attachment storage")}> | ||||
|                     <div> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{formatBytes(usage.attachments_size)}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(limits.attachment_total_size) }) : t("Unlimited")}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography> | ||||
|                         <Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")}</Typography> | ||||
|                     </div> | ||||
|                     <LinearProgress variant="determinate" value={limits.attachment_total_size > 0 ? normalize(usage.attachments_size, limits.attachment_total_size) : 100} /> | ||||
|                     <LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} /> | ||||
|                 </Pref> | ||||
|             </PrefGroup> | ||||
|         </Card> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 binwiederhier
						binwiederhier