User endpoint
This commit is contained in:
		
							parent
							
								
									625b13280f
								
							
						
					
					
						commit
						97fc287b78
					
				
					 6 changed files with 123 additions and 51 deletions
				
			
		| 
						 | 
					@ -106,6 +106,8 @@ var (
 | 
				
			||||||
	errHTTPBadRequestNotAPaidUser                    = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil}
 | 
						errHTTPBadRequestNotAPaidUser                    = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil}
 | 
				
			||||||
	errHTTPBadRequestBillingRequestInvalid           = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil}
 | 
						errHTTPBadRequestBillingRequestInvalid           = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil}
 | 
				
			||||||
	errHTTPBadRequestBillingSubscriptionExists       = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil}
 | 
						errHTTPBadRequestBillingSubscriptionExists       = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil}
 | 
				
			||||||
 | 
						errHTTPBadRequestTierInvalid                     = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil}
 | 
				
			||||||
 | 
						errHTTPBadRequestUserNotFound                    = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil}
 | 
				
			||||||
	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
 | 
						errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
 | 
				
			||||||
	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
 | 
						errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
 | 
				
			||||||
	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
 | 
						errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -413,7 +413,11 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 | 
				
			||||||
		return s.handleHealth(w, r, v)
 | 
							return s.handleHealth(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.MethodPost && r.URL.Path == apiAccessPath {
 | 
						} else if r.Method == http.MethodPut && r.URL.Path == apiUserPath {
 | 
				
			||||||
 | 
							return s.ensureAdmin(s.handleUserAdd)(w, r, v)
 | 
				
			||||||
 | 
						} else if r.Method == http.MethodDelete && r.URL.Path == apiUserPath {
 | 
				
			||||||
 | 
							return s.ensureAdmin(s.handleUserDelete)(w, r, v)
 | 
				
			||||||
 | 
						} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiAccessPath {
 | 
				
			||||||
		return s.ensureAdmin(s.handleAccessAllow)(w, r, v)
 | 
							return s.ensureAdmin(s.handleAccessAllow)(w, r, v)
 | 
				
			||||||
	} else if r.Method == http.MethodDelete && r.URL.Path == apiAccessPath {
 | 
						} else if r.Method == http.MethodDelete && r.URL.Path == apiAccessPath {
 | 
				
			||||||
		return s.ensureAdmin(s.handleAccessReset)(w, r, v)
 | 
							return s.ensureAdmin(s.handleAccessReset)(w, r, v)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,50 +0,0 @@
 | 
				
			||||||
package server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"heckel.io/ntfy/user"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
					 | 
				
			||||||
	req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, jsonBodyBytesLimit, false)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	permission, err := user.ParsePermission(req.Permission)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return errHTTPBadRequestPermissionInvalid
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := s.userManager.AllowAccess(req.Username, req.Topic, permission); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return s.writeJSON(w, newSuccessResponse())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
					 | 
				
			||||||
	req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, jsonBodyBytesLimit, false)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	u, err := s.userManager.User(req.Username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := s.userManager.ResetAccess(req.Username, req.Topic); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := s.killUserSubscriber(u, req.Topic); err != nil { // This may be a pattern
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return s.writeJSON(w, newSuccessResponse())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Server) killUserSubscriber(u *user.User, topicPattern string) error {
 | 
					 | 
				
			||||||
	topics, err := s.topicsFromPattern(topicPattern)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	for _, t := range topics {
 | 
					 | 
				
			||||||
		t.CancelSubscriberUser(u.ID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										105
									
								
								server/server_admin.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								server/server_admin.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,105 @@
 | 
				
			||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"heckel.io/ntfy/user"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) handleUserAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
 | 
						req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if !user.AllowedUsername(req.Username) || req.Password == "" {
 | 
				
			||||||
 | 
							return errHTTPBadRequest.Wrap("username invalid, or password missing")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u, err := s.userManager.User(req.Username)
 | 
				
			||||||
 | 
						if err != nil && err != user.ErrUserNotFound {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if u != nil {
 | 
				
			||||||
 | 
							return errHTTPConflictUserExists
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var tier *user.Tier
 | 
				
			||||||
 | 
						if req.Tier != "" {
 | 
				
			||||||
 | 
							tier, err = s.userManager.Tier(req.Tier)
 | 
				
			||||||
 | 
							if err == user.ErrTierNotFound {
 | 
				
			||||||
 | 
								return errHTTPBadRequestTierInvalid
 | 
				
			||||||
 | 
							} else if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if tier != nil {
 | 
				
			||||||
 | 
							if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.writeJSON(w, newSuccessResponse())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) handleUserDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
 | 
						req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u, err := s.userManager.User(req.Username)
 | 
				
			||||||
 | 
						if err == user.ErrUserNotFound {
 | 
				
			||||||
 | 
							return errHTTPBadRequestUserNotFound
 | 
				
			||||||
 | 
						} else if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else if !u.IsUser() {
 | 
				
			||||||
 | 
							return errHTTPUnauthorized.Wrap("can only remove regular users from API")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.userManager.RemoveUser(req.Username); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.killUserSubscriber(u, "*"); err != nil { // FIXME super inefficient
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.writeJSON(w, newSuccessResponse())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
 | 
						req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, jsonBodyBytesLimit, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						permission, err := user.ParsePermission(req.Permission)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errHTTPBadRequestPermissionInvalid
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.userManager.AllowAccess(req.Username, req.Topic, permission); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.writeJSON(w, newSuccessResponse())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Request, v *visitor) error {
 | 
				
			||||||
 | 
						req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, jsonBodyBytesLimit, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u, err := s.userManager.User(req.Username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.userManager.ResetAccess(req.Username, req.Topic); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.killUserSubscriber(u, req.Topic); err != nil { // This may be a pattern
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.writeJSON(w, newSuccessResponse())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Server) killUserSubscriber(u *user.User, topicPattern string) error {
 | 
				
			||||||
 | 
						topics, err := s.topicsFromPattern(topicPattern)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, t := range topics {
 | 
				
			||||||
 | 
							t.CancelSubscriberUser(u.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -244,6 +244,17 @@ type apiStatsResponse struct {
 | 
				
			||||||
	MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
 | 
						MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type apiUserAddRequest struct {
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
						Password string `json:"password"`
 | 
				
			||||||
 | 
						Tier     string `json:"tier"`
 | 
				
			||||||
 | 
						// Do not add 'role' here. We don't want to add admins via the API.
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type apiUserDeleteRequest struct {
 | 
				
			||||||
 | 
						Username string `json:"username"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type apiAccessAllowRequest struct {
 | 
					type apiAccessAllowRequest struct {
 | 
				
			||||||
	Username   string `json:"username"`
 | 
						Username   string `json:"username"`
 | 
				
			||||||
	Topic      string `json:"topic"`
 | 
						Topic      string `json:"topic"`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue