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} | ||||
| 	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} | ||||
| 	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} | ||||
| 	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} | ||||
|  |  | |||
|  | @ -413,7 +413,11 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit | |||
| 		return s.handleHealth(w, r, v) | ||||
| 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { | ||||
| 		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) | ||||
| 	} else if r.Method == http.MethodDelete && r.URL.Path == apiAccessPath { | ||||
| 		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 | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	Username   string `json:"username"` | ||||
| 	Topic      string `json:"topic"` | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue