mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-24 17:33:26 +03:00
Rename plan->tier, topics->reservations, more tests, more todos
This commit is contained in:
parent
df512d0ba2
commit
1f54adad71
@ -502,7 +502,7 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) MarkAttachmentsDeleted(ids []string) error {
|
func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error {
|
||||||
tx, err := c.db.Begin()
|
tx, err := c.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -57,8 +57,9 @@ import (
|
|||||||
- visitor with/without user
|
- visitor with/without user
|
||||||
- plan-based message expiry
|
- plan-based message expiry
|
||||||
- plan-based attachment expiry
|
- plan-based attachment expiry
|
||||||
|
Docs:
|
||||||
|
- "expires" field in message
|
||||||
Refactor:
|
Refactor:
|
||||||
- rename TopicsLimit -> ReservationsLimit
|
|
||||||
- rename /access -> /reservation
|
- rename /access -> /reservation
|
||||||
Later:
|
Later:
|
||||||
- Password reset
|
- Password reset
|
||||||
@ -544,8 +545,8 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
|
|||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
m.User = v.user.Name
|
m.User = v.user.Name
|
||||||
}
|
}
|
||||||
if v.user != nil && v.user.Plan != nil {
|
if v.user != nil && v.user.Tier != nil {
|
||||||
m.Expires = time.Now().Unix() + v.user.Plan.MessagesExpiryDuration
|
m.Expires = time.Now().Unix() + v.user.Tier.MessagesExpiryDuration
|
||||||
} else {
|
} else {
|
||||||
m.Expires = time.Now().Add(s.config.CacheDuration).Unix()
|
m.Expires = time.Now().Add(s.config.CacheDuration).Unix()
|
||||||
}
|
}
|
||||||
@ -822,8 +823,8 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
|||||||
return errHTTPBadRequestAttachmentsDisallowed
|
return errHTTPBadRequestAttachmentsDisallowed
|
||||||
}
|
}
|
||||||
var attachmentExpiryDuration time.Duration
|
var attachmentExpiryDuration time.Duration
|
||||||
if v.user != nil && v.user.Plan != nil {
|
if v.user != nil && v.user.Tier != nil {
|
||||||
attachmentExpiryDuration = time.Duration(v.user.Plan.AttachmentExpiryDuration) * time.Second
|
attachmentExpiryDuration = time.Duration(v.user.Tier.AttachmentExpiryDuration) * time.Second
|
||||||
} else {
|
} else {
|
||||||
attachmentExpiryDuration = s.config.AttachmentExpiryDuration
|
attachmentExpiryDuration = s.config.AttachmentExpiryDuration
|
||||||
}
|
}
|
||||||
@ -1240,13 +1241,16 @@ func (s *Server) execManager() {
|
|||||||
if s.fileCache != nil {
|
if s.fileCache != nil {
|
||||||
ids, err := s.messageCache.AttachmentsExpired()
|
ids, err := s.messageCache.AttachmentsExpired()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error retrieving expired attachments: %s", err.Error())
|
log.Warn("Manager: Error retrieving expired attachments: %s", err.Error())
|
||||||
} else if len(ids) > 0 {
|
} else if len(ids) > 0 {
|
||||||
if err := s.fileCache.Remove(ids...); err != nil {
|
if log.IsDebug() {
|
||||||
log.Warn("Error deleting attachments: %s", err.Error())
|
log.Debug("Manager: Deleting attachments %s", strings.Join(ids, ", "))
|
||||||
}
|
}
|
||||||
if err := s.messageCache.MarkAttachmentsDeleted(ids); err != nil {
|
if err := s.fileCache.Remove(ids...); err != nil {
|
||||||
log.Warn("Error marking attachments deleted: %s", err.Error())
|
log.Warn("Manager: Error deleting attachments: %s", err.Error())
|
||||||
|
}
|
||||||
|
if err := s.messageCache.MarkAttachmentsDeleted(ids...); err != nil {
|
||||||
|
log.Warn("Manager: Error marking attachments deleted: %s", err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Manager: No expired attachments to delete")
|
log.Debug("Manager: No expired attachments to delete")
|
||||||
|
@ -50,8 +50,8 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||||||
MessagesRemaining: stats.MessagesRemaining,
|
MessagesRemaining: stats.MessagesRemaining,
|
||||||
Emails: stats.Emails,
|
Emails: stats.Emails,
|
||||||
EmailsRemaining: stats.EmailsRemaining,
|
EmailsRemaining: stats.EmailsRemaining,
|
||||||
Topics: stats.Topics,
|
Reservations: stats.Reservations,
|
||||||
TopicsRemaining: stats.TopicsRemaining,
|
ReservationsRemaining: stats.ReservationsRemaining,
|
||||||
AttachmentTotalSize: stats.AttachmentTotalSize,
|
AttachmentTotalSize: stats.AttachmentTotalSize,
|
||||||
AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
|
AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
|
||||||
},
|
},
|
||||||
@ -60,7 +60,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||||||
Messages: stats.MessagesLimit,
|
Messages: stats.MessagesLimit,
|
||||||
MessagesExpiryDuration: stats.MessagesExpiryDuration,
|
MessagesExpiryDuration: stats.MessagesExpiryDuration,
|
||||||
Emails: stats.EmailsLimit,
|
Emails: stats.EmailsLimit,
|
||||||
Topics: stats.TopicsLimit,
|
Reservations: stats.ReservationsLimit,
|
||||||
AttachmentTotalSize: stats.AttachmentTotalSizeLimit,
|
AttachmentTotalSize: stats.AttachmentTotalSizeLimit,
|
||||||
AttachmentFileSize: stats.AttachmentFileSizeLimit,
|
AttachmentFileSize: stats.AttachmentFileSizeLimit,
|
||||||
AttachmentExpiryDuration: stats.AttachmentExpiryDuration,
|
AttachmentExpiryDuration: stats.AttachmentExpiryDuration,
|
||||||
@ -80,19 +80,19 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||||||
response.Subscriptions = v.user.Prefs.Subscriptions
|
response.Subscriptions = v.user.Prefs.Subscriptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v.user.Plan != nil {
|
if v.user.Tier != nil {
|
||||||
response.Plan = &apiAccountPlan{
|
response.Tier = &apiAccountTier{
|
||||||
Code: v.user.Plan.Code,
|
Code: v.user.Tier.Code,
|
||||||
Upgradeable: v.user.Plan.Upgradeable,
|
Upgradeable: v.user.Tier.Upgradeable,
|
||||||
}
|
}
|
||||||
} else if v.user.Role == user.RoleAdmin {
|
} else if v.user.Role == user.RoleAdmin {
|
||||||
response.Plan = &apiAccountPlan{
|
response.Tier = &apiAccountTier{
|
||||||
Code: string(user.PlanUnlimited),
|
Code: string(user.TierUnlimited),
|
||||||
Upgradeable: false,
|
Upgradeable: false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response.Plan = &apiAccountPlan{
|
response.Tier = &apiAccountTier{
|
||||||
Code: string(user.PlanDefault),
|
Code: string(user.TierDefault),
|
||||||
Upgradeable: true,
|
Upgradeable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,8 +112,8 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||||||
} else {
|
} else {
|
||||||
response.Username = user.Everyone
|
response.Username = user.Everyone
|
||||||
response.Role = string(user.RoleAnonymous)
|
response.Role = string(user.RoleAnonymous)
|
||||||
response.Plan = &apiAccountPlan{
|
response.Tier = &apiAccountTier{
|
||||||
Code: string(user.PlanNone),
|
Code: string(user.TierNone),
|
||||||
Upgradeable: true,
|
Upgradeable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,7 +340,7 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errHTTPBadRequestPermissionInvalid
|
return errHTTPBadRequestPermissionInvalid
|
||||||
}
|
}
|
||||||
if v.user.Plan == nil {
|
if v.user.Tier == nil {
|
||||||
return errHTTPUnauthorized // FIXME there should always be a plan!
|
return errHTTPUnauthorized // FIXME there should always be a plan!
|
||||||
}
|
}
|
||||||
if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
|
if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
|
||||||
@ -354,7 +354,7 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
|
|||||||
reservations, err := s.userManager.ReservationsCount(v.user.Name)
|
reservations, err := s.userManager.ReservationsCount(v.user.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if reservations >= v.user.Plan.TopicsLimit {
|
} else if reservations >= v.user.Tier.ReservationsLimit {
|
||||||
return errHTTPTooManyRequestsLimitReservations
|
return errHTTPTooManyRequestsLimitReservations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/user"
|
"heckel.io/ntfy/user"
|
||||||
@ -343,7 +342,7 @@ func TestAccount_Delete_Not_Allowed(t *testing.T) {
|
|||||||
require.Equal(t, 401, rr.Code)
|
require.Equal(t, 401, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccount_Reservation_Add_User_No_Plan_Failure(t *testing.T) {
|
func TestAccount_Reservation_AddWithoutTierFails(t *testing.T) {
|
||||||
conf := newTestConfigWithAuthFile(t)
|
conf := newTestConfigWithAuthFile(t)
|
||||||
conf.EnableSignup = true
|
conf.EnableSignup = true
|
||||||
s := newTestServer(t, conf)
|
s := newTestServer(t, conf)
|
||||||
@ -357,7 +356,7 @@ func TestAccount_Reservation_Add_User_No_Plan_Failure(t *testing.T) {
|
|||||||
require.Equal(t, 401, rr.Code)
|
require.Equal(t, 401, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccount_Reservation_Add_Admin_Success(t *testing.T) {
|
func TestAccount_Reservation_AddAdminSuccess(t *testing.T) {
|
||||||
conf := newTestConfigWithAuthFile(t)
|
conf := newTestConfigWithAuthFile(t)
|
||||||
conf.EnableSignup = true
|
conf.EnableSignup = true
|
||||||
s := newTestServer(t, conf)
|
s := newTestServer(t, conf)
|
||||||
@ -370,7 +369,7 @@ func TestAccount_Reservation_Add_Admin_Success(t *testing.T) {
|
|||||||
require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code)
|
require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccount_Reservation_Add_Remove_User_With_Plan_Success(t *testing.T) {
|
func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) {
|
||||||
conf := newTestConfigWithAuthFile(t)
|
conf := newTestConfigWithAuthFile(t)
|
||||||
conf.EnableSignup = true
|
conf.EnableSignup = true
|
||||||
s := newTestServer(t, conf)
|
s := newTestServer(t, conf)
|
||||||
@ -379,17 +378,19 @@ func TestAccount_Reservation_Add_Remove_User_With_Plan_Success(t *testing.T) {
|
|||||||
rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil)
|
rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
// Create a plan (hack!)
|
// Create a tier
|
||||||
db, err := sql.Open("sqlite3", conf.AuthFile)
|
require.Nil(t, s.userManager.CreateTier(&user.Tier{
|
||||||
require.Nil(t, err)
|
Code: "pro",
|
||||||
|
Upgradeable: false,
|
||||||
_, err = db.Exec(`
|
MessagesLimit: 123,
|
||||||
INSERT INTO plan (id, code, messages_limit, messages_expiry_duration, emails_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, topics_limit)
|
MessagesExpiryDuration: 86400,
|
||||||
VALUES (1, 'testplan', 10, 86400, 10, 10, 10, 10800, 2);
|
EmailsLimit: 32,
|
||||||
|
ReservationsLimit: 2,
|
||||||
UPDATE user SET plan_id = 1 WHERE user = 'phil';
|
AttachmentFileSizeLimit: 1231231,
|
||||||
`)
|
AttachmentTotalSizeLimit: 123123,
|
||||||
require.Nil(t, err)
|
AttachmentExpiryDuration: 10800,
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
|
||||||
// Reserve two topics
|
// Reserve two topics
|
||||||
rr = request(t, s, "POST", "/v1/account/access", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{
|
rr = request(t, s, "POST", "/v1/account/access", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{
|
||||||
@ -420,6 +421,14 @@ func TestAccount_Reservation_Add_Remove_User_With_Plan_Success(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
|
account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
|
||||||
|
require.Equal(t, "pro", account.Tier.Code)
|
||||||
|
require.Equal(t, int64(123), account.Limits.Messages)
|
||||||
|
require.Equal(t, int64(86400), account.Limits.MessagesExpiryDuration)
|
||||||
|
require.Equal(t, int64(32), account.Limits.Emails)
|
||||||
|
require.Equal(t, int64(2), account.Limits.Reservations)
|
||||||
|
require.Equal(t, int64(1231231), account.Limits.AttachmentFileSize)
|
||||||
|
require.Equal(t, int64(123123), account.Limits.AttachmentTotalSize)
|
||||||
|
require.Equal(t, int64(10800), account.Limits.AttachmentExpiryDuration)
|
||||||
require.Equal(t, 2, len(account.Reservations))
|
require.Equal(t, 2, len(account.Reservations))
|
||||||
require.Equal(t, "another", account.Reservations[0].Topic)
|
require.Equal(t, "another", account.Reservations[0].Topic)
|
||||||
require.Equal(t, "write-only", account.Reservations[0].Everyone)
|
require.Equal(t, "write-only", account.Reservations[0].Everyone)
|
||||||
@ -441,27 +450,21 @@ func TestAccount_Reservation_Add_Remove_User_With_Plan_Success(t *testing.T) {
|
|||||||
require.Equal(t, "mytopic", account.Reservations[0].Topic)
|
require.Equal(t, "mytopic", account.Reservations[0].Topic)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccount_Reservation_Add_Access_By_Anonymous_Fails(t *testing.T) {
|
func TestAccount_Reservation_PublishByAnonymousFails(t *testing.T) {
|
||||||
conf := newTestConfigWithAuthFile(t)
|
conf := newTestConfigWithAuthFile(t)
|
||||||
conf.AuthDefault = user.PermissionReadWrite
|
conf.AuthDefault = user.PermissionReadWrite
|
||||||
conf.EnableSignup = true
|
conf.EnableSignup = true
|
||||||
s := newTestServer(t, conf)
|
s := newTestServer(t, conf)
|
||||||
|
|
||||||
// Create user
|
// Create user with tier
|
||||||
rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil)
|
rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
// Create a plan (hack!)
|
require.Nil(t, s.userManager.CreateTier(&user.Tier{
|
||||||
db, err := sql.Open("sqlite3", conf.AuthFile)
|
Code: "pro",
|
||||||
require.Nil(t, err)
|
ReservationsLimit: 2,
|
||||||
|
}))
|
||||||
_, err = db.Exec(`
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
INSERT INTO plan (id, code, messages_limit, messages_expiry_duration, emails_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, topics_limit)
|
|
||||||
VALUES (1, 'testplan', 10, 86400, 10, 10, 10, 10800, 2);
|
|
||||||
|
|
||||||
UPDATE user SET plan_id = 1 WHERE user = 'phil';
|
|
||||||
`)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// Reserve a topic
|
// Reserve a topic
|
||||||
rr = request(t, s, "POST", "/v1/account/access", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{
|
rr = request(t, s, "POST", "/v1/account/access", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{
|
||||||
|
@ -1090,6 +1090,34 @@ func TestServer_PublishAsJSON_Invalid(t *testing.T) {
|
|||||||
require.Equal(t, 400, response.Code)
|
require.Equal(t, 400, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) {
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Create tier with certain limits
|
||||||
|
require.Nil(t, s.userManager.CreateTier(&user.Tier{
|
||||||
|
Code: "test",
|
||||||
|
MessagesLimit: 5,
|
||||||
|
MessagesExpiryDuration: 1, // Second
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||||
|
|
||||||
|
// Publish to reach message limit
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("this is message %d", i+1), map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
msg := toMessage(t, response.Body.String())
|
||||||
|
require.True(t, msg.Expires < time.Now().Unix()+5)
|
||||||
|
}
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "this is too much", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 413, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishAttachment(t *testing.T) {
|
func TestServer_PublishAttachment(t *testing.T) {
|
||||||
content := util.RandomString(5000) // > 4096
|
content := util.RandomString(5000) // > 4096
|
||||||
s := newTestServer(t, newTestConfig(t))
|
s := newTestServer(t, newTestConfig(t))
|
||||||
@ -1271,7 +1299,7 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
|||||||
require.Equal(t, 200, response.Code)
|
require.Equal(t, 200, response.Code)
|
||||||
require.Equal(t, content, response.Body.String())
|
require.Equal(t, content, response.Body.String())
|
||||||
|
|
||||||
// DeleteMessages and makes sure it's gone
|
// Prune and makes sure it's gone
|
||||||
time.Sleep(time.Second) // Sigh ...
|
time.Sleep(time.Second) // Sigh ...
|
||||||
s.execManager()
|
s.execManager()
|
||||||
require.NoFileExists(t, file)
|
require.NoFileExists(t, file)
|
||||||
@ -1279,6 +1307,99 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
|||||||
require.Equal(t, 404, response.Code)
|
require.Equal(t, 404, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) {
|
||||||
|
content := util.RandomString(5000) // > 4096
|
||||||
|
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.AttachmentExpiryDuration = time.Millisecond // Hack
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Create tier with certain limits
|
||||||
|
sevenDaysInSeconds := int64(604800)
|
||||||
|
require.Nil(t, s.userManager.CreateTier(&user.Tier{
|
||||||
|
Code: "test",
|
||||||
|
MessagesExpiryDuration: sevenDaysInSeconds,
|
||||||
|
AttachmentFileSizeLimit: 50_000,
|
||||||
|
AttachmentTotalSizeLimit: 200_000,
|
||||||
|
AttachmentExpiryDuration: sevenDaysInSeconds, // 7 days
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||||
|
|
||||||
|
// Publish and make sure we can retrieve it
|
||||||
|
response := request(t, s, "PUT", "/mytopic", content, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
msg := toMessage(t, response.Body.String())
|
||||||
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
|
require.True(t, msg.Attachment.Expires > time.Now().Unix()+sevenDaysInSeconds-30)
|
||||||
|
require.True(t, msg.Expires > time.Now().Unix()+sevenDaysInSeconds-30)
|
||||||
|
file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
|
||||||
|
require.FileExists(t, file)
|
||||||
|
|
||||||
|
path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
|
||||||
|
response = request(t, s, "GET", path, "", nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, content, response.Body.String())
|
||||||
|
|
||||||
|
// Prune and makes sure it's still there
|
||||||
|
time.Sleep(time.Second) // Sigh ...
|
||||||
|
s.execManager()
|
||||||
|
require.FileExists(t, file)
|
||||||
|
response = request(t, s, "GET", path, "", nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
|
||||||
|
smallFile := util.RandomString(20_000)
|
||||||
|
largeFile := util.RandomString(50_000)
|
||||||
|
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.AttachmentFileSizeLimit = 20_000
|
||||||
|
c.VisitorAttachmentTotalSizeLimit = 40_000
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Create tier with certain limits
|
||||||
|
require.Nil(t, s.userManager.CreateTier(&user.Tier{
|
||||||
|
Code: "test",
|
||||||
|
AttachmentFileSizeLimit: 50_000,
|
||||||
|
AttachmentTotalSizeLimit: 200_000,
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||||
|
|
||||||
|
// Publish small file as anonymous
|
||||||
|
response := request(t, s, "PUT", "/mytopic", smallFile, nil)
|
||||||
|
msg := toMessage(t, response.Body.String())
|
||||||
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
|
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
|
||||||
|
|
||||||
|
// Publish large file as anonymous
|
||||||
|
response = request(t, s, "PUT", "/mytopic", largeFile, nil)
|
||||||
|
require.Equal(t, 413, response.Code)
|
||||||
|
|
||||||
|
// Publish too large file as phil
|
||||||
|
response = request(t, s, "PUT", "/mytopic", largeFile+" a few more bytes", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 413, response.Code)
|
||||||
|
|
||||||
|
// Publish large file as phil (4x)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
msg = toMessage(t, response.Body.String())
|
||||||
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
|
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
|
||||||
|
}
|
||||||
|
response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
})
|
||||||
|
require.Equal(t, 413, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) {
|
func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) {
|
||||||
content := util.RandomString(5000) // > 4096
|
content := util.RandomString(5000) // > 4096
|
||||||
|
|
||||||
|
@ -235,17 +235,17 @@ type apiAccountTokenResponse struct {
|
|||||||
Expires int64 `json:"expires"`
|
Expires int64 `json:"expires"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountPlan struct {
|
type apiAccountTier struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Upgradeable bool `json:"upgradeable"`
|
Upgradeable bool `json:"upgradeable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountLimits struct {
|
type apiAccountLimits struct {
|
||||||
Basis string `json:"basis"` // "ip", "role" or "plan"
|
Basis string `json:"basis"` // "ip", "role" or "tier"
|
||||||
Messages int64 `json:"messages"`
|
Messages int64 `json:"messages"`
|
||||||
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
||||||
Emails int64 `json:"emails"`
|
Emails int64 `json:"emails"`
|
||||||
Topics int64 `json:"topics"`
|
Reservations int64 `json:"reservations"`
|
||||||
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
||||||
AttachmentFileSize int64 `json:"attachment_file_size"`
|
AttachmentFileSize int64 `json:"attachment_file_size"`
|
||||||
AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
|
AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
|
||||||
@ -256,8 +256,8 @@ type apiAccountStats struct {
|
|||||||
MessagesRemaining int64 `json:"messages_remaining"`
|
MessagesRemaining int64 `json:"messages_remaining"`
|
||||||
Emails int64 `json:"emails"`
|
Emails int64 `json:"emails"`
|
||||||
EmailsRemaining int64 `json:"emails_remaining"`
|
EmailsRemaining int64 `json:"emails_remaining"`
|
||||||
Topics int64 `json:"topics"`
|
Reservations int64 `json:"reservations"`
|
||||||
TopicsRemaining int64 `json:"topics_remaining"`
|
ReservationsRemaining int64 `json:"reservations_remaining"`
|
||||||
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
||||||
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
||||||
}
|
}
|
||||||
@ -274,7 +274,7 @@ type apiAccountResponse struct {
|
|||||||
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
||||||
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
||||||
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
|
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
|
||||||
Plan *apiAccountPlan `json:"plan,omitempty"`
|
Tier *apiAccountTier `json:"tier,omitempty"`
|
||||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ type visitor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type visitorInfo struct {
|
type visitorInfo struct {
|
||||||
Basis string // "ip", "role" or "plan"
|
Basis string // "ip", "role" or "tier"
|
||||||
Messages int64
|
Messages int64
|
||||||
MessagesLimit int64
|
MessagesLimit int64
|
||||||
MessagesRemaining int64
|
MessagesRemaining int64
|
||||||
@ -50,9 +50,9 @@ type visitorInfo struct {
|
|||||||
Emails int64
|
Emails int64
|
||||||
EmailsLimit int64
|
EmailsLimit int64
|
||||||
EmailsRemaining int64
|
EmailsRemaining int64
|
||||||
Topics int64
|
Reservations int64
|
||||||
TopicsLimit int64
|
ReservationsLimit int64
|
||||||
TopicsRemaining int64
|
ReservationsRemaining int64
|
||||||
AttachmentTotalSize int64
|
AttachmentTotalSize int64
|
||||||
AttachmentTotalSizeLimit int64
|
AttachmentTotalSizeLimit int64
|
||||||
AttachmentTotalSizeRemaining int64
|
AttachmentTotalSizeRemaining int64
|
||||||
@ -69,9 +69,9 @@ func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Mana
|
|||||||
} else {
|
} else {
|
||||||
accountLimiter = rate.NewLimiter(rate.Every(conf.VisitorAccountCreateLimitReplenish), conf.VisitorAccountCreateLimitBurst)
|
accountLimiter = rate.NewLimiter(rate.Every(conf.VisitorAccountCreateLimitReplenish), conf.VisitorAccountCreateLimitBurst)
|
||||||
}
|
}
|
||||||
if user != nil && user.Plan != nil {
|
if user != nil && user.Tier != nil {
|
||||||
requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst)
|
requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Tier.MessagesLimit), conf.VisitorRequestLimitBurst)
|
||||||
emailsLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.EmailsLimit), conf.VisitorEmailLimitBurst)
|
emailsLimiter = rate.NewLimiter(dailyLimitToRate(user.Tier.EmailsLimit), conf.VisitorEmailLimitBurst)
|
||||||
} else {
|
} else {
|
||||||
requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst)
|
requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst)
|
||||||
emailsLimiter = rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst)
|
emailsLimiter = rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst)
|
||||||
@ -183,21 +183,21 @@ func (v *visitor) Info() (*visitorInfo, error) {
|
|||||||
// All limits are zero!
|
// All limits are zero!
|
||||||
info.MessagesExpiryDuration = 24 * 3600 // FIXME this is awful. Should be from the Unlimited plan
|
info.MessagesExpiryDuration = 24 * 3600 // FIXME this is awful. Should be from the Unlimited plan
|
||||||
info.AttachmentExpiryDuration = 24 * 3600 // FIXME this is awful. Should be from the Unlimited plan
|
info.AttachmentExpiryDuration = 24 * 3600 // FIXME this is awful. Should be from the Unlimited plan
|
||||||
} else if v.user != nil && v.user.Plan != nil {
|
} else if v.user != nil && v.user.Tier != nil {
|
||||||
info.Basis = "plan"
|
info.Basis = "tier"
|
||||||
info.MessagesLimit = v.user.Plan.MessagesLimit
|
info.MessagesLimit = v.user.Tier.MessagesLimit
|
||||||
info.MessagesExpiryDuration = v.user.Plan.MessagesExpiryDuration
|
info.MessagesExpiryDuration = v.user.Tier.MessagesExpiryDuration
|
||||||
info.EmailsLimit = v.user.Plan.EmailsLimit
|
info.EmailsLimit = v.user.Tier.EmailsLimit
|
||||||
info.TopicsLimit = v.user.Plan.TopicsLimit
|
info.ReservationsLimit = v.user.Tier.ReservationsLimit
|
||||||
info.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit
|
info.AttachmentTotalSizeLimit = v.user.Tier.AttachmentTotalSizeLimit
|
||||||
info.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit
|
info.AttachmentFileSizeLimit = v.user.Tier.AttachmentFileSizeLimit
|
||||||
info.AttachmentExpiryDuration = v.user.Plan.AttachmentExpiryDuration
|
info.AttachmentExpiryDuration = v.user.Tier.AttachmentExpiryDuration
|
||||||
} else {
|
} else {
|
||||||
info.Basis = "ip"
|
info.Basis = "ip"
|
||||||
info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
|
info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
|
||||||
info.MessagesExpiryDuration = int64(v.config.CacheDuration.Seconds())
|
info.MessagesExpiryDuration = int64(v.config.CacheDuration.Seconds())
|
||||||
info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
|
info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
|
||||||
info.TopicsLimit = 0 // FIXME
|
info.ReservationsLimit = 0 // FIXME
|
||||||
info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
|
info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
|
||||||
info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
|
info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
|
||||||
info.AttachmentExpiryDuration = int64(v.config.AttachmentExpiryDuration.Seconds())
|
info.AttachmentExpiryDuration = int64(v.config.AttachmentExpiryDuration.Seconds())
|
||||||
@ -212,20 +212,19 @@ func (v *visitor) Info() (*visitorInfo, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var topics int64
|
var reservations int64
|
||||||
if v.user != nil && v.userManager != nil {
|
if v.user != nil && v.userManager != nil {
|
||||||
reservations, err := v.userManager.Reservations(v.user.Name) // FIXME dup call, move this to endpoint?
|
reservations, err = v.userManager.ReservationsCount(v.user.Name) // FIXME dup call, move this to endpoint?
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
topics = int64(len(reservations))
|
|
||||||
}
|
}
|
||||||
info.Messages = messages
|
info.Messages = messages
|
||||||
info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
|
info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
|
||||||
info.Emails = emails
|
info.Emails = emails
|
||||||
info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails)
|
info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails)
|
||||||
info.Topics = topics
|
info.Reservations = reservations
|
||||||
info.TopicsRemaining = zeroIfNegative(info.TopicsLimit - info.Topics)
|
info.ReservationsRemaining = zeroIfNegative(info.ReservationsLimit - info.Reservations)
|
||||||
info.AttachmentTotalSize = attachmentsBytesUsed
|
info.AttachmentTotalSize = attachmentsBytesUsed
|
||||||
info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize)
|
info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize)
|
||||||
return info, nil
|
return info, nil
|
||||||
|
@ -32,28 +32,27 @@ var (
|
|||||||
// Manager-related queries
|
// Manager-related queries
|
||||||
const (
|
const (
|
||||||
createTablesQueriesNoTx = `
|
createTablesQueriesNoTx = `
|
||||||
CREATE TABLE IF NOT EXISTS plan (
|
CREATE TABLE IF NOT EXISTS tier (
|
||||||
id INT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
code TEXT NOT NULL,
|
code TEXT NOT NULL,
|
||||||
messages_limit INT NOT NULL,
|
messages_limit INT NOT NULL,
|
||||||
messages_expiry_duration INT NOT NULL,
|
messages_expiry_duration INT NOT NULL,
|
||||||
emails_limit INT NOT NULL,
|
emails_limit INT NOT NULL,
|
||||||
topics_limit INT NOT NULL,
|
reservations_limit INT NOT NULL,
|
||||||
attachment_file_size_limit INT NOT NULL,
|
attachment_file_size_limit INT NOT NULL,
|
||||||
attachment_total_size_limit INT NOT NULL,
|
attachment_total_size_limit INT NOT NULL,
|
||||||
attachment_expiry_duration INT NOT NULL,
|
attachment_expiry_duration INT NOT NULL
|
||||||
PRIMARY KEY (id)
|
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
plan_id INT,
|
tier_id INT,
|
||||||
user TEXT NOT NULL,
|
user TEXT NOT NULL,
|
||||||
pass TEXT NOT NULL,
|
pass TEXT NOT NULL,
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
messages INT NOT NULL DEFAULT (0),
|
messages INT NOT NULL DEFAULT (0),
|
||||||
emails INT NOT NULL DEFAULT (0),
|
emails INT NOT NULL DEFAULT (0),
|
||||||
settings JSON,
|
settings JSON,
|
||||||
FOREIGN KEY (plan_id) REFERENCES plan (id)
|
FOREIGN KEY (tier_id) REFERENCES tier (id)
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX idx_user ON user (user);
|
CREATE UNIQUE INDEX idx_user ON user (user);
|
||||||
CREATE TABLE IF NOT EXISTS user_access (
|
CREATE TABLE IF NOT EXISTS user_access (
|
||||||
@ -85,16 +84,16 @@ const (
|
|||||||
`
|
`
|
||||||
|
|
||||||
selectUserByNameQuery = `
|
selectUserByNameQuery = `
|
||||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.topics_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||||
FROM user u
|
FROM user u
|
||||||
LEFT JOIN plan p on p.id = u.plan_id
|
LEFT JOIN tier p on p.id = u.tier_id
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
`
|
`
|
||||||
selectUserByTokenQuery = `
|
selectUserByTokenQuery = `
|
||||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.topics_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||||
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 tier p on p.id = u.tier_id
|
||||||
WHERE t.token = ? AND t.expires >= ?
|
WHERE t.token = ? AND t.expires >= ?
|
||||||
`
|
`
|
||||||
selectTopicPermsQuery = `
|
selectTopicPermsQuery = `
|
||||||
@ -178,8 +177,14 @@ const (
|
|||||||
ORDER BY expires DESC
|
ORDER BY expires DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
)
|
)
|
||||||
;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
|
insertTierQuery = `
|
||||||
|
INSERT INTO tier (code, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
selectTierIDQuery = `SELECT id FROM tier WHERE code = ?`
|
||||||
|
updateUserTierQuery = `UPDATE user SET tier_id = ? WHERE user = ?`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
@ -523,13 +528,13 @@ func (a *Manager) userByToken(token string) (*User, error) {
|
|||||||
func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var username, hash, role string
|
var username, hash, role string
|
||||||
var settings, planCode sql.NullString
|
var settings, tierCode sql.NullString
|
||||||
var messages, emails int64
|
var messages, emails int64
|
||||||
var messagesLimit, messagesExpiryDuration, emailsLimit, topicsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration sql.NullInt64
|
var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration sql.NullInt64
|
||||||
if !rows.Next() {
|
if !rows.Next() {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &planCode, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &topicsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); err != nil {
|
if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &tierCode, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); 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
|
||||||
@ -549,14 +554,14 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if planCode.Valid {
|
if tierCode.Valid {
|
||||||
user.Plan = &Plan{
|
user.Tier = &Tier{
|
||||||
Code: planCode.String,
|
Code: tierCode.String,
|
||||||
Upgradeable: false,
|
Upgradeable: false,
|
||||||
MessagesLimit: messagesLimit.Int64,
|
MessagesLimit: messagesLimit.Int64,
|
||||||
MessagesExpiryDuration: messagesExpiryDuration.Int64,
|
MessagesExpiryDuration: messagesExpiryDuration.Int64,
|
||||||
EmailsLimit: emailsLimit.Int64,
|
EmailsLimit: emailsLimit.Int64,
|
||||||
TopicsLimit: topicsLimit.Int64,
|
ReservationsLimit: reservationsLimit.Int64,
|
||||||
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
||||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
||||||
AttachmentExpiryDuration: attachmentExpiryDuration.Int64,
|
AttachmentExpiryDuration: attachmentExpiryDuration.Int64,
|
||||||
@ -678,6 +683,30 @@ func (a *Manager) ChangeRole(username string, role Role) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeTier changes a user's tier using the tier code
|
||||||
|
func (a *Manager) ChangeTier(username, tier string) error {
|
||||||
|
if !AllowedUsername(username) {
|
||||||
|
return ErrInvalidArgument
|
||||||
|
}
|
||||||
|
rows, err := a.db.Query(selectTierIDQuery, tier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
return ErrInvalidArgument
|
||||||
|
}
|
||||||
|
var tierID int64
|
||||||
|
if err := rows.Scan(&tierID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
if _, err := a.db.Exec(updateUserTierQuery, tierID, username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CheckAllowAccess tests if a user may create an access control entry for the given topic.
|
// CheckAllowAccess tests if a user may create an access control entry for the given topic.
|
||||||
// If there are any ACL entries that are not owned by the user, an error is returned.
|
// If there are any ACL entries that are not owned by the user, an error is returned.
|
||||||
func (a *Manager) CheckAllowAccess(username string, topic string) error {
|
func (a *Manager) CheckAllowAccess(username string, topic string) error {
|
||||||
@ -743,6 +772,14 @@ func (a *Manager) DefaultAccess() Permission {
|
|||||||
return a.defaultAccess
|
return a.defaultAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTier creates a new tier in the database
|
||||||
|
func (a *Manager) CreateTier(tier *Tier) error {
|
||||||
|
if _, err := a.db.Exec(insertTierQuery, tier.Code, tier.MessagesLimit, tier.MessagesExpiryDuration, tier.EmailsLimit, tier.ReservationsLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, tier.AttachmentExpiryDuration); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func toSQLWildcard(s string) string {
|
func toSQLWildcard(s string) string {
|
||||||
return strings.ReplaceAll(s, "*", "%")
|
return strings.ReplaceAll(s, "*", "%")
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ type User struct {
|
|||||||
Token string // Only set if token was used to log in
|
Token string // Only set if token was used to log in
|
||||||
Role Role
|
Role Role
|
||||||
Prefs *Prefs
|
Prefs *Prefs
|
||||||
Plan *Plan
|
Tier *Tier
|
||||||
Stats *Stats
|
Stats *Stats
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,27 +43,27 @@ type Prefs struct {
|
|||||||
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
|
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanCode is code identifying a user's plan
|
// TierCode is code identifying a user's tier
|
||||||
type PlanCode string
|
type TierCode string
|
||||||
|
|
||||||
// Default plan codes
|
// Default tier codes
|
||||||
const (
|
const (
|
||||||
PlanUnlimited = PlanCode("unlimited")
|
TierUnlimited = TierCode("unlimited")
|
||||||
PlanDefault = PlanCode("default")
|
TierDefault = TierCode("default")
|
||||||
PlanNone = PlanCode("none")
|
TierNone = TierCode("none")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Plan represents a user's account type, including its account limits
|
// Tier represents a user's account type, including its account limits
|
||||||
type Plan struct {
|
type Tier struct {
|
||||||
Code string `json:"name"`
|
Code string `json:"name"`
|
||||||
Upgradeable bool `json:"upgradeable"`
|
Upgradeable bool `json:"upgradeable"`
|
||||||
MessagesLimit int64 `json:"messages_limit"`
|
MessagesLimit int64 `json:"messages_limit"`
|
||||||
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
||||||
EmailsLimit int64 `json:"emails_limit"`
|
EmailsLimit int64 `json:"emails_limit"`
|
||||||
TopicsLimit int64 `json:"topics_limit"`
|
ReservationsLimit int64 `json:"reservations_limit"`
|
||||||
AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"`
|
AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"`
|
||||||
AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"`
|
AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"`
|
||||||
AttachmentExpiryDuration int64 `json:"attachment_expiry_seconds"`
|
AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscription represents a user's topic subscription
|
// Subscription represents a user's topic subscription
|
||||||
|
@ -178,13 +178,13 @@
|
|||||||
"account_usage_of_limit": "of {{limit}}",
|
"account_usage_of_limit": "of {{limit}}",
|
||||||
"account_usage_unlimited": "Unlimited",
|
"account_usage_unlimited": "Unlimited",
|
||||||
"account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
|
"account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
|
||||||
"account_usage_plan_title": "Account type",
|
"account_usage_tier_title": "Account type",
|
||||||
"account_usage_plan_code_default": "Default",
|
"account_usage_tier_code_default": "Default",
|
||||||
"account_usage_plan_code_unlimited": "Unlimited",
|
"account_usage_tier_code_unlimited": "Unlimited",
|
||||||
"account_usage_plan_code_none": "None",
|
"account_usage_tier_code_none": "None",
|
||||||
"account_usage_plan_code_pro": "Pro",
|
"account_usage_tier_code_pro": "Pro",
|
||||||
"account_usage_plan_code_business": "Business",
|
"account_usage_tier_code_business": "Business",
|
||||||
"account_usage_plan_code_business_plus": "Business Plus",
|
"account_usage_tier_code_business_plus": "Business Plus",
|
||||||
"account_usage_messages_title": "Published messages",
|
"account_usage_messages_title": "Published messages",
|
||||||
"account_usage_emails_title": "Emails sent",
|
"account_usage_emails_title": "Emails sent",
|
||||||
"account_usage_topics_title": "Reserved topics",
|
"account_usage_topics_title": "Reserved topics",
|
||||||
|
@ -169,7 +169,7 @@ const Stats = () => {
|
|||||||
if (!account) {
|
if (!account) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
const planCode = account.plan.code ?? "none";
|
const tierCode = account.tier.code ?? "none";
|
||||||
const normalize = (value, max) => Math.min(value / max * 100, 100);
|
const normalize = (value, max) => Math.min(value / max * 100, 100);
|
||||||
const barColor = (remaining, limit) => {
|
const barColor = (remaining, limit) => {
|
||||||
if (account.role === "admin") {
|
if (account.role === "admin") {
|
||||||
@ -186,12 +186,12 @@ const Stats = () => {
|
|||||||
{t("account_usage_title")}
|
{t("account_usage_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<PrefGroup>
|
<PrefGroup>
|
||||||
<Pref title={t("account_usage_plan_title")}>
|
<Pref title={t("account_usage_tier_title")}>
|
||||||
<div>
|
<div>
|
||||||
{account.role === "admin"
|
{account.role === "admin"
|
||||||
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
||||||
: t(`account_usage_plan_code_${planCode}`)}
|
: t(`account_usage_tier_code_${tierCode}`)}
|
||||||
{config.enable_payments && account.plan.upgradeable &&
|
{config.enable_payments && account.tier.upgradeable &&
|
||||||
<em>{" "}
|
<em>{" "}
|
||||||
<Link onClick={() => {}}>Upgrade</Link>
|
<Link onClick={() => {}}>Upgrade</Link>
|
||||||
</em>
|
</em>
|
||||||
@ -199,20 +199,20 @@ const Stats = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref title={t("account_usage_topics_title")}>
|
<Pref title={t("account_usage_topics_title")}>
|
||||||
{account.limits.topics > 0 &&
|
{account.limits.reservations > 0 &&
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.reservations}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.reservations }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
|
value={account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
|
||||||
color={barColor(account.stats.topics_remaining, account.limits.topics)}
|
color={barColor(account.stats.reservations_remaining, account.limits.reservations)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{account.limits.topics === 0 &&
|
{account.limits.reservations === 0 &&
|
||||||
<em>No reserved topics for this account</em>
|
<em>No reserved topics for this account</em>
|
||||||
}
|
}
|
||||||
</Pref>
|
</Pref>
|
||||||
|
@ -99,7 +99,7 @@ const NavList = (props) => {
|
|||||||
navigate(routes.account);
|
navigate(routes.account);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showUpgradeBanner = config.enable_payments && (!props.account || props.account.plan.upgradeable);
|
const showUpgradeBanner = config.enable_payments && (!props.account || props.account.tier.upgradeable);
|
||||||
const showSubscriptionsList = props.subscriptions?.length > 0;
|
const showSubscriptionsList = props.subscriptions?.length > 0;
|
||||||
const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
|
const showNotificationBrowserNotSupportedBox = !notifier.browserSupported();
|
||||||
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
|
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
|
||||||
|
@ -489,7 +489,7 @@ const Reservations = () => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
const reservations = account.reservations || [];
|
const reservations = account.reservations || [];
|
||||||
const limitReached = account.role === "user" && account.stats.topics_remaining === 0;
|
const limitReached = account.role === "user" && account.stats.reservations_remaining === 0;
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
setDialogKey(prev => prev+1);
|
setDialogKey(prev => prev+1);
|
||||||
|
@ -87,7 +87,7 @@ const SubscribePage = (props) => {
|
|||||||
const existingBaseUrls = Array
|
const existingBaseUrls = Array
|
||||||
.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
|
||||||
.filter(s => s !== config.base_url);
|
.filter(s => s !== config.base_url);
|
||||||
//const reserveTopicEnabled = session.exists() && (account?.stats.topics_remaining || 0) > 0;
|
//const reserveTopicEnabled = session.exists() && (account?.stats.reservations_remaining || 0) > 0;
|
||||||
|
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
const user = await userManager.get(baseUrl); // May be undefined
|
const user = await userManager.get(baseUrl); // May be undefined
|
||||||
@ -184,7 +184,7 @@ const SubscribePage = (props) => {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
fullWidth
|
fullWidth
|
||||||
// disabled={account.stats.topics_remaining}
|
// disabled={account.stats.reservations_remaining}
|
||||||
checked={reserveTopicVisible}
|
checked={reserveTopicVisible}
|
||||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
|
Loading…
Reference in New Issue
Block a user