diff --git a/server/errors.go b/server/errors.go index e136287d..2b197d14 100644 --- a/server/errors.go +++ b/server/errors.go @@ -69,8 +69,9 @@ var ( errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"} - errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} - errHTTPTooManyRequestsAccountCreateLimit = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: daily account creation limit reached", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit + errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth", "https://ntfy.sh/docs/publish/#limitations"} + errHTTPTooManyRequestsLimitAccountCreation = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many accounts created", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit + errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", ""} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"} diff --git a/server/server.go b/server/server.go index 48b8dfbe..961b6ca4 100644 --- a/server/server.go +++ b/server/server.go @@ -36,26 +36,31 @@ import ( /* TODO - limits: + limits & rate limiting: message cache duration Keep 10000 messages or keep X days? Attachment expiration based on plan + login/account endpoints plan: weirdness with admin and "default" account - "account topic" sync mechanism v.Info() endpoint double selects from DB - JS constants purge accounts that were not logged into in X reset daily limits for users + Make sure account endpoints make sense for admins UI: - flicker of upgrade banner + - JS constants + - useContext for account Sync: + - "account topic" sync mechanism - "mute" setting - figure out what settings are "web" or "phone" - rate limiting: - - login/account endpoints Tests: + - /access endpoints - visitor with/without user + Refactor: + - rename TopicsLimit -> ReservationsLimit + - rename /access -> /reservation Later: - Password reset - Pricing @@ -496,7 +501,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) } if r.Method == http.MethodGet { if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil { - return errHTTPTooManyRequestsAttachmentBandwidthLimit + return errHTTPTooManyRequestsLimitAttachmentBandwidth } } w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) diff --git a/server/server_account.go b/server/server_account.go index cdd1c62b..b436bb26 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "errors" "heckel.io/ntfy/user" "heckel.io/ntfy/util" "net/http" @@ -29,7 +30,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return errHTTPConflictUserExists } if v.accountLimiter != nil && !v.accountLimiter.Allow() { - return errHTTPTooManyRequestsAccountCreateLimit + return errHTTPTooManyRequestsLimitAccountCreation } if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User return err @@ -331,6 +332,15 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request, if !topicRegex.MatchString(req.Topic) { return errHTTPBadRequestTopicInvalid } + if v.user.Plan == nil { + return errors.New("no plan") // FIXME there should always be a plan! + } + reservations, err := s.userManager.ReservationsCount(v.user.Name) + if err != nil { + return err + } else if reservations >= v.user.Plan.TopicsLimit { + return errHTTPTooManyRequestsLimitReservations // FIXME test this + } if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil { return errHTTPConflictTopicReserved } diff --git a/user/manager.go b/user/manager.go index d7e069c4..ab9928cb 100644 --- a/user/manager.go +++ b/user/manager.go @@ -140,6 +140,11 @@ const ( AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?) ORDER BY a_user.topic ` + selectUserReservationsCountQuery = ` + SELECT COUNT(*) + FROM user_access + WHERE user_id = owner_user_id AND owner_user_id = (SELECT id FROM user WHERE user = ?) + ` selectOtherAccessCountQuery = ` SELECT COUNT(*) FROM user_access @@ -599,6 +604,23 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { return reservations, nil } +// ReservationsCount returns the number of reservations owned by this user +func (a *Manager) ReservationsCount(username string) (int64, error) { + rows, err := a.db.Query(selectUserReservationsCountQuery, username) + if err != nil { + return 0, err + } + defer rows.Close() + if !rows.Next() { + return 0, errNoRows + } + var count int64 + if err := rows.Scan(&count); err != nil { + return 0, err + } + return count, nil +} + // ChangePassword changes a user's password func (a *Manager) ChangePassword(username, password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)