This commit is contained in:
binwiederhier 2022-12-19 09:59:32 -05:00
parent 42e46a7c22
commit 6598ce2fe4
10 changed files with 157 additions and 160 deletions

View file

@ -82,11 +82,12 @@ 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 {

View file

@ -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
@ -353,11 +353,12 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) {
} }
if planCode.Valid { if planCode.Valid {
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

View file

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

View file

@ -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
@ -59,43 +59,60 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
if v.user.Plan != nil { if v.user.Plan != nil {
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
} }

View file

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

View file

@ -225,26 +225,31 @@ 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 {
Basis string `json:"basis"` // "ip" or "account" MessagesLimit int64 `json:"messages"`
Requests int64 `json:"requests"` EmailsLimit int64 `json:"emails"`
Emails int `json:"emails"` AttachmentFileSizeLimit int64 `json:"attachment_file_size"`
AttachmentsBytes int64 `json:"attachments_bytes"` AttachmentTotalSizeLimit int64 `json:"attachment_total_size"`
}
type apiAccountStats struct {
Basis string `json:"basis"` // "ip" or "account"
Messages int64 `json:"messages"`
Emails int64 `json:"emails"`
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"`
} }

View file

@ -24,46 +24,47 @@ var (
// visitor represents an API user, and its associated rate.Limiter used for rate limiting // visitor represents an API user, and its associated rate.Limiter used for rate limiting
type visitor struct { type visitor struct {
config *Config config *Config
messageCache *messageCache messageCache *messageCache
ip netip.Addr ip netip.Addr
user *auth.User user *auth.User
requests *util.AtomicCounter[int64] messages int64
requestLimiter *rate.Limiter emails int64
emails *rate.Limiter requestLimiter *rate.Limiter
subscriptions util.Limiter emailsLimiter *rate.Limiter
bandwidth util.Limiter subscriptionLimiter util.Limiter
firebase time.Time // Next allowed Firebase message bandwidthLimiter util.Limiter
seen time.Time firebase time.Time // Next allowed Firebase message
mu sync.Mutex seen time.Time
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)
} }
return &visitor{ return &visitor{
config: conf, config: conf,
messageCache: messageCache, messageCache: messageCache,
ip: ip, ip: ip,
user: user, user: user,
requests: util.NewAtomicCounter[int64](0), messages: 0, // TODO
requestLimiter: requestLimiter, emails: 0, // TODO
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), requestLimiter: requestLimiter,
subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), emailsLimiter: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
bandwidth: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
firebase: time.Unix(0, 0), bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour),
seen: time.Now(), firebase: time.Unix(0, 0),
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
} }

View file

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

View file

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

View file

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