Limits
This commit is contained in:
		
							parent
							
								
									42e46a7c22
								
							
						
					
					
						commit
						6598ce2fe4
					
				
					 10 changed files with 157 additions and 160 deletions
				
			
		|  | @ -84,9 +84,10 @@ const ( | ||||||
| type Plan struct { | type Plan struct { | ||||||
| 	Code                     string `json:"name"` | 	Code                     string `json:"name"` | ||||||
| 	Upgradable               bool   `json:"upgradable"` | 	Upgradable               bool   `json:"upgradable"` | ||||||
| 	RequestLimit         int    `json:"request_limit"` | 	MessageLimit             int64  `json:"messages_limit"` | ||||||
| 	EmailsLimit          int    `json:"emails_limit"` | 	EmailsLimit              int64  `json:"emails_limit"` | ||||||
| 	AttachmentBytesLimit int64  `json:"attachment_bytes_limit"` | 	AttachmentFileSizeLimit  int64  `json:"attachment_file_size_limit"` | ||||||
|  | 	AttachmentTotalSizeLimit int64  `json:"attachment_total_size_limit"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type UserSubscription struct { | type UserSubscription struct { | ||||||
|  |  | ||||||
|  | @ -24,9 +24,10 @@ const ( | ||||||
| 		CREATE TABLE IF NOT EXISTS plan ( | 		CREATE TABLE IF NOT EXISTS plan ( | ||||||
| 			id INT NOT NULL,		 | 			id INT NOT NULL,		 | ||||||
| 			code TEXT NOT NULL, | 			code TEXT NOT NULL, | ||||||
| 			request_limit INT NOT NULL, | 			messages_limit INT NOT NULL, | ||||||
| 			emails_limit INT NOT NULL, | 			emails_limit INT NOT NULL, | ||||||
| 			attachment_bytes_limit INT NOT NULL, | 			attachment_file_size_limit INT NOT NULL, | ||||||
|  | 			attachment_total_size_limit INT NOT NULL, | ||||||
| 			PRIMARY KEY (id) | 			PRIMARY KEY (id) | ||||||
| 		); | 		); | ||||||
| 		CREATE TABLE IF NOT EXISTS user ( | 		CREATE TABLE IF NOT EXISTS user ( | ||||||
|  | @ -61,13 +62,13 @@ const ( | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
| 	selectUserByNameQuery = ` | 	selectUserByNameQuery = ` | ||||||
| 		SELECT u.user, u.pass, u.role, u.settings, p.code, p.request_limit, p.emails_limit, p.attachment_bytes_limit | 		SELECT u.user, u.pass, u.role, u.settings, p.code, p.messages_limit, p.emails_limit, p.attachment_file_size_limit, p.attachment_total_size_limit | ||||||
| 		FROM user u | 		FROM user u | ||||||
| 		LEFT JOIN plan p on p.id = u.plan_id | 		LEFT JOIN plan p on p.id = u.plan_id | ||||||
| 		WHERE user = ?		 | 		WHERE user = ?		 | ||||||
| 	` | 	` | ||||||
| 	selectUserByTokenQuery = ` | 	selectUserByTokenQuery = ` | ||||||
| 		SELECT u.user, u.pass, u.role, u.settings, p.code, p.request_limit, p.emails_limit, p.attachment_bytes_limit | 		SELECT u.user, u.pass, u.role, u.settings, p.code, p.messages_limit, p.emails_limit, p.attachment_file_size_limit, p.attachment_total_size_limit | ||||||
| 		FROM user u | 		FROM user u | ||||||
| 		JOIN user_token t on u.id = t.user_id | 		JOIN user_token t on u.id = t.user_id | ||||||
| 		LEFT JOIN plan p on p.id = u.plan_id | 		LEFT JOIN plan p on p.id = u.plan_id | ||||||
|  | @ -325,12 +326,11 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | ||||||
| 	defer rows.Close() | 	defer rows.Close() | ||||||
| 	var username, hash, role string | 	var username, hash, role string | ||||||
| 	var prefs, planCode sql.NullString | 	var prefs, planCode sql.NullString | ||||||
| 	var requestLimit, emailLimit sql.NullInt32 | 	var messagesLimit, emailsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit sql.NullInt64 | ||||||
| 	var attachmentBytesLimit sql.NullInt64 |  | ||||||
| 	if !rows.Next() { | 	if !rows.Next() { | ||||||
| 		return nil, ErrNotFound | 		return nil, ErrNotFound | ||||||
| 	} | 	} | ||||||
| 	if err := rows.Scan(&username, &hash, &role, &prefs, &planCode, &requestLimit, &emailLimit, &attachmentBytesLimit); err != nil { | 	if err := rows.Scan(&username, &hash, &role, &prefs, &planCode, &messagesLimit, &emailsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} else if err := rows.Err(); err != nil { | 	} else if err := rows.Err(); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -355,9 +355,10 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { | ||||||
| 		user.Plan = &Plan{ | 		user.Plan = &Plan{ | ||||||
| 			Code:                     planCode.String, | 			Code:                     planCode.String, | ||||||
| 			Upgradable:               true, // FIXME | 			Upgradable:               true, // FIXME | ||||||
| 			RequestLimit:         int(requestLimit.Int32), | 			MessageLimit:             messagesLimit.Int64, | ||||||
| 			EmailsLimit:          int(emailLimit.Int32), | 			EmailsLimit:              emailsLimit.Int64, | ||||||
| 			AttachmentBytesLimit: attachmentBytesLimit.Int64, | 			AttachmentFileSizeLimit:  attachmentFileSizeLimit.Int64, | ||||||
|  | 			AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return user, nil | 	return user, nil | ||||||
|  |  | ||||||
|  | @ -91,7 +91,6 @@ var ( | ||||||
| 	publishPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) | 	publishPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) | ||||||
| 
 | 
 | ||||||
| 	webConfigPath                  = "/config.js" | 	webConfigPath                  = "/config.js" | ||||||
| 	userStatsPath                  = "/user/stats" // FIXME get rid of this in favor of /user/account |  | ||||||
| 	accountPath                    = "/v1/account" | 	accountPath                    = "/v1/account" | ||||||
| 	accountTokenPath               = "/v1/account/token" | 	accountTokenPath               = "/v1/account/token" | ||||||
| 	accountPasswordPath            = "/v1/account/password" | 	accountPasswordPath            = "/v1/account/password" | ||||||
|  | @ -329,8 +328,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | ||||||
| 		return s.ensureWebEnabled(s.handleEmpty)(w, r, v) | 		return s.ensureWebEnabled(s.handleEmpty)(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { | 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { | ||||||
| 		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) | 		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath { |  | ||||||
| 		return s.handleUserStats(w, r, v) |  | ||||||
| 	} else if r.Method == http.MethodPost && r.URL.Path == accountPath { | 	} else if r.Method == http.MethodPost && r.URL.Path == accountPath { | ||||||
| 		return s.handleAccountCreate(w, r, v) | 		return s.handleAccountCreate(w, r, v) | ||||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == accountPath { | 	} else if r.Method == http.MethodGet && r.URL.Path == accountPath { | ||||||
|  | @ -430,19 +427,6 @@ var config = { | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) handleUserStats(w http.ResponseWriter, r *http.Request, v *visitor) error { |  | ||||||
| 	stats, err := v.Stats() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	w.Header().Set("Content-Type", "text/json") |  | ||||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests |  | ||||||
| 	if err := json.NewEncoder(w).Encode(stats); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error { | func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error { | ||||||
| 	r.URL.Path = webSiteDir + r.URL.Path | 	r.URL.Path = webSiteDir + r.URL.Path | ||||||
| 	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) | 	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) | ||||||
|  | @ -531,6 +515,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes | ||||||
| 			go s.sendToFirebase(v, m) | 			go s.sendToFirebase(v, m) | ||||||
| 		} | 		} | ||||||
| 		if s.smtpSender != nil && email != "" { | 		if s.smtpSender != nil && email != "" { | ||||||
|  | 			v.IncrEmails() | ||||||
| 			go s.sendEmail(v, m, email) | 			go s.sendEmail(v, m, email) | ||||||
| 		} | 		} | ||||||
| 		if s.config.UpstreamBaseURL != "" { | 		if s.config.UpstreamBaseURL != "" { | ||||||
|  | @ -545,7 +530,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	v.requests.Inc() | 	v.IncrMessages() | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	s.messages++ | 	s.messages++ | ||||||
| 	s.mu.Unlock() | 	s.mu.Unlock() | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	response := &apiAccountSettingsResponse{ | 	response := &apiAccountSettingsResponse{ | ||||||
| 		Usage: &apiAccountUsageLimits{}, | 		Usage: &apiAccountStats{}, | ||||||
| 	} | 	} | ||||||
| 	if v.user != nil { | 	if v.user != nil { | ||||||
| 		response.Username = v.user.Name | 		response.Username = v.user.Name | ||||||
|  | @ -60,42 +60,59 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis | ||||||
| 			response.Usage.Basis = "account" | 			response.Usage.Basis = "account" | ||||||
| 			response.Plan = &apiAccountSettingsPlan{ | 			response.Plan = &apiAccountSettingsPlan{ | ||||||
| 				Code:       v.user.Plan.Code, | 				Code:       v.user.Plan.Code, | ||||||
| 				RequestLimit:          v.user.Plan.RequestLimit, | 				Upgradable: v.user.Plan.Upgradable, | ||||||
| 				EmailLimit:            v.user.Plan.EmailsLimit, | 			} | ||||||
| 				AttachmentsBytesLimit: v.user.Plan.AttachmentBytesLimit, | 			response.Limits = &apiAccountLimits{ | ||||||
|  | 				MessagesLimit:            v.user.Plan.MessageLimit, | ||||||
|  | 				EmailsLimit:              v.user.Plan.EmailsLimit, | ||||||
|  | 				AttachmentFileSizeLimit:  v.user.Plan.AttachmentFileSizeLimit, | ||||||
|  | 				AttachmentTotalSizeLimit: v.user.Plan.AttachmentTotalSizeLimit, | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			if v.user.Role == auth.RoleAdmin { | 			if v.user.Role == auth.RoleAdmin { | ||||||
| 				response.Usage.Basis = "account" | 				response.Usage.Basis = "account" | ||||||
| 				response.Plan = &apiAccountSettingsPlan{ | 				response.Plan = &apiAccountSettingsPlan{ | ||||||
| 					Code:       string(auth.PlanUnlimited), | 					Code:       string(auth.PlanUnlimited), | ||||||
| 					RequestLimit:          0, | 					Upgradable: false, | ||||||
| 					EmailLimit:            0, | 				} | ||||||
| 					AttachmentsBytesLimit: 0, | 				response.Limits = &apiAccountLimits{ | ||||||
|  | 					MessagesLimit:            0, | ||||||
|  | 					EmailsLimit:              0, | ||||||
|  | 					AttachmentFileSizeLimit:  0, | ||||||
|  | 					AttachmentTotalSizeLimit: 0, | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				response.Usage.Basis = "ip" | 				response.Usage.Basis = "ip" | ||||||
| 				response.Plan = &apiAccountSettingsPlan{ | 				response.Plan = &apiAccountSettingsPlan{ | ||||||
| 					Code:       string(auth.PlanDefault), | 					Code:       string(auth.PlanDefault), | ||||||
| 					RequestLimit:          s.config.VisitorRequestLimitBurst, | 					Upgradable: true, | ||||||
| 					EmailLimit:            s.config.VisitorEmailLimitBurst, | 				} | ||||||
| 					AttachmentsBytesLimit: s.config.VisitorAttachmentTotalSizeLimit, | 				response.Limits = &apiAccountLimits{ | ||||||
|  | 					MessagesLimit:            int64(s.config.VisitorRequestLimitBurst), | ||||||
|  | 					EmailsLimit:              int64(s.config.VisitorEmailLimitBurst), | ||||||
|  | 					AttachmentFileSizeLimit:  s.config.AttachmentFileSizeLimit, | ||||||
|  | 					AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		response.Username = auth.Everyone | 		response.Username = auth.Everyone | ||||||
| 		response.Role = string(auth.RoleAnonymous) | 		response.Role = string(auth.RoleAnonymous) | ||||||
| 		response.Usage.Basis = "account" | 		response.Usage.Basis = "ip" | ||||||
| 		response.Plan = &apiAccountSettingsPlan{ | 		response.Plan = &apiAccountSettingsPlan{ | ||||||
| 			Code:       string(auth.PlanNone), | 			Code:       string(auth.PlanNone), | ||||||
| 			RequestLimit:          s.config.VisitorRequestLimitBurst, | 			Upgradable: true, | ||||||
| 			EmailLimit:            s.config.VisitorEmailLimitBurst, | 		} | ||||||
| 			AttachmentsBytesLimit: s.config.VisitorAttachmentTotalSizeLimit, | 		response.Limits = &apiAccountLimits{ | ||||||
|  | 			MessagesLimit:            int64(s.config.VisitorRequestLimitBurst), | ||||||
|  | 			EmailsLimit:              int64(s.config.VisitorEmailLimitBurst), | ||||||
|  | 			AttachmentFileSizeLimit:  s.config.AttachmentFileSizeLimit, | ||||||
|  | 			AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	response.Usage.Requests = v.requests.Value() | 	response.Usage.Messages = stats.Messages | ||||||
| 	response.Usage.AttachmentsBytes = stats.VisitorAttachmentBytesUsed | 	response.Usage.Emails = stats.Emails | ||||||
|  | 	response.Usage.AttachmentsSize = stats.AttachmentBytes | ||||||
| 	if err := json.NewEncoder(w).Encode(response); err != nil { | 	if err := json.NewEncoder(w).Encode(response); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1381,7 +1381,7 @@ func TestServer_PublishAttachmentUserStats(t *testing.T) { | ||||||
| 	require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&stats)) | 	require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&stats)) | ||||||
| 	require.Equal(t, int64(5000), stats.AttachmentFileSizeLimit) | 	require.Equal(t, int64(5000), stats.AttachmentFileSizeLimit) | ||||||
| 	require.Equal(t, int64(6000), stats.VisitorAttachmentBytesTotal) | 	require.Equal(t, int64(6000), stats.VisitorAttachmentBytesTotal) | ||||||
| 	require.Equal(t, int64(4999), stats.VisitorAttachmentBytesUsed) | 	require.Equal(t, int64(4999), stats.AttachmentBytes) | ||||||
| 	require.Equal(t, int64(1001), stats.VisitorAttachmentBytesRemaining) | 	require.Equal(t, int64(1001), stats.VisitorAttachmentBytesRemaining) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -227,24 +227,29 @@ type apiAccountTokenResponse struct { | ||||||
| type apiAccountSettingsPlan struct { | type apiAccountSettingsPlan struct { | ||||||
| 	Code       string `json:"code"` | 	Code       string `json:"code"` | ||||||
| 	Upgradable bool   `json:"upgradable"` | 	Upgradable bool   `json:"upgradable"` | ||||||
| 	RequestLimit          int    `json:"request_limit"` |  | ||||||
| 	EmailLimit            int    `json:"email_limit"` |  | ||||||
| 	AttachmentsBytesLimit int64  `json:"attachments_bytes_limit"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountUsageLimits struct { | type apiAccountLimits struct { | ||||||
|  | 	MessagesLimit            int64 `json:"messages"` | ||||||
|  | 	EmailsLimit              int64 `json:"emails"` | ||||||
|  | 	AttachmentFileSizeLimit  int64 `json:"attachment_file_size"` | ||||||
|  | 	AttachmentTotalSizeLimit int64 `json:"attachment_total_size"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type apiAccountStats struct { | ||||||
| 	Basis           string `json:"basis"` // "ip" or "account" | 	Basis           string `json:"basis"` // "ip" or "account" | ||||||
| 	Requests         int64  `json:"requests"` | 	Messages        int64  `json:"messages"` | ||||||
| 	Emails           int    `json:"emails"` | 	Emails          int64  `json:"emails"` | ||||||
| 	AttachmentsBytes int64  `json:"attachments_bytes"` | 	AttachmentsSize int64  `json:"attachments_size"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountSettingsResponse struct { | type apiAccountSettingsResponse struct { | ||||||
| 	Username      string                      `json:"username"` | 	Username      string                      `json:"username"` | ||||||
| 	Role          string                      `json:"role,omitempty"` | 	Role          string                      `json:"role,omitempty"` | ||||||
| 	Plan          *apiAccountSettingsPlan     `json:"plan,omitempty"` |  | ||||||
| 	Language      string                      `json:"language,omitempty"` | 	Language      string                      `json:"language,omitempty"` | ||||||
| 	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"` | 	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"` | ||||||
| 	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"` | 	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"` | ||||||
| 	Usage         *apiAccountUsageLimits      `json:"usage,omitempty"` | 	Plan          *apiAccountSettingsPlan     `json:"plan,omitempty"` | ||||||
|  | 	Limits        *apiAccountLimits           `json:"limits,omitempty"` | ||||||
|  | 	Usage         *apiAccountStats            `json:"usage,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,27 +28,27 @@ type visitor struct { | ||||||
| 	messageCache        *messageCache | 	messageCache        *messageCache | ||||||
| 	ip                  netip.Addr | 	ip                  netip.Addr | ||||||
| 	user                *auth.User | 	user                *auth.User | ||||||
| 	requests       *util.AtomicCounter[int64] | 	messages            int64 | ||||||
|  | 	emails              int64 | ||||||
| 	requestLimiter      *rate.Limiter | 	requestLimiter      *rate.Limiter | ||||||
| 	emails         *rate.Limiter | 	emailsLimiter       *rate.Limiter | ||||||
| 	subscriptions  util.Limiter | 	subscriptionLimiter util.Limiter | ||||||
| 	bandwidth      util.Limiter | 	bandwidthLimiter    util.Limiter | ||||||
| 	firebase            time.Time // Next allowed Firebase message | 	firebase            time.Time // Next allowed Firebase message | ||||||
| 	seen                time.Time | 	seen                time.Time | ||||||
| 	mu                  sync.Mutex | 	mu                  sync.Mutex | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type visitorStats struct { | type visitorStats struct { | ||||||
| 	AttachmentFileSizeLimit         int64 `json:"attachmentFileSizeLimit"` | 	Messages        int64 | ||||||
| 	VisitorAttachmentBytesTotal     int64 `json:"visitorAttachmentBytesTotal"` | 	Emails          int64 | ||||||
| 	VisitorAttachmentBytesUsed      int64 `json:"visitorAttachmentBytesUsed"` | 	AttachmentBytes int64 | ||||||
| 	VisitorAttachmentBytesRemaining int64 `json:"visitorAttachmentBytesRemaining"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor { | func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor { | ||||||
| 	var requestLimiter *rate.Limiter | 	var requestLimiter *rate.Limiter | ||||||
| 	if user != nil && user.Plan != nil { | 	if user != nil && user.Plan != nil { | ||||||
| 		requestLimiter = rate.NewLimiter(rate.Limit(user.Plan.RequestLimit)*rate.Every(24*time.Hour), conf.VisitorRequestLimitBurst) | 		requestLimiter = rate.NewLimiter(rate.Limit(user.Plan.MessageLimit)*rate.Every(24*time.Hour), conf.VisitorRequestLimitBurst) | ||||||
| 	} else { | 	} else { | ||||||
| 		requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst) | 		requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst) | ||||||
| 	} | 	} | ||||||
|  | @ -57,11 +57,12 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *a | ||||||
| 		messageCache:        messageCache, | 		messageCache:        messageCache, | ||||||
| 		ip:                  ip, | 		ip:                  ip, | ||||||
| 		user:                user, | 		user:                user, | ||||||
| 		requests:       util.NewAtomicCounter[int64](0), | 		messages:            0, // TODO | ||||||
|  | 		emails:              0, // TODO | ||||||
| 		requestLimiter:      requestLimiter, | 		requestLimiter:      requestLimiter, | ||||||
| 		emails:         rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), | 		emailsLimiter:       rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), | ||||||
| 		subscriptions:  util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), | 		subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), | ||||||
| 		bandwidth:      util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), | 		bandwidthLimiter:    util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), | ||||||
| 		firebase:            time.Unix(0, 0), | 		firebase:            time.Unix(0, 0), | ||||||
| 		seen:                time.Now(), | 		seen:                time.Now(), | ||||||
| 	} | 	} | ||||||
|  | @ -90,7 +91,7 @@ func (v *visitor) FirebaseTemporarilyDeny() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (v *visitor) EmailAllowed() error { | func (v *visitor) EmailAllowed() error { | ||||||
| 	if !v.emails.Allow() { | 	if !v.emailsLimiter.Allow() { | ||||||
| 		return errVisitorLimitReached | 		return errVisitorLimitReached | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -99,7 +100,7 @@ func (v *visitor) EmailAllowed() error { | ||||||
| func (v *visitor) SubscriptionAllowed() error { | func (v *visitor) SubscriptionAllowed() error { | ||||||
| 	v.mu.Lock() | 	v.mu.Lock() | ||||||
| 	defer v.mu.Unlock() | 	defer v.mu.Unlock() | ||||||
| 	if err := v.subscriptions.Allow(1); err != nil { | 	if err := v.subscriptionLimiter.Allow(1); err != nil { | ||||||
| 		return errVisitorLimitReached | 		return errVisitorLimitReached | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -108,7 +109,7 @@ func (v *visitor) SubscriptionAllowed() error { | ||||||
| func (v *visitor) RemoveSubscription() { | func (v *visitor) RemoveSubscription() { | ||||||
| 	v.mu.Lock() | 	v.mu.Lock() | ||||||
| 	defer v.mu.Unlock() | 	defer v.mu.Unlock() | ||||||
| 	v.subscriptions.Allow(-1) | 	v.subscriptionLimiter.Allow(-1) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (v *visitor) Keepalive() { | func (v *visitor) Keepalive() { | ||||||
|  | @ -118,7 +119,7 @@ func (v *visitor) Keepalive() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (v *visitor) BandwidthLimiter() util.Limiter { | func (v *visitor) BandwidthLimiter() util.Limiter { | ||||||
| 	return v.bandwidth | 	return v.bandwidthLimiter | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (v *visitor) Stale() bool { | func (v *visitor) Stale() bool { | ||||||
|  | @ -127,19 +128,28 @@ func (v *visitor) Stale() bool { | ||||||
| 	return time.Since(v.seen) > visitorExpungeAfter | 	return time.Since(v.seen) > visitorExpungeAfter | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (v *visitor) IncrMessages() { | ||||||
|  | 	v.mu.Lock() | ||||||
|  | 	defer v.mu.Unlock() | ||||||
|  | 	v.messages++ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (v *visitor) IncrEmails() { | ||||||
|  | 	v.mu.Lock() | ||||||
|  | 	defer v.mu.Unlock() | ||||||
|  | 	v.emails++ | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (v *visitor) Stats() (*visitorStats, error) { | func (v *visitor) Stats() (*visitorStats, error) { | ||||||
| 	attachmentsBytesUsed, err := v.messageCache.AttachmentBytesUsed(v.ip.String()) | 	attachmentsBytesUsed, err := v.messageCache.AttachmentBytesUsed(v.ip.String()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	attachmentsBytesRemaining := v.config.VisitorAttachmentTotalSizeLimit - attachmentsBytesUsed | 	v.mu.Lock() | ||||||
| 	if attachmentsBytesRemaining < 0 { | 	defer v.mu.Unlock() | ||||||
| 		attachmentsBytesRemaining = 0 |  | ||||||
| 	} |  | ||||||
| 	return &visitorStats{ | 	return &visitorStats{ | ||||||
| 		AttachmentFileSizeLimit:         v.config.AttachmentFileSizeLimit, | 		Messages:        v.messages, | ||||||
| 		VisitorAttachmentBytesTotal:     v.config.VisitorAttachmentTotalSizeLimit, | 		Emails:          v.emails, | ||||||
| 		VisitorAttachmentBytesUsed:      attachmentsBytesUsed, | 		AttachmentBytes: attachmentsBytesUsed, | ||||||
| 		VisitorAttachmentBytesRemaining: attachmentsBytesRemaining, |  | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| package util |  | ||||||
| 
 |  | ||||||
| import "sync" |  | ||||||
| 
 |  | ||||||
| type AtomicCounter[T int | int32 | int64] struct { |  | ||||||
| 	value T |  | ||||||
| 	mu    sync.Mutex |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewAtomicCounter[T int | int32 | int64](value T) *AtomicCounter[T] { |  | ||||||
| 	return &AtomicCounter[T]{ |  | ||||||
| 		value: value, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| func (c *AtomicCounter[T]) Inc() T { |  | ||||||
| 	c.mu.Lock() |  | ||||||
| 	defer c.mu.Unlock() |  | ||||||
| 	c.value++ |  | ||||||
| 	return c.value |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (c *AtomicCounter[T]) Value() T { |  | ||||||
| 	c.mu.Lock() |  | ||||||
| 	defer c.mu.Unlock() |  | ||||||
| 	return c.value |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (c *AtomicCounter[T]) Reset() { |  | ||||||
| 	c.mu.Lock() |  | ||||||
| 	defer c.mu.Unlock() |  | ||||||
| 	c.value = 0 |  | ||||||
| } |  | ||||||
|  | @ -19,10 +19,14 @@ import DialogActions from "@mui/material/DialogActions"; | ||||||
| import api from "../app/Api"; | import api from "../app/Api"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import {NavLink, useOutletContext} from "react-router-dom"; | import {useNavigate, useOutletContext} from "react-router-dom"; | ||||||
| import Box from "@mui/material/Box"; | import {formatBytes} from "../app/utils"; | ||||||
| 
 | 
 | ||||||
| const Account = () => { | const Account = () => { | ||||||
|  |     if (!session.exists()) { | ||||||
|  |         window.location.href = routes.app; | ||||||
|  |         return <></>; | ||||||
|  |     } | ||||||
|     return ( |     return ( | ||||||
|         <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}> |         <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}> | ||||||
|             <Stack spacing={3}> |             <Stack spacing={3}> | ||||||
|  | @ -52,10 +56,13 @@ const Basics = () => { | ||||||
| const Stats = () => { | const Stats = () => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const { account } = useOutletContext(); |     const { account } = useOutletContext(); | ||||||
|     const admin = account?.role === "admin" |     if (!account) { | ||||||
|     const usage = account?.usage; |         return <></>; // TODO loading | ||||||
|     const plan = account?.plan; |     } | ||||||
|     const accountType = plan?.code ?? "none"; |     const accountType = account.plan.code ?? "none"; | ||||||
|  |     const limits = account.limits; | ||||||
|  |     const usage = account.usage; | ||||||
|  |     const normalize = (value, max) => (value / max * 100); | ||||||
|     return ( |     return ( | ||||||
|         <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}> |         <Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}> | ||||||
|             <Typography variant="h5" sx={{marginBottom: 2}}> |             <Typography variant="h5" sx={{marginBottom: 2}}> | ||||||
|  | @ -69,26 +76,26 @@ const Stats = () => { | ||||||
|                             : t(`account_type_${accountType}`)} |                             : t(`account_type_${accountType}`)} | ||||||
|                     </div> |                     </div> | ||||||
|                 </Pref> |                 </Pref> | ||||||
|                 <Pref labelId={"dailyMessages"} title={t("Daily messages")}> |                 <Pref labelId={"messages"} title={t("Published messages")}> | ||||||
|                     <div> |                     <div> | ||||||
|                         <Typography variant="body2" sx={{float: "left"}}>{usage?.requests ?? 0}</Typography> |                         <Typography variant="body2" sx={{float: "left"}}>{usage.messages}</Typography> | ||||||
|                         <Typography variant="body2" sx={{float: "right"}}>{plan?.request_limit > 0 ? t("of {{limit}}", { limit: plan.request_limit }) : t("Unlimited")}</Typography> |                         <Typography variant="body2" sx={{float: "right"}}>{limits.messages > 0 ? t("of {{limit}}", { limit: limits.messages }) : t("Unlimited")}</Typography> | ||||||
|                     </div> |                     </div> | ||||||
|                     <LinearProgress variant="determinate" value={10} /> |                     <LinearProgress variant="determinate" value={limits.messages > 0 ? normalize(usage.messages, limits.messages) : 100} /> | ||||||
|                 </Pref> |                 </Pref> | ||||||
|                 <Pref labelId={"attachmentStorage"} title={t("Attachment storage")}> |                 <Pref labelId={"emails"} title={t("Emails sent")}> | ||||||
|                     <div> |                     <div> | ||||||
|                         <Typography variant="body2" sx={{float: "left"}}>15 MB used</Typography> |                         <Typography variant="body2" sx={{float: "left"}}>{usage.emails}</Typography> | ||||||
|                         <Typography variant="body2" sx={{float: "right"}}>of 150 MB</Typography> |                         <Typography variant="body2" sx={{float: "right"}}>{limits.emails > 0 ? t("of {{limit}}", { limit: limits.emails }) : t("Unlimited")}</Typography> | ||||||
|                     </div> |                     </div> | ||||||
|                     <LinearProgress variant="determinate" value={40} /> |                     <LinearProgress variant="determinate" value={limits.emails > 0 ? normalize(usage.emails, limits.emails) : 100} /> | ||||||
|                 </Pref> |                 </Pref> | ||||||
|                 <Pref labelId={"emailLimits"} title={t("Emails sent")}> |                 <Pref labelId={"attachments"} title={t("Attachment storage")}> | ||||||
|                     <div> |                     <div> | ||||||
|                         <Typography variant="body2" sx={{float: "left"}}>2</Typography> |                         <Typography variant="body2" sx={{float: "left"}}>{formatBytes(usage.attachments_size)}</Typography> | ||||||
|                         <Typography variant="body2" sx={{float: "right"}}>of 15</Typography> |                         <Typography variant="body2" sx={{float: "right"}}>{limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(limits.attachment_total_size) }) : t("Unlimited")}</Typography> | ||||||
|                     </div> |                     </div> | ||||||
|                     <LinearProgress variant="determinate" value={20} /> |                     <LinearProgress variant="determinate" value={limits.attachment_total_size > 0 ? normalize(usage.attachments_size, limits.attachment_total_size) : 100} /> | ||||||
|                 </Pref> |                 </Pref> | ||||||
|             </PrefGroup> |             </PrefGroup> | ||||||
|         </Card> |         </Card> | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ import api from "../app/Api"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
| import EmojiPicker from "./EmojiPicker"; | import EmojiPicker from "./EmojiPicker"; | ||||||
| import {Trans, useTranslation} from "react-i18next"; | import {Trans, useTranslation} from "react-i18next"; | ||||||
|  | import session from "../app/Session"; | ||||||
| 
 | 
 | ||||||
| const PublishDialog = (props) => { | const PublishDialog = (props) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|  | @ -159,9 +160,11 @@ const PublishDialog = (props) => { | ||||||
| 
 | 
 | ||||||
|     const checkAttachmentLimits = async (file) => { |     const checkAttachmentLimits = async (file) => { | ||||||
|         try { |         try { | ||||||
|             const stats = await api.userStats(baseUrl); |             const account = await api.getAccount(baseUrl, session.token()); | ||||||
|             const fileSizeLimit = stats.attachmentFileSizeLimit ?? 0; |             const fileSizeLimit = account.limits.attachment_file_size ?? 0; | ||||||
|             const remainingBytes = stats.visitorAttachmentBytesRemaining ?? 0; |             const totalSizeLimit = account.limits.attachment_total_size ?? 0; | ||||||
|  |             const usedSize = account.usage.attachments_size ?? 0; | ||||||
|  |             const remainingBytes = (totalSizeLimit > 0) ? totalSizeLimit - usedSize : 0; | ||||||
|             const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; |             const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; | ||||||
|             const quotaReached = remainingBytes > 0 && file.size > remainingBytes; |             const quotaReached = remainingBytes > 0 && file.size > remainingBytes; | ||||||
|             if (fileSizeLimitReached && quotaReached) { |             if (fileSizeLimitReached && quotaReached) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue