| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- package server
- import (
- "encoding/json"
- "heckel.io/ntfy/user"
- "heckel.io/ntfy/util"
- "net/http"
- )
- const (
- jsonBodyBytesLimit = 4096
- subscriptionIDLength = 16
- )
- func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
- admin := v.user != nil && v.user.Role == user.RoleAdmin
- if !admin {
- if !s.config.EnableSignup {
- return errHTTPBadRequestSignupNotEnabled
- } else if v.user != nil {
- return errHTTPUnauthorized // Cannot create account from user context
- }
- }
- newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
- return errHTTPConflictUserExists
- }
- if v.accountLimiter != nil && !v.accountLimiter.Allow() {
- return errHTTPTooManyRequestsAccountCreateLimit
- }
- if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
- stats, err := v.Info()
- if err != nil {
- return err
- }
- response := &apiAccountResponse{
- Stats: &apiAccountStats{
- Messages: stats.Messages,
- MessagesRemaining: stats.MessagesRemaining,
- Emails: stats.Emails,
- EmailsRemaining: stats.EmailsRemaining,
- AttachmentTotalSize: stats.AttachmentTotalSize,
- AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
- },
- Limits: &apiAccountLimits{
- Basis: stats.Basis,
- Messages: stats.MessagesLimit,
- Emails: stats.EmailsLimit,
- AttachmentTotalSize: stats.AttachmentTotalSizeLimit,
- AttachmentFileSize: stats.AttachmentFileSizeLimit,
- },
- }
- if v.user != nil {
- response.Username = v.user.Name
- response.Role = string(v.user.Role)
- if v.user.Prefs != nil {
- if v.user.Prefs.Language != "" {
- response.Language = v.user.Prefs.Language
- }
- if v.user.Prefs.Notification != nil {
- response.Notification = v.user.Prefs.Notification
- }
- if v.user.Prefs.Subscriptions != nil {
- response.Subscriptions = v.user.Prefs.Subscriptions
- }
- }
- if v.user.Plan != nil {
- response.Plan = &apiAccountPlan{
- Code: v.user.Plan.Code,
- Upgradable: v.user.Plan.Upgradable,
- }
- } else if v.user.Role == user.RoleAdmin {
- response.Plan = &apiAccountPlan{
- Code: string(user.PlanUnlimited),
- Upgradable: false,
- }
- } else {
- response.Plan = &apiAccountPlan{
- Code: string(user.PlanDefault),
- Upgradable: true,
- }
- }
- } else {
- response.Username = user.Everyone
- response.Role = string(user.RoleAnonymous)
- response.Plan = &apiAccountPlan{
- Code: string(user.PlanNone),
- Upgradable: true,
- }
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- if err := json.NewEncoder(w).Encode(response); err != nil {
- return err
- }
- return nil
- }
- func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
- if err := s.userManager.RemoveUser(v.user.Name); err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
- newPassword, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if err := s.userManager.ChangePassword(v.user.Name, newPassword.Password); err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, r *http.Request, v *visitor) error {
- // TODO rate limit
- token, err := s.userManager.CreateToken(v.user)
- if err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- response := &apiAccountTokenResponse{
- Token: token.Value,
- Expires: token.Expires.Unix(),
- }
- if err := json.NewEncoder(w).Encode(response); err != nil {
- return err
- }
- return nil
- }
- func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, r *http.Request, v *visitor) error {
- // TODO rate limit
- if v.user == nil {
- return errHTTPUnauthorized
- } else if v.user.Token == "" {
- return errHTTPBadRequestNoTokenProvided
- }
- token, err := s.userManager.ExtendToken(v.user)
- if err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- response := &apiAccountTokenResponse{
- Token: token.Value,
- Expires: token.Expires.Unix(),
- }
- if err := json.NewEncoder(w).Encode(response); err != nil {
- return err
- }
- return nil
- }
- func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
- // TODO rate limit
- if v.user.Token == "" {
- return errHTTPBadRequestNoTokenProvided
- }
- if err := s.userManager.RemoveToken(v.user); err != nil {
- return err
- }
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
- newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if v.user.Prefs == nil {
- v.user.Prefs = &user.Prefs{}
- }
- prefs := v.user.Prefs
- if newPrefs.Language != "" {
- prefs.Language = newPrefs.Language
- }
- if newPrefs.Notification != nil {
- if prefs.Notification == nil {
- prefs.Notification = &user.NotificationPrefs{}
- }
- if newPrefs.Notification.DeleteAfter > 0 {
- prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
- }
- if newPrefs.Notification.Sound != "" {
- prefs.Notification.Sound = newPrefs.Notification.Sound
- }
- if newPrefs.Notification.MinPriority > 0 {
- prefs.Notification.MinPriority = newPrefs.Notification.MinPriority
- }
- }
- if err := s.userManager.ChangeSettings(v.user); err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
- newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if v.user.Prefs == nil {
- v.user.Prefs = &user.Prefs{}
- }
- newSubscription.ID = "" // Client cannot set ID
- for _, subscription := range v.user.Prefs.Subscriptions {
- if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
- newSubscription = subscription
- break
- }
- }
- if newSubscription.ID == "" {
- newSubscription.ID = util.RandomString(subscriptionIDLength)
- v.user.Prefs.Subscriptions = append(v.user.Prefs.Subscriptions, newSubscription)
- if err := s.userManager.ChangeSettings(v.user); err != nil {
- return err
- }
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- if err := json.NewEncoder(w).Encode(newSubscription); err != nil {
- return err
- }
- return nil
- }
- func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
- matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
- if len(matches) != 2 {
- return errHTTPInternalErrorInvalidPath
- }
- subscriptionID := matches[1]
- updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
- return errHTTPNotFound
- }
- var subscription *user.Subscription
- for _, sub := range v.user.Prefs.Subscriptions {
- if sub.ID == subscriptionID {
- sub.DisplayName = updatedSubscription.DisplayName
- subscription = sub
- break
- }
- }
- if subscription == nil {
- return errHTTPNotFound
- }
- if err := s.userManager.ChangeSettings(v.user); err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- if err := json.NewEncoder(w).Encode(subscription); err != nil {
- return err
- }
- return nil
- }
- func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
- matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
- if len(matches) != 2 {
- return errHTTPInternalErrorInvalidPath
- }
- subscriptionID := matches[1]
- if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
- return nil
- }
- newSubscriptions := make([]*user.Subscription, 0)
- for _, subscription := range v.user.Prefs.Subscriptions {
- if subscription.ID != subscriptionID {
- newSubscriptions = append(newSubscriptions, subscription)
- }
- }
- if len(newSubscriptions) < len(v.user.Prefs.Subscriptions) {
- v.user.Prefs.Subscriptions = newSubscriptions
- if err := s.userManager.ChangeSettings(v.user); err != nil {
- return err
- }
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
- func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
- req, err := readJSONWithLimit[apiAccountAccessRequest](r.Body, jsonBodyBytesLimit)
- if err != nil {
- return err
- }
- if !topicRegex.MatchString(req.Topic) {
- return errHTTPBadRequestTopicInvalid
- }
- if err := s.userManager.AllowAccess(v.user.Name, req.Topic, true, true); err != nil {
- return err
- }
- if err := s.userManager.AllowAccess(user.Everyone, req.Topic, false, false); err != nil {
- return err
- }
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
- return nil
- }
|