ntfy/server/server_account.go

386 lines
12 KiB
Go
Raw Normal View History

2022-12-16 03:07:04 +00:00
package server
import (
"encoding/json"
"heckel.io/ntfy/user"
2022-12-16 03:07:04 +00:00
"heckel.io/ntfy/util"
"net/http"
)
const (
2022-12-29 16:09:45 +00:00
jsonBodyBytesLimit = 4096
subscriptionIDLength = 16
)
2022-12-16 03:07:04 +00:00
func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
admin := v.user != nil && v.user.Role == user.RoleAdmin
2022-12-24 17:10:51 +00:00
if !admin {
if !s.config.EnableSignup {
return errHTTPBadRequestSignupNotEnabled
} else if v.user != nil {
return errHTTPUnauthorized // Cannot create account from user context
}
2022-12-16 03:07:04 +00:00
}
2022-12-29 14:57:42 +00:00
newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit)
2022-12-16 03:07:04 +00:00
if err != nil {
return err
}
if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
2022-12-22 02:55:39 +00:00
return errHTTPConflictUserExists
}
2022-12-24 17:10:51 +00:00
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
2022-12-16 03:07:04 +00:00
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
return nil
}
2023-01-02 01:42:33 +00:00
func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *visitor) error {
2022-12-28 03:14:14 +00:00
stats, err := v.Info()
2022-12-17 20:17:52 +00:00
if err != nil {
return err
}
2022-12-28 03:14:14 +00:00
response := &apiAccountResponse{
2022-12-19 21:22:13 +00:00
Stats: &apiAccountStats{
Messages: stats.Messages,
MessagesRemaining: stats.MessagesRemaining,
Emails: stats.Emails,
EmailsRemaining: stats.EmailsRemaining,
2023-01-02 01:42:33 +00:00
Topics: stats.Topics,
TopicsRemaining: stats.TopicsRemaining,
2022-12-19 21:22:13 +00:00
AttachmentTotalSize: stats.AttachmentTotalSize,
AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
},
Limits: &apiAccountLimits{
Basis: stats.Basis,
Messages: stats.MessagesLimit,
Emails: stats.EmailsLimit,
2023-01-02 01:42:33 +00:00
Topics: stats.TopicsLimit,
2022-12-19 21:22:13 +00:00
AttachmentTotalSize: stats.AttachmentTotalSizeLimit,
AttachmentFileSize: stats.AttachmentFileSizeLimit,
},
2022-12-17 20:17:52 +00:00
}
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 {
2022-12-19 21:22:13 +00:00
response.Plan = &apiAccountPlan{
2023-01-05 03:47:12 +00:00
Code: v.user.Plan.Code,
Upgradeable: v.user.Plan.Upgradeable,
2022-12-19 14:59:32 +00:00
}
} else if v.user.Role == user.RoleAdmin {
2022-12-20 02:42:36 +00:00
response.Plan = &apiAccountPlan{
2023-01-05 03:47:12 +00:00
Code: string(user.PlanUnlimited),
Upgradeable: false,
2022-12-20 02:42:36 +00:00
}
} else {
2022-12-20 02:42:36 +00:00
response.Plan = &apiAccountPlan{
2023-01-05 03:47:12 +00:00
Code: string(user.PlanDefault),
Upgradeable: true,
}
2022-12-17 20:17:52 +00:00
}
2023-01-03 01:08:37 +00:00
reservations, err := s.userManager.Reservations(v.user.Name)
if err != nil {
return err
}
if len(reservations) > 0 {
response.Reservations = make([]*apiAccountReservation, 0)
for _, r := range reservations {
response.Reservations = append(response.Reservations, &apiAccountReservation{
Topic: r.Topic,
Everyone: r.Everyone.String(),
2023-01-03 01:08:37 +00:00
})
2023-01-01 20:21:43 +00:00
}
}
2022-12-17 20:17:52 +00:00
} else {
response.Username = user.Everyone
response.Role = string(user.RoleAnonymous)
2022-12-19 21:22:13 +00:00
response.Plan = &apiAccountPlan{
2023-01-05 03:47:12 +00:00
Code: string(user.PlanNone),
Upgradeable: true,
2022-12-19 14:59:32 +00:00
}
2022-12-17 20:17:52 +00:00
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
2022-12-17 20:17:52 +00:00
if err := json.NewEncoder(w).Encode(response); err != nil {
return err
}
return nil
}
2023-01-02 01:42:33 +00:00
func (s *Server) handleAccountDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error {
if err := s.userManager.RemoveUser(v.user.Name); err != nil {
2022-12-16 03:07:04 +00:00
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 {
2022-12-29 14:57:42 +00:00
newPassword, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit)
2022-12-16 03:07:04 +00:00
if err != nil {
return err
}
if err := s.userManager.ChangePassword(v.user.Name, newPassword.Password); err != nil {
2022-12-16 03:07:04 +00:00
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
return nil
}
2023-01-02 01:42:33 +00:00
func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, _ *http.Request, v *visitor) error {
2022-12-16 03:07:04 +00:00
// TODO rate limit
token, err := s.userManager.CreateToken(v.user)
2022-12-16 03:07:04 +00:00
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,
2022-12-28 18:46:18 +00:00
Expires: token.Expires.Unix(),
}
if err := json.NewEncoder(w).Encode(response); err != nil {
return err
}
return nil
}
2023-01-02 01:42:33 +00:00
func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, _ *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,
2022-12-28 18:46:18 +00:00
Expires: token.Expires.Unix(),
2022-12-16 03:07:04 +00:00
}
if err := json.NewEncoder(w).Encode(response); err != nil {
return err
}
return nil
}
2023-01-02 01:42:33 +00:00
func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, _ *http.Request, v *visitor) error {
2022-12-16 03:07:04 +00:00
// TODO rate limit
if v.user.Token == "" {
2022-12-29 14:57:42 +00:00
return errHTTPBadRequestNoTokenProvided
2022-12-16 03:07:04 +00:00
}
if err := s.userManager.RemoveToken(v.user); err != nil {
2022-12-16 03:07:04 +00:00
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 {
2022-12-29 14:57:42 +00:00
newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit)
2022-12-16 03:07:04 +00:00
if err != nil {
return err
}
if v.user.Prefs == nil {
v.user.Prefs = &user.Prefs{}
2022-12-16 03:07:04 +00:00
}
prefs := v.user.Prefs
if newPrefs.Language != "" {
prefs.Language = newPrefs.Language
}
if newPrefs.Notification != nil {
if prefs.Notification == nil {
prefs.Notification = &user.NotificationPrefs{}
2022-12-16 03:07:04 +00:00
}
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
2022-12-16 03:07:04 +00:00
}
func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
2022-12-29 14:57:42 +00:00
newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
2022-12-16 03:07:04 +00:00
if err != nil {
return err
}
if v.user.Prefs == nil {
v.user.Prefs = &user.Prefs{}
2022-12-16 03:07:04 +00:00
}
newSubscription.ID = "" // Client cannot set ID
for _, subscription := range v.user.Prefs.Subscriptions {
if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
2022-12-26 03:29:55 +00:00
newSubscription = subscription
2022-12-16 03:07:04 +00:00
break
}
}
if newSubscription.ID == "" {
2022-12-29 16:09:45 +00:00
newSubscription.ID = util.RandomString(subscriptionIDLength)
2022-12-26 03:29:55 +00:00
v.user.Prefs.Subscriptions = append(v.user.Prefs.Subscriptions, newSubscription)
if err := s.userManager.ChangeSettings(v.user); err != nil {
2022-12-16 03:07:04 +00:00
return err
}
}
2022-12-26 03:29:55 +00:00
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
2022-12-16 03:07:04 +00:00
if err := json.NewEncoder(w).Encode(newSubscription); err != nil {
return err
}
return nil
}
2022-12-26 03:29:55 +00:00
func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
2022-12-29 14:57:42 +00:00
return errHTTPInternalErrorInvalidPath
2022-12-26 03:29:55 +00:00
}
2022-12-29 14:57:42 +00:00
subscriptionID := matches[1]
updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit)
2022-12-26 03:29:55 +00:00
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
}
2022-12-16 03:07:04 +00:00
func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
2022-12-29 14:57:42 +00:00
return errHTTPInternalErrorInvalidPath
2022-12-16 03:07:04 +00:00
}
subscriptionID := matches[1]
if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
return nil
}
newSubscriptions := make([]*user.Subscription, 0)
2022-12-16 03:07:04 +00:00
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 {
2022-12-16 03:07:04 +00:00
return err
}
}
2022-12-29 14:57:42 +00:00
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
2022-12-16 03:07:04 +00:00
return nil
}
2022-12-30 19:20:48 +00:00
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
}
2023-01-01 20:21:43 +00:00
if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
return errHTTPConflictTopicReserved
}
owner, username := v.user.Name, v.user.Name
everyone, err := user.ParsePermission(req.Everyone)
if err != nil {
return errHTTPBadRequestPermissionInvalid
}
2023-01-01 20:21:43 +00:00
if err := s.userManager.AllowAccess(owner, username, req.Topic, true, true); err != nil {
return err
}
if err := s.userManager.AllowAccess(owner, user.Everyone, req.Topic, everyone.IsRead(), everyone.IsWrite()); err != nil {
2023-01-01 20:21:43 +00:00
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
return nil
}
func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
matches := accountAccessSingleRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
return errHTTPInternalErrorInvalidPath
}
topic := matches[1]
if !topicRegex.MatchString(topic) {
return errHTTPBadRequestTopicInvalid
}
2023-01-03 01:08:37 +00:00
reservations, err := s.userManager.Reservations(v.user.Name) // FIXME replace with HasReservation
if err != nil {
return err
}
2023-01-01 20:21:43 +00:00
authorized := false
2023-01-03 01:08:37 +00:00
for _, r := range reservations {
if r.Topic == topic {
2023-01-01 20:21:43 +00:00
authorized = true
break
}
}
if !authorized {
return errHTTPUnauthorized
}
if err := s.userManager.ResetAccess(v.user.Name, topic); err != nil {
2022-12-30 19:20:48 +00:00
return err
}
2023-01-01 20:21:43 +00:00
if err := s.userManager.ResetAccess(user.Everyone, topic); err != nil {
2022-12-30 19:20:48 +00:00
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
return nil
}