server_account.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. package server
  2. import (
  3. "encoding/json"
  4. "heckel.io/ntfy/user"
  5. "heckel.io/ntfy/util"
  6. "net/http"
  7. )
  8. const (
  9. jsonBodyBytesLimit = 4096
  10. subscriptionIDLength = 16
  11. )
  12. func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
  13. admin := v.user != nil && v.user.Role == user.RoleAdmin
  14. if !admin {
  15. if !s.config.EnableSignup {
  16. return errHTTPBadRequestSignupNotEnabled
  17. } else if v.user != nil {
  18. return errHTTPUnauthorized // Cannot create account from user context
  19. }
  20. }
  21. newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit)
  22. if err != nil {
  23. return err
  24. }
  25. if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
  26. return errHTTPConflictUserExists
  27. }
  28. if v.accountLimiter != nil && !v.accountLimiter.Allow() {
  29. return errHTTPTooManyRequestsLimitAccountCreation
  30. }
  31. if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
  32. return err
  33. }
  34. w.Header().Set("Content-Type", "application/json")
  35. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  36. return nil
  37. }
  38. func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *visitor) error {
  39. info, err := v.Info()
  40. if err != nil {
  41. return err
  42. }
  43. limits, stats := info.Limits, info.Stats
  44. response := &apiAccountResponse{
  45. Limits: &apiAccountLimits{
  46. Basis: string(limits.Basis),
  47. Messages: limits.MessagesLimit,
  48. MessagesExpiryDuration: int64(limits.MessagesExpiryDuration.Seconds()),
  49. Emails: limits.EmailsLimit,
  50. Reservations: limits.ReservationsLimit,
  51. AttachmentTotalSize: limits.AttachmentTotalSizeLimit,
  52. AttachmentFileSize: limits.AttachmentFileSizeLimit,
  53. AttachmentExpiryDuration: int64(limits.AttachmentExpiryDuration.Seconds()),
  54. },
  55. Stats: &apiAccountStats{
  56. Messages: stats.Messages,
  57. MessagesRemaining: stats.MessagesRemaining,
  58. Emails: stats.Emails,
  59. EmailsRemaining: stats.EmailsRemaining,
  60. Reservations: stats.Reservations,
  61. ReservationsRemaining: stats.ReservationsRemaining,
  62. AttachmentTotalSize: stats.AttachmentTotalSize,
  63. AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
  64. },
  65. }
  66. if v.user != nil {
  67. response.Username = v.user.Name
  68. response.Role = string(v.user.Role)
  69. if v.user.Prefs != nil {
  70. if v.user.Prefs.Language != "" {
  71. response.Language = v.user.Prefs.Language
  72. }
  73. if v.user.Prefs.Notification != nil {
  74. response.Notification = v.user.Prefs.Notification
  75. }
  76. if v.user.Prefs.Subscriptions != nil {
  77. response.Subscriptions = v.user.Prefs.Subscriptions
  78. }
  79. }
  80. if v.user.Tier != nil {
  81. response.Tier = &apiAccountTier{
  82. Code: v.user.Tier.Code,
  83. Name: v.user.Tier.Name,
  84. Paid: v.user.Tier.Paid,
  85. }
  86. }
  87. reservations, err := s.userManager.Reservations(v.user.Name)
  88. if err != nil {
  89. return err
  90. }
  91. if len(reservations) > 0 {
  92. response.Reservations = make([]*apiAccountReservation, 0)
  93. for _, r := range reservations {
  94. response.Reservations = append(response.Reservations, &apiAccountReservation{
  95. Topic: r.Topic,
  96. Everyone: r.Everyone.String(),
  97. })
  98. }
  99. }
  100. } else {
  101. response.Username = user.Everyone
  102. response.Role = string(user.RoleAnonymous)
  103. }
  104. w.Header().Set("Content-Type", "application/json")
  105. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  106. if err := json.NewEncoder(w).Encode(response); err != nil {
  107. return err
  108. }
  109. return nil
  110. }
  111. func (s *Server) handleAccountDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error {
  112. if err := s.userManager.RemoveUser(v.user.Name); err != nil {
  113. return err
  114. }
  115. w.Header().Set("Content-Type", "application/json")
  116. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  117. return nil
  118. }
  119. func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
  120. newPassword, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit)
  121. if err != nil {
  122. return err
  123. }
  124. if err := s.userManager.ChangePassword(v.user.Name, newPassword.Password); err != nil {
  125. return err
  126. }
  127. w.Header().Set("Content-Type", "application/json")
  128. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  129. return nil
  130. }
  131. func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, _ *http.Request, v *visitor) error {
  132. // TODO rate limit
  133. token, err := s.userManager.CreateToken(v.user)
  134. if err != nil {
  135. return err
  136. }
  137. w.Header().Set("Content-Type", "application/json")
  138. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  139. response := &apiAccountTokenResponse{
  140. Token: token.Value,
  141. Expires: token.Expires.Unix(),
  142. }
  143. if err := json.NewEncoder(w).Encode(response); err != nil {
  144. return err
  145. }
  146. return nil
  147. }
  148. func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, _ *http.Request, v *visitor) error {
  149. // TODO rate limit
  150. if v.user == nil {
  151. return errHTTPUnauthorized
  152. } else if v.user.Token == "" {
  153. return errHTTPBadRequestNoTokenProvided
  154. }
  155. token, err := s.userManager.ExtendToken(v.user)
  156. if err != nil {
  157. return err
  158. }
  159. w.Header().Set("Content-Type", "application/json")
  160. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  161. response := &apiAccountTokenResponse{
  162. Token: token.Value,
  163. Expires: token.Expires.Unix(),
  164. }
  165. if err := json.NewEncoder(w).Encode(response); err != nil {
  166. return err
  167. }
  168. return nil
  169. }
  170. func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error {
  171. // TODO rate limit
  172. if v.user.Token == "" {
  173. return errHTTPBadRequestNoTokenProvided
  174. }
  175. if err := s.userManager.RemoveToken(v.user); err != nil {
  176. return err
  177. }
  178. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  179. return nil
  180. }
  181. func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
  182. newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit)
  183. if err != nil {
  184. return err
  185. }
  186. if v.user.Prefs == nil {
  187. v.user.Prefs = &user.Prefs{}
  188. }
  189. prefs := v.user.Prefs
  190. if newPrefs.Language != "" {
  191. prefs.Language = newPrefs.Language
  192. }
  193. if newPrefs.Notification != nil {
  194. if prefs.Notification == nil {
  195. prefs.Notification = &user.NotificationPrefs{}
  196. }
  197. if newPrefs.Notification.DeleteAfter > 0 {
  198. prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
  199. }
  200. if newPrefs.Notification.Sound != "" {
  201. prefs.Notification.Sound = newPrefs.Notification.Sound
  202. }
  203. if newPrefs.Notification.MinPriority > 0 {
  204. prefs.Notification.MinPriority = newPrefs.Notification.MinPriority
  205. }
  206. }
  207. if err := s.userManager.ChangeSettings(v.user); err != nil {
  208. return err
  209. }
  210. w.Header().Set("Content-Type", "application/json")
  211. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  212. return nil
  213. }
  214. func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
  215. newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
  216. if err != nil {
  217. return err
  218. }
  219. if v.user.Prefs == nil {
  220. v.user.Prefs = &user.Prefs{}
  221. }
  222. newSubscription.ID = "" // Client cannot set ID
  223. for _, subscription := range v.user.Prefs.Subscriptions {
  224. if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
  225. newSubscription = subscription
  226. break
  227. }
  228. }
  229. if newSubscription.ID == "" {
  230. newSubscription.ID = util.RandomString(subscriptionIDLength)
  231. v.user.Prefs.Subscriptions = append(v.user.Prefs.Subscriptions, newSubscription)
  232. if err := s.userManager.ChangeSettings(v.user); err != nil {
  233. return err
  234. }
  235. }
  236. w.Header().Set("Content-Type", "application/json")
  237. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  238. if err := json.NewEncoder(w).Encode(newSubscription); err != nil {
  239. return err
  240. }
  241. return nil
  242. }
  243. func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
  244. matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
  245. if len(matches) != 2 {
  246. return errHTTPInternalErrorInvalidPath
  247. }
  248. subscriptionID := matches[1]
  249. updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
  250. if err != nil {
  251. return err
  252. }
  253. if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
  254. return errHTTPNotFound
  255. }
  256. var subscription *user.Subscription
  257. for _, sub := range v.user.Prefs.Subscriptions {
  258. if sub.ID == subscriptionID {
  259. sub.DisplayName = updatedSubscription.DisplayName
  260. subscription = sub
  261. break
  262. }
  263. }
  264. if subscription == nil {
  265. return errHTTPNotFound
  266. }
  267. if err := s.userManager.ChangeSettings(v.user); err != nil {
  268. return err
  269. }
  270. w.Header().Set("Content-Type", "application/json")
  271. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  272. if err := json.NewEncoder(w).Encode(subscription); err != nil {
  273. return err
  274. }
  275. return nil
  276. }
  277. func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
  278. matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
  279. if len(matches) != 2 {
  280. return errHTTPInternalErrorInvalidPath
  281. }
  282. subscriptionID := matches[1]
  283. if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
  284. return nil
  285. }
  286. newSubscriptions := make([]*user.Subscription, 0)
  287. for _, subscription := range v.user.Prefs.Subscriptions {
  288. if subscription.ID != subscriptionID {
  289. newSubscriptions = append(newSubscriptions, subscription)
  290. }
  291. }
  292. if len(newSubscriptions) < len(v.user.Prefs.Subscriptions) {
  293. v.user.Prefs.Subscriptions = newSubscriptions
  294. if err := s.userManager.ChangeSettings(v.user); err != nil {
  295. return err
  296. }
  297. }
  298. w.Header().Set("Content-Type", "application/json")
  299. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  300. return nil
  301. }
  302. func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
  303. if v.user != nil && v.user.Role == user.RoleAdmin {
  304. return errHTTPBadRequestMakesNoSenseForAdmin
  305. }
  306. req, err := readJSONWithLimit[apiAccountAccessRequest](r.Body, jsonBodyBytesLimit)
  307. if err != nil {
  308. return err
  309. }
  310. if !topicRegex.MatchString(req.Topic) {
  311. return errHTTPBadRequestTopicInvalid
  312. }
  313. everyone, err := user.ParsePermission(req.Everyone)
  314. if err != nil {
  315. return errHTTPBadRequestPermissionInvalid
  316. }
  317. if v.user.Tier == nil {
  318. return errHTTPUnauthorized // FIXME there should always be a plan!
  319. }
  320. if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
  321. return errHTTPConflictTopicReserved
  322. }
  323. hasReservation, err := s.userManager.HasReservation(v.user.Name, req.Topic)
  324. if err != nil {
  325. return err
  326. }
  327. if !hasReservation {
  328. reservations, err := s.userManager.ReservationsCount(v.user.Name)
  329. if err != nil {
  330. return err
  331. } else if reservations >= v.user.Tier.ReservationsLimit {
  332. return errHTTPTooManyRequestsLimitReservations
  333. }
  334. }
  335. owner, username := v.user.Name, v.user.Name
  336. if err := s.userManager.AllowAccess(owner, username, req.Topic, true, true); err != nil {
  337. return err
  338. }
  339. if err := s.userManager.AllowAccess(owner, user.Everyone, req.Topic, everyone.IsRead(), everyone.IsWrite()); err != nil {
  340. return err
  341. }
  342. w.Header().Set("Content-Type", "application/json")
  343. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  344. return nil
  345. }
  346. func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
  347. matches := accountAccessSingleRegex.FindStringSubmatch(r.URL.Path)
  348. if len(matches) != 2 {
  349. return errHTTPInternalErrorInvalidPath
  350. }
  351. topic := matches[1]
  352. if !topicRegex.MatchString(topic) {
  353. return errHTTPBadRequestTopicInvalid
  354. }
  355. authorized, err := s.userManager.HasReservation(v.user.Name, topic)
  356. if err != nil {
  357. return err
  358. } else if !authorized {
  359. return errHTTPUnauthorized
  360. }
  361. if err := s.userManager.ResetAccess(v.user.Name, topic); err != nil {
  362. return err
  363. }
  364. if err := s.userManager.ResetAccess(user.Everyone, topic); err != nil {
  365. return err
  366. }
  367. w.Header().Set("Content-Type", "application/json")
  368. w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
  369. return nil
  370. }