ntfy/server/server_account.go

629 lines
20 KiB
Go
Raw Permalink Normal View History

2022-12-16 04:07:04 +01:00
package server
import (
"encoding/json"
2023-02-08 04:45:55 +01:00
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
2022-12-16 04:07:04 +01:00
"heckel.io/ntfy/util"
"net/http"
2023-01-29 02:29:06 +01:00
"net/netip"
"strings"
2023-01-28 05:10:59 +01:00
"time"
2022-12-16 04:07:04 +01:00
)
const (
syncTopicAccountSyncEvent = "sync"
2023-01-28 05:10:59 +01:00
tokenExpiryDuration = 72 * time.Hour // Extend tokens by this much
)
2022-12-16 04:07:04 +01:00
func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-29 02:43:06 +01:00
u := v.User()
2023-02-10 03:51:12 +01:00
if !u.IsAdmin() { // u may be nil, but that's fine
2022-12-24 18:10:51 +01:00
if !s.config.EnableSignup {
return errHTTPBadRequestSignupNotEnabled
2023-01-29 02:43:06 +01:00
} else if u != nil {
2022-12-24 18:10:51 +01:00
return errHTTPUnauthorized // Cannot create account from user context
}
if !v.AccountCreationAllowed() {
2023-01-27 04:57:18 +01:00
return errHTTPTooManyRequestsLimitAccountCreation
}
2022-12-16 04:07:04 +01:00
}
2023-01-28 05:10:59 +01:00
newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false)
2022-12-16 04:07:04 +01:00
if err != nil {
return err
}
if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
2022-12-22 03:55:39 +01:00
return errHTTPConflictUserExists
}
2023-02-08 04:45:55 +01:00
logvr(v, r).Tag(tagAccount).Field("user_name", newAccount.Username).Info("Creating user %s", newAccount.Username)
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil {
2022-12-16 04:07:04 +01:00
return err
}
2023-02-08 21:20:44 +01:00
v.AccountCreated()
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
2023-02-07 22:20:49 +01:00
func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-09 21:40:46 +01:00
info, err := v.Info()
2022-12-17 21:17:52 +01:00
if err != nil {
return err
}
2023-02-07 22:20:49 +01:00
logvr(v, r).Tag(tagAccount).Fields(visitorExtendedInfoContext(info)).Debug("Retrieving account stats")
2023-01-09 21:40:46 +01:00
limits, stats := info.Limits, info.Stats
2022-12-28 04:14:14 +01:00
response := &apiAccountResponse{
2023-01-09 21:40:46 +01:00
Limits: &apiAccountLimits{
Basis: string(limits.Basis),
2023-01-27 04:57:18 +01:00
Messages: limits.MessageLimit,
MessagesExpiryDuration: int64(limits.MessageExpiryDuration.Seconds()),
Emails: limits.EmailLimit,
2023-05-07 17:59:15 +02:00
Calls: limits.CallLimit,
2023-01-09 21:40:46 +01:00
Reservations: limits.ReservationsLimit,
AttachmentTotalSize: limits.AttachmentTotalSizeLimit,
AttachmentFileSize: limits.AttachmentFileSizeLimit,
AttachmentExpiryDuration: int64(limits.AttachmentExpiryDuration.Seconds()),
AttachmentBandwidth: limits.AttachmentBandwidthLimit,
2023-01-09 21:40:46 +01:00
},
2022-12-19 22:22:13 +01:00
Stats: &apiAccountStats{
Messages: stats.Messages,
MessagesRemaining: stats.MessagesRemaining,
Emails: stats.Emails,
EmailsRemaining: stats.EmailsRemaining,
2023-05-07 17:59:15 +02:00
Calls: stats.Calls,
CallsRemaining: stats.CallsRemaining,
Reservations: stats.Reservations,
ReservationsRemaining: stats.ReservationsRemaining,
2022-12-19 22:22:13 +01:00
AttachmentTotalSize: stats.AttachmentTotalSize,
AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
},
2022-12-17 21:17:52 +01:00
}
2023-01-28 05:10:59 +01:00
u := v.User()
if u != nil {
response.Username = u.Name
response.Role = string(u.Role)
response.SyncTopic = u.SyncTopic
if u.Prefs != nil {
if u.Prefs.Language != nil {
response.Language = *u.Prefs.Language
2022-12-17 21:17:52 +01:00
}
2023-01-28 05:10:59 +01:00
if u.Prefs.Notification != nil {
response.Notification = u.Prefs.Notification
2022-12-17 21:17:52 +01:00
}
2023-01-28 05:10:59 +01:00
if u.Prefs.Subscriptions != nil {
response.Subscriptions = u.Prefs.Subscriptions
2022-12-17 21:17:52 +01:00
}
}
2023-01-28 05:10:59 +01:00
if u.Tier != nil {
response.Tier = &apiAccountTier{
2023-01-28 05:10:59 +01:00
Code: u.Tier.Code,
Name: u.Tier.Name,
}
2022-12-17 21:17:52 +01:00
}
2023-01-28 05:10:59 +01:00
if u.Billing.StripeCustomerID != "" {
2023-01-16 05:29:46 +01:00
response.Billing = &apiAccountBilling{
Customer: true,
2023-01-28 05:10:59 +01:00
Subscription: u.Billing.StripeSubscriptionID != "",
Status: string(u.Billing.StripeSubscriptionStatus),
2023-02-22 04:44:30 +01:00
Interval: string(u.Billing.StripeSubscriptionInterval),
2023-01-28 05:10:59 +01:00
PaidUntil: u.Billing.StripeSubscriptionPaidUntil.Unix(),
CancelAt: u.Billing.StripeSubscriptionCancelAt.Unix(),
2023-01-16 05:29:46 +01:00
}
}
2023-05-17 16:58:28 +02:00
if s.config.EnableReservations {
reservations, err := s.userManager.Reservations(u.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-01 21:21:43 +01:00
}
}
2023-01-28 05:10:59 +01:00
tokens, err := s.userManager.Tokens(u.ID)
if err != nil {
return err
}
if len(tokens) > 0 {
response.Tokens = make([]*apiAccountTokenResponse, 0)
for _, t := range tokens {
2023-01-29 02:29:06 +01:00
var lastOrigin string
if t.LastOrigin != netip.IPv4Unspecified() {
lastOrigin = t.LastOrigin.String()
}
2023-01-28 05:10:59 +01:00
response.Tokens = append(response.Tokens, &apiAccountTokenResponse{
2023-01-29 02:29:06 +01:00
Token: t.Value,
Label: t.Label,
LastAccess: t.LastAccess.Unix(),
LastOrigin: lastOrigin,
Expires: t.Expires.Unix(),
2023-01-28 05:10:59 +01:00
})
}
}
2023-05-17 16:58:28 +02:00
if s.config.TwilioAccount != "" {
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
if err != nil {
return err
}
if len(phoneNumbers) > 0 {
response.PhoneNumbers = phoneNumbers
}
2023-05-11 19:50:10 +02:00
}
2022-12-17 21:17:52 +01:00
} else {
response.Username = user.Everyone
response.Role = string(user.RoleAnonymous)
2022-12-17 21:17:52 +01:00
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, response)
2022-12-17 21:17:52 +01:00
}
2023-01-23 04:21:30 +01:00
func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-28 05:10:59 +01:00
req, err := readJSONWithLimit[apiAccountDeleteRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil {
return err
} else if req.Password == "" {
return errHTTPBadRequest
}
2023-01-29 02:43:06 +01:00
u := v.User()
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
return errHTTPBadRequestIncorrectPasswordConfirmation
}
2023-06-17 20:44:55 +02:00
if s.webPush != nil && u.ID != "" {
2023-06-09 05:09:38 +02:00
if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil {
logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name)
}
}
2023-01-29 02:43:06 +01:00
if u.Billing.StripeSubscriptionID != "" {
2023-02-06 05:34:27 +01:00
logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name)
2023-01-29 02:43:06 +01:00
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {
2023-01-23 04:21:30 +01:00
return err
}
2023-01-19 20:03:39 +01:00
}
2023-02-05 03:26:01 +01:00
if err := s.maybeRemoveMessagesAndExcessReservations(r, v, u, 0); err != nil {
return err
}
2023-02-04 04:21:50 +01:00
logvr(v, r).Tag(tagAccount).Info("Marking user %s as deleted", u.Name)
2023-01-29 02:43:06 +01:00
if err := s.userManager.MarkUserRemoved(u); err != nil {
2022-12-16 04:07:04 +01:00
return err
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-28 05:10:59 +01:00
req, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit, false)
2022-12-16 04:07:04 +01:00
if err != nil {
return err
} else if req.Password == "" || req.NewPassword == "" {
return errHTTPBadRequest
2022-12-16 04:07:04 +01:00
}
2023-01-29 02:43:06 +01:00
u := v.User()
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
return errHTTPBadRequestIncorrectPasswordConfirmation
}
2023-02-08 04:45:55 +01:00
logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name)
2023-01-29 02:43:06 +01:00
if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil {
2022-12-16 04:07:04 +01:00
return err
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
2023-01-28 05:10:59 +01:00
func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
req, err := readJSONWithLimit[apiAccountTokenIssueRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body!
if err != nil {
return err
}
var label string
if req.Label != nil {
label = *req.Label
}
expires := time.Now().Add(tokenExpiryDuration)
if req.Expires != nil {
expires = time.Unix(*req.Expires, 0)
}
2023-02-04 04:21:50 +01:00
u := v.User()
2023-02-08 04:45:55 +01:00
logvr(v, r).
Tag(tagAccount).
Fields(log.Context{
"token_label": label,
"token_expires": expires,
}).
Debug("Creating token for user %s", u.Name)
2023-02-04 04:21:50 +01:00
token, err := s.userManager.CreateToken(u.ID, label, expires, v.IP())
2022-12-16 04:07:04 +01:00
if err != nil {
return err
}
response := &apiAccountTokenResponse{
2023-01-29 02:29:06 +01:00
Token: token.Value,
Label: token.Label,
LastAccess: token.LastAccess.Unix(),
LastOrigin: token.LastOrigin.String(),
Expires: token.Expires.Unix(),
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, response)
}
2023-01-28 05:10:59 +01:00
func (s *Server) handleAccountTokenUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User()
req, err := readJSONWithLimit[apiAccountTokenUpdateRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body!
if err != nil {
return err
} else if req.Token == "" {
req.Token = u.Token
if req.Token == "" {
return errHTTPBadRequestNoTokenProvided
}
}
2023-01-28 05:10:59 +01:00
var expires *time.Time
if req.Expires != nil {
expires = util.Time(time.Unix(*req.Expires, 0))
} else if req.Label == nil {
2023-02-08 04:45:55 +01:00
expires = util.Time(time.Now().Add(tokenExpiryDuration)) // If label/expires not set, extend token by 72 hours
}
logvr(v, r).
Tag(tagAccount).
Fields(log.Context{
"token_label": req.Label,
"token_expires": expires,
}).
Debug("Updating token for user %s as deleted", u.Name)
2023-01-28 05:10:59 +01:00
token, err := s.userManager.ChangeToken(u.ID, req.Token, req.Label, expires)
if err != nil {
return err
}
response := &apiAccountTokenResponse{
2023-01-29 02:29:06 +01:00
Token: token.Value,
Label: token.Label,
LastAccess: token.LastAccess.Unix(),
LastOrigin: token.LastOrigin.String(),
Expires: token.Expires.Unix(),
2022-12-16 04:07:04 +01:00
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, response)
2022-12-16 04:07:04 +01:00
}
2023-01-28 05:10:59 +01:00
func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User()
token := readParam(r, "X-Token", "Token") // DELETEs cannot have a body, and we don't want it in the path
if token == "" {
token = u.Token
if token == "" {
return errHTTPBadRequestNoTokenProvided
}
2022-12-16 04:07:04 +01:00
}
2023-01-28 05:10:59 +01:00
if err := s.userManager.RemoveToken(u.ID, token); err != nil {
2022-12-16 04:07:04 +01:00
return err
}
2023-02-08 04:45:55 +01:00
logvr(v, r).
Tag(tagAccount).
Field("token", token).
Debug("Deleted token for user %s", u.Name)
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-28 05:10:59 +01:00
newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit, false)
2022-12-16 04:07:04 +01:00
if err != nil {
return err
}
2023-01-29 02:43:06 +01:00
u := v.User()
if u.Prefs == nil {
u.Prefs = &user.Prefs{}
2022-12-16 04:07:04 +01:00
}
2023-01-29 02:43:06 +01:00
prefs := u.Prefs
if newPrefs.Language != nil {
2022-12-16 04:07:04 +01:00
prefs.Language = newPrefs.Language
}
if newPrefs.Notification != nil {
if prefs.Notification == nil {
prefs.Notification = &user.NotificationPrefs{}
2022-12-16 04:07:04 +01:00
}
if newPrefs.Notification.DeleteAfter != nil {
2022-12-16 04:07:04 +01:00
prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
}
if newPrefs.Notification.Sound != nil {
2022-12-16 04:07:04 +01:00
prefs.Notification.Sound = newPrefs.Notification.Sound
}
if newPrefs.Notification.MinPriority != nil {
2022-12-16 04:07:04 +01:00
prefs.Notification.MinPriority = newPrefs.Notification.MinPriority
}
}
2023-02-08 04:45:55 +01:00
logvr(v, r).Tag(tagAccount).Debug("Changing account settings for user %s", u.Name)
2023-02-09 21:24:12 +01:00
if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
return err
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-28 05:10:59 +01:00
newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false)
2022-12-16 04:07:04 +01:00
if err != nil {
return err
}
2023-01-29 02:43:06 +01:00
u := v.User()
2023-02-12 20:09:44 +01:00
prefs := u.Prefs
if prefs == nil {
prefs = &user.Prefs{}
2022-12-16 04:07:04 +01:00
}
2023-02-12 20:09:44 +01:00
for _, subscription := range prefs.Subscriptions {
2022-12-16 04:07:04 +01:00
if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
2023-02-12 20:09:44 +01:00
return errHTTPConflictSubscriptionExists
2022-12-16 04:07:04 +01:00
}
}
2023-02-12 20:09:44 +01:00
prefs.Subscriptions = append(prefs.Subscriptions, newSubscription)
logvr(v, r).Tag(tagAccount).With(newSubscription).Debug("Adding subscription for user %s", u.Name)
if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
return err
2022-12-16 04:07:04 +01:00
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSubscription)
2022-12-16 04:07:04 +01:00
}
2022-12-26 04:29:55 +01:00
func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-28 05:10:59 +01:00
updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false)
2022-12-26 04:29:55 +01:00
if err != nil {
return err
}
2023-01-29 02:43:06 +01:00
u := v.User()
2023-02-12 20:09:44 +01:00
prefs := u.Prefs
if prefs == nil || prefs.Subscriptions == nil {
2022-12-26 04:29:55 +01:00
return errHTTPNotFound
}
var subscription *user.Subscription
2023-02-09 21:24:12 +01:00
for _, sub := range prefs.Subscriptions {
2023-02-12 20:09:44 +01:00
if sub.BaseURL == updatedSubscription.BaseURL && sub.Topic == updatedSubscription.Topic {
2022-12-26 04:29:55 +01:00
sub.DisplayName = updatedSubscription.DisplayName
subscription = sub
break
}
}
if subscription == nil {
return errHTTPNotFound
}
2023-02-12 20:09:44 +01:00
logvr(v, r).Tag(tagAccount).With(subscription).Debug("Changing subscription for user %s", u.Name)
2023-02-09 21:24:12 +01:00
if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
2022-12-26 04:29:55 +01:00
return err
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, subscription)
2022-12-26 04:29:55 +01:00
}
2022-12-16 04:07:04 +01:00
func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-02-12 20:09:44 +01:00
// DELETEs cannot have a body, and we don't want it in the path
deleteBaseURL := readParam(r, "X-BaseURL", "BaseURL")
deleteTopic := readParam(r, "X-Topic", "Topic")
2023-01-29 02:43:06 +01:00
u := v.User()
2023-02-12 20:09:44 +01:00
prefs := u.Prefs
if prefs == nil || prefs.Subscriptions == nil {
2022-12-16 04:07:04 +01:00
return nil
}
newSubscriptions := make([]*user.Subscription, 0)
2023-02-12 20:09:44 +01:00
for _, sub := range u.Prefs.Subscriptions {
if sub.BaseURL == deleteBaseURL && sub.Topic == deleteTopic {
logvr(v, r).Tag(tagAccount).With(sub).Debug("Removing subscription for user %s", u.Name)
2023-02-08 04:45:55 +01:00
} else {
2023-02-12 20:09:44 +01:00
newSubscriptions = append(newSubscriptions, sub)
2022-12-16 04:07:04 +01:00
}
}
2023-02-12 20:09:44 +01:00
if len(newSubscriptions) < len(prefs.Subscriptions) {
2023-02-09 21:24:12 +01:00
prefs.Subscriptions = newSubscriptions
if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
2022-12-16 04:07:04 +01:00
return err
}
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-16 04:07:04 +01:00
}
2022-12-30 20:20:48 +01:00
2023-02-02 21:19:37 +01:00
// handleAccountReservationAdd adds a topic reservation for the logged-in user, but only if the user has a tier
// with enough remaining reservations left, or if the user is an admin. Admins can always reserve a topic, unless
// it is already reserved by someone else.
2023-01-12 16:50:09 +01:00
func (s *Server) handleAccountReservationAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-29 02:43:06 +01:00
u := v.User()
2023-01-28 05:10:59 +01:00
req, err := readJSONWithLimit[apiAccountReservationRequest](r.Body, jsonBodyBytesLimit, false)
2022-12-30 20:20:48 +01:00
if err != nil {
return err
}
if !topicRegex.MatchString(req.Topic) {
return errHTTPBadRequestTopicInvalid
}
2023-01-06 16:45:38 +01:00
everyone, err := user.ParsePermission(req.Everyone)
if err != nil {
return errHTTPBadRequestPermissionInvalid
}
2023-02-02 21:19:37 +01:00
// Check if we are allowed to reserve this topic
2023-02-10 03:51:12 +01:00
if u.IsUser() && u.Tier == nil {
2023-01-12 16:50:09 +01:00
return errHTTPUnauthorized
2023-02-12 04:14:09 +01:00
} else if err := s.userManager.AllowReservation(u.Name, req.Topic); err != nil {
2023-01-01 21:21:43 +01:00
return errHTTPConflictTopicReserved
2023-02-10 03:51:12 +01:00
} else if u.IsUser() {
2023-02-02 21:19:37 +01:00
hasReservation, err := s.userManager.HasReservation(u.Name, req.Topic)
2023-01-06 16:45:38 +01:00
if err != nil {
return err
2023-02-02 21:19:37 +01:00
}
if !hasReservation {
reservations, err := s.userManager.ReservationsCount(u.Name)
if err != nil {
return err
} else if reservations >= u.Tier.ReservationLimit {
return errHTTPTooManyRequestsLimitReservations
}
2023-01-06 16:45:38 +01:00
}
}
// Actually add the reservation
2023-02-08 04:45:55 +01:00
logvr(v, r).
Tag(tagAccount).
Fields(log.Context{
"topic": req.Topic,
"everyone": everyone.String(),
}).
Debug("Adding topic reservation")
2023-01-29 02:43:06 +01:00
if err := s.userManager.AddReservation(u.Name, req.Topic, everyone); err != nil {
2023-01-01 21:21:43 +01:00
return err
}
// Kill existing subscribers
t, err := s.topicFromID(req.Topic)
if err != nil {
return err
}
2023-05-13 20:39:31 +02:00
t.CancelSubscribersExceptUser(u.ID)
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2023-01-01 21:21:43 +01:00
}
2023-02-02 21:19:37 +01:00
// handleAccountReservationDelete deletes a topic reservation if it is owned by the current user
2023-01-12 16:50:09 +01:00
func (s *Server) handleAccountReservationDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-01-17 16:09:37 +01:00
matches := apiAccountReservationSingleRegex.FindStringSubmatch(r.URL.Path)
2023-01-01 21:21:43 +01:00
if len(matches) != 2 {
return errHTTPInternalErrorInvalidPath
}
topic := matches[1]
if !topicRegex.MatchString(topic) {
return errHTTPBadRequestTopicInvalid
}
2023-01-29 02:43:06 +01:00
u := v.User()
authorized, err := s.userManager.HasReservation(u.Name, topic)
2023-01-03 02:08:37 +01:00
if err != nil {
return err
2023-01-06 16:45:38 +01:00
} else if !authorized {
2023-01-01 21:21:43 +01:00
return errHTTPUnauthorized
}
2023-02-08 04:45:55 +01:00
deleteMessages := readBoolParam(r, false, "X-Delete-Messages", "Delete-Messages")
logvr(v, r).
Tag(tagAccount).
Fields(log.Context{
"topic": topic,
"delete_messages": deleteMessages,
}).
Debug("Removing topic reservation")
2023-01-29 02:43:06 +01:00
if err := s.userManager.RemoveReservations(u.Name, topic); err != nil {
2022-12-30 20:20:48 +01:00
return err
}
2023-02-01 03:39:30 +01:00
if deleteMessages {
if err := s.messageCache.ExpireMessages(topic); err != nil {
return err
}
2023-02-13 03:05:24 +01:00
s.pruneMessages()
2023-02-01 03:39:30 +01:00
}
2023-01-18 21:50:06 +01:00
return s.writeJSON(w, newSuccessResponse())
2022-12-30 20:20:48 +01:00
}
// maybeRemoveMessagesAndExcessReservations deletes topic reservations for the given user (if too many for tier),
// and marks associated messages for the topics as deleted. This also eventually deletes attachments.
// The process relies on the manager to perform the actual deletions (see runManager).
2023-02-05 03:26:01 +01:00
func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *visitor, u *user.User, reservationsLimit int64) error {
reservations, err := s.userManager.Reservations(u.Name)
if err != nil {
return err
} else if int64(len(reservations)) <= reservationsLimit {
2023-02-05 03:26:01 +01:00
logvr(v, r).Tag(tagAccount).Debug("No excess reservations to remove")
return nil
}
topics := make([]string, 0)
for i := int64(len(reservations)) - 1; i >= reservationsLimit; i-- {
topics = append(topics, reservations[i].Topic)
}
2023-02-05 03:26:01 +01:00
logvr(v, r).Tag(tagAccount).Info("Removing excess reservations for topics %s", strings.Join(topics, ", "))
if err := s.userManager.RemoveReservations(u.Name, topics...); err != nil {
return err
}
if err := s.messageCache.ExpireMessages(topics...); err != nil {
return err
}
go s.pruneMessages()
return nil
}
2023-05-16 20:15:58 +02:00
func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-05-11 19:50:10 +02:00
u := v.User()
2023-05-17 04:27:48 +02:00
req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false)
2023-05-11 19:50:10 +02:00
if err != nil {
return err
2023-05-17 04:27:48 +02:00
} else if !phoneNumberRegex.MatchString(req.Number) {
2023-05-11 19:50:10 +02:00
return errHTTPBadRequestPhoneNumberInvalid
2023-05-17 04:27:48 +02:00
} else if req.Channel != "sms" && req.Channel != "call" {
return errHTTPBadRequestPhoneNumberVerifyChannelInvalid
2023-05-11 19:50:10 +02:00
}
// Check user is allowed to add phone numbers
if u == nil || (u.IsUser() && u.Tier == nil) {
return errHTTPUnauthorized
2023-05-13 02:01:12 +02:00
} else if u.IsUser() && u.Tier.CallLimit == 0 {
2023-05-11 19:50:10 +02:00
return errHTTPUnauthorized
}
2023-05-13 03:47:41 +02:00
// Check if phone number exists
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
if err != nil {
2023-05-11 19:50:10 +02:00
return err
2023-05-13 03:47:41 +02:00
} else if util.Contains(phoneNumbers, req.Number) {
return errHTTPConflictPhoneNumberExists
2023-05-11 19:50:10 +02:00
}
2023-05-13 03:47:41 +02:00
// Actually add the unverified number, and send verification
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification")
2023-05-17 04:27:48 +02:00
if err := s.verifyPhoneNumber(v, r, req.Number, req.Channel); err != nil {
2023-05-11 19:50:10 +02:00
return err
}
return s.writeJSON(w, newSuccessResponse())
}
2023-05-16 20:15:58 +02:00
func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
2023-05-11 19:50:10 +02:00
u := v.User()
2023-05-17 04:27:48 +02:00
req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false)
2023-05-11 19:50:10 +02:00
if err != nil {
return err
}
if !phoneNumberRegex.MatchString(req.Number) {
return errHTTPBadRequestPhoneNumberInvalid
}
2023-05-16 20:15:58 +02:00
if err := s.verifyPhoneNumberCheck(v, r, req.Number, req.Code); err != nil {
2023-05-11 19:50:10 +02:00
return err
}
2023-05-13 03:47:41 +02:00
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified")
if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil {
return err
2023-05-11 19:50:10 +02:00
}
2023-05-13 03:47:41 +02:00
return s.writeJSON(w, newSuccessResponse())
}
func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
u := v.User()
2023-05-17 04:27:48 +02:00
req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false)
2023-05-13 03:47:41 +02:00
if err != nil {
2023-05-11 19:50:10 +02:00
return err
}
2023-05-13 03:47:41 +02:00
if !phoneNumberRegex.MatchString(req.Number) {
return errHTTPBadRequestPhoneNumberInvalid
}
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number")
2023-05-17 16:39:15 +02:00
if err := s.userManager.RemovePhoneNumber(u.ID, req.Number); err != nil {
2023-05-11 19:50:10 +02:00
return err
}
return s.writeJSON(w, newSuccessResponse())
}
2023-01-29 02:43:06 +01:00
// publishSyncEventAsync kicks of a Go routine to publish a sync message to the user's sync topic
func (s *Server) publishSyncEventAsync(v *visitor) {
go func() {
if err := s.publishSyncEvent(v); err != nil {
2023-02-06 05:34:27 +01:00
logv(v).Err(err).Trace("Error publishing to user's sync topic")
2023-01-29 02:43:06 +01:00
}
}()
}
// publishSyncEvent publishes a sync message to the user's sync topic
func (s *Server) publishSyncEvent(v *visitor) error {
2023-01-29 02:43:06 +01:00
u := v.User()
if u == nil || u.SyncTopic == "" {
return nil
}
2023-02-06 05:34:27 +01:00
logv(v).Field("sync_topic", u.SyncTopic).Trace("Publishing sync event to user's sync topic")
2023-01-29 02:43:06 +01:00
syncTopic, err := s.topicFromID(u.SyncTopic)
if err != nil {
return err
}
messageBytes, err := json.Marshal(&apiAccountSyncTopicResponse{Event: syncTopicAccountSyncEvent})
if err != nil {
return err
}
m := newDefaultMessage(syncTopic.ID, string(messageBytes))
if err := syncTopic.Publish(v, m); err != nil {
return err
}
return nil
}