Restructure limits

This commit is contained in:
binwiederhier 2022-12-19 16:22:13 -05:00
parent 6598ce2fe4
commit 84785b7a60
7 changed files with 105 additions and 74 deletions

View file

@ -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"`

View file

@ -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,

View file

@ -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 {

View file

@ -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
}

View file

@ -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"`
MessagesRemaining int64 `json:"messages_remaining"`
Emails int64 `json:"emails"`
AttachmentsSize int64 `json:"attachments_size"`
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"`
}

View file

@ -40,17 +40,27 @@ type visitor struct {
}
type visitorStats struct {
Basis string // "ip", "role" or "plan"
Messages int64
MessagesLimit int64
MessagesRemaining int64
Emails int64
AttachmentBytes 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)
}

View file

@ -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>