Reject reservation limits in endpoint
This commit is contained in:
		
							parent
							
								
									1bc40693bb
								
							
						
					
					
						commit
						a51d95743a
					
				
					 4 changed files with 47 additions and 9 deletions
				
			
		|  | @ -69,8 +69,9 @@ var ( | ||||||
| 	errHTTPTooManyRequestsLimitEmails                = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | 	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"} | 	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"} | 	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"} | 	errHTTPTooManyRequestsLimitAttachmentBandwidth   = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth", "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 | 	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", ""} | 	errHTTPInternalError                             = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} | ||||||
| 	errHTTPInternalErrorInvalidPath                  = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""} | 	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/"} | 	errHTTPInternalErrorMissingBaseURL               = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"} | ||||||
|  |  | ||||||
|  | @ -36,26 +36,31 @@ import ( | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| 	TODO | 	TODO | ||||||
| 		limits: | 		limits & rate limiting: | ||||||
| 			message cache duration | 			message cache duration | ||||||
| 			Keep 10000 messages or keep X days? | 			Keep 10000 messages or keep X days? | ||||||
| 			Attachment expiration based on plan | 			Attachment expiration based on plan | ||||||
|  | 			login/account endpoints | ||||||
| 		plan: | 		plan: | ||||||
| 			weirdness with admin and "default" account | 			weirdness with admin and "default" account | ||||||
| 		"account topic" sync mechanism |  | ||||||
| 		v.Info() endpoint double selects from DB | 		v.Info() endpoint double selects from DB | ||||||
| 		JS constants |  | ||||||
| 		purge accounts that were not logged into in X | 		purge accounts that were not logged into in X | ||||||
| 		reset daily limits for users | 		reset daily limits for users | ||||||
|  | 		Make sure account endpoints make sense for admins | ||||||
| 		UI: | 		UI: | ||||||
| 		- flicker of upgrade banner | 		- flicker of upgrade banner | ||||||
|  | 		- JS constants | ||||||
|  | 		- useContext for account | ||||||
| 		Sync: | 		Sync: | ||||||
|  | 			- "account topic" sync mechanism | ||||||
| 			- "mute" setting | 			- "mute" setting | ||||||
| 			- figure out what settings are "web" or "phone" | 			- figure out what settings are "web" or "phone" | ||||||
| 		rate limiting: |  | ||||||
| 		- login/account endpoints |  | ||||||
| 		Tests: | 		Tests: | ||||||
|  | 		- /access endpoints | ||||||
| 		- visitor with/without user | 		- visitor with/without user | ||||||
|  | 		Refactor: | ||||||
|  | 		- rename TopicsLimit -> ReservationsLimit | ||||||
|  | 		- rename /access -> /reservation | ||||||
| 		Later: | 		Later: | ||||||
| 		- Password reset | 		- Password reset | ||||||
| 		- Pricing | 		- Pricing | ||||||
|  | @ -496,7 +501,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) | ||||||
| 	} | 	} | ||||||
| 	if r.Method == http.MethodGet { | 	if r.Method == http.MethodGet { | ||||||
| 		if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil { | 		if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil { | ||||||
| 			return errHTTPTooManyRequestsAttachmentBandwidthLimit | 			return errHTTPTooManyRequestsLimitAttachmentBandwidth | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) | 	w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"heckel.io/ntfy/user" | 	"heckel.io/ntfy/user" | ||||||
| 	"heckel.io/ntfy/util" | 	"heckel.io/ntfy/util" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | @ -29,7 +30,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * | ||||||
| 		return errHTTPConflictUserExists | 		return errHTTPConflictUserExists | ||||||
| 	} | 	} | ||||||
| 	if v.accountLimiter != nil && !v.accountLimiter.Allow() { | 	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 | 	if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User | ||||||
| 		return err | 		return err | ||||||
|  | @ -331,6 +332,15 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request, | ||||||
| 	if !topicRegex.MatchString(req.Topic) { | 	if !topicRegex.MatchString(req.Topic) { | ||||||
| 		return errHTTPBadRequestTopicInvalid | 		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 { | 	if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil { | ||||||
| 		return errHTTPConflictTopicReserved | 		return errHTTPConflictTopicReserved | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -140,6 +140,11 @@ const ( | ||||||
| 		  AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?) | 		  AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?) | ||||||
| 		ORDER BY a_user.topic | 		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 = ` | 	selectOtherAccessCountQuery = ` | ||||||
| 		SELECT COUNT(*) | 		SELECT COUNT(*) | ||||||
| 		FROM user_access | 		FROM user_access | ||||||
|  | @ -599,6 +604,23 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { | ||||||
| 	return reservations, nil | 	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 | // ChangePassword changes a user's password | ||||||
| func (a *Manager) ChangePassword(username, password string) error { | func (a *Manager) ChangePassword(username, password string) error { | ||||||
| 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) | 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue