server_account.go 14 KB

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