diff --git a/server/server_account.go b/server/server_account.go index 1b2e38f1..9e8cba0c 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -39,7 +39,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return nil } -func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error { +func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *visitor) error { stats, err := v.Info() if err != nil { return err @@ -50,6 +50,8 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis MessagesRemaining: stats.MessagesRemaining, Emails: stats.Emails, EmailsRemaining: stats.EmailsRemaining, + Topics: stats.Topics, + TopicsRemaining: stats.TopicsRemaining, AttachmentTotalSize: stats.AttachmentTotalSize, AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining, }, @@ -57,6 +59,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis Basis: stats.Basis, Messages: stats.MessagesLimit, Emails: stats.EmailsLimit, + Topics: stats.TopicsLimit, AttachmentTotalSize: stats.AttachmentTotalSizeLimit, AttachmentFileSize: stats.AttachmentFileSizeLimit, }, @@ -119,7 +122,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis return nil } -func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { +func (s *Server) handleAccountDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error { if err := s.userManager.RemoveUser(v.user.Name); err != nil { return err } @@ -141,7 +144,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ return nil } -func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, r *http.Request, v *visitor) error { +func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, _ *http.Request, v *visitor) error { // TODO rate limit token, err := s.userManager.CreateToken(v.user) if err != nil { @@ -159,7 +162,7 @@ func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, r *http.Request, return nil } -func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, r *http.Request, v *visitor) error { +func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, _ *http.Request, v *visitor) error { // TODO rate limit if v.user == nil { return errHTTPUnauthorized @@ -182,7 +185,7 @@ func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, r *http.Request return nil } -func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { +func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error { // TODO rate limit if v.user.Token == "" { return errHTTPBadRequestNoTokenProvided diff --git a/server/types.go b/server/types.go index bfa6b322..fef12691 100644 --- a/server/types.go +++ b/server/types.go @@ -243,6 +243,7 @@ type apiAccountLimits struct { Basis string `json:"basis"` // "ip", "role" or "plan" Messages int64 `json:"messages"` Emails int64 `json:"emails"` + Topics int64 `json:"topics"` AttachmentTotalSize int64 `json:"attachment_total_size"` AttachmentFileSize int64 `json:"attachment_file_size"` } @@ -252,6 +253,8 @@ type apiAccountStats struct { MessagesRemaining int64 `json:"messages_remaining"` Emails int64 `json:"emails"` EmailsRemaining int64 `json:"emails_remaining"` + Topics int64 `json:"topics"` + TopicsRemaining int64 `json:"topics_remaining"` AttachmentTotalSize int64 `json:"attachment_total_size"` AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"` } diff --git a/server/visitor.go b/server/visitor.go index c39b47ef..2fbce8ce 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -48,6 +48,9 @@ type visitorInfo struct { Emails int64 EmailsLimit int64 EmailsRemaining int64 + Topics int64 + TopicsLimit int64 + TopicsRemaining int64 AttachmentTotalSize int64 AttachmentTotalSizeLimit int64 AttachmentTotalSizeRemaining int64 @@ -173,20 +176,19 @@ func (v *visitor) Info() (*visitorInfo, error) { info := &visitorInfo{} if v.user != nil && v.user.Role == user.RoleAdmin { info.Basis = "role" - info.MessagesLimit = 0 - info.EmailsLimit = 0 - info.AttachmentTotalSizeLimit = 0 - info.AttachmentFileSizeLimit = 0 + // All limits are zero! } else if v.user != nil && v.user.Plan != nil { info.Basis = "plan" info.MessagesLimit = v.user.Plan.MessagesLimit info.EmailsLimit = v.user.Plan.EmailsLimit + info.TopicsLimit = v.user.Plan.TopicsLimit info.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit info.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit } else { info.Basis = "ip" info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) + info.TopicsLimit = 0 // FIXME info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit } @@ -200,10 +202,20 @@ func (v *visitor) Info() (*visitorInfo, error) { if err != nil { return nil, err } + var topics int64 + if v.user != nil { + for _, grant := range v.user.Grants { + if grant.Owner { + topics++ + } + } + } info.Messages = messages info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages) info.Emails = emails info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails) + info.Topics = topics + info.TopicsRemaining = zeroIfNegative(info.TopicsLimit - info.Topics) info.AttachmentTotalSize = attachmentsBytesUsed info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize) return info, nil diff --git a/user/manager.go b/user/manager.go index ef7862d3..9916f959 100644 --- a/user/manager.go +++ b/user/manager.go @@ -35,6 +35,7 @@ const ( code TEXT NOT NULL, messages_limit INT NOT NULL, emails_limit INT NOT NULL, + topics_limit INT NOT NULL, attachment_file_size_limit INT NOT NULL, attachment_total_size_limit INT NOT NULL, PRIMARY KEY (id) @@ -75,13 +76,13 @@ const ( ` createTablesQueries = `BEGIN; ` + createTablesQueriesNoTx + ` COMMIT;` selectUserByNameQuery = ` - SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.emails_limit, p.attachment_file_size_limit, p.attachment_total_size_limit + SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.emails_limit, p.topics_limit, p.attachment_file_size_limit, p.attachment_total_size_limit FROM user u LEFT JOIN plan p on p.id = u.plan_id WHERE user = ? ` selectUserByTokenQuery = ` - SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.emails_limit, p.attachment_file_size_limit, p.attachment_total_size_limit + SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.emails_limit, p.topics_limit, p.attachment_file_size_limit, p.attachment_total_size_limit FROM user u JOIN user_token t on u.id = t.user_id LEFT JOIN plan p on p.id = u.plan_id @@ -469,11 +470,11 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { var username, hash, role string var settings, planCode sql.NullString var messages, emails int64 - var messagesLimit, emailsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit sql.NullInt64 + var messagesLimit, emailsLimit, topicsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit sql.NullInt64 if !rows.Next() { return nil, ErrNotFound } - if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &planCode, &messagesLimit, &emailsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit); err != nil { + if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &planCode, &messagesLimit, &emailsLimit, &topicsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -504,6 +505,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { Upgradable: true, // FIXME MessagesLimit: messagesLimit.Int64, EmailsLimit: emailsLimit.Int64, + TopicsLimit: topicsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, } diff --git a/user/types.go b/user/types.go index cf53adae..8c8ecac0 100644 --- a/user/types.go +++ b/user/types.go @@ -60,6 +60,7 @@ type Plan struct { Upgradable bool `json:"upgradable"` MessagesLimit int64 `json:"messages_limit"` EmailsLimit int64 `json:"emails_limit"` + TopicsLimit int64 `json:"topics_limit"` AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"` AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"` } diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 14527331..a34c734a 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -183,6 +183,7 @@ "account_usage_plan_code_business_plus": "Business Plus", "account_usage_messages_title": "Published messages", "account_usage_emails_title": "Emails sent", + "account_usage_topics_title": "Topics reserved", "account_usage_attachment_storage_title": "Attachment storage", "account_usage_attachment_storage_subtitle": "{{filesize}} per file", "account_usage_basis_ip_description": "Usage stats and limits for this account are based on your IP address, so they may be shared with other users.", diff --git a/web/src/components/Account.js b/web/src/components/Account.js index f7d22606..7ef175b1 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -168,7 +168,7 @@ const Stats = () => { return <>; } const planCode = account.plan.code ?? "none"; - const normalize = (value, max) => (value / max * 100); + const normalize = (value, max) => Math.min(value / max * 100, 100); return ( @@ -182,6 +182,13 @@ const Stats = () => { : t(`account_usage_plan_code_${planCode}`)} + +
+ {account.stats.topics} + {account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")} +
+ 0 ? normalize(account.stats.topics, account.limits.topics) : 100} /> +
{account.stats.messages}