Self-review
This commit is contained in:
		
							parent
							
								
									79a3259c86
								
							
						
					
					
						commit
						ac029c389e
					
				
					 16 changed files with 201 additions and 133 deletions
				
			
		|  | @ -856,8 +856,8 @@ billing-contact: "phil@example.com" | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Phone calls | ## Phone calls | ||||||
| ntfy supports phone calls via [Twilio](https://www.twilio.com/) as a phone call provider. If phone calls are enabled, | ntfy supports phone calls via [Twilio](https://www.twilio.com/) as a call provider. If phone calls are enabled, | ||||||
| users can verify and add a phone number, and then receive phone calls when publish a message with the `X-Call` header. | users can verify and add a phone number, and then receive phone calls when publishing a message using the `X-Call` header. | ||||||
| See [publishing page](publish.md#phone-calls) for more details. | See [publishing page](publish.md#phone-calls) for more details. | ||||||
| 
 | 
 | ||||||
| To enable Twilio integration, sign up with [Twilio](https://www.twilio.com/), purchase a phone number (Toll free numbers | To enable Twilio integration, sign up with [Twilio](https://www.twilio.com/), purchase a phone number (Toll free numbers | ||||||
|  |  | ||||||
|  | @ -2709,7 +2709,7 @@ You may also simply pass `yes` as a value to pick the first of your verified pho | ||||||
| On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. | On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. | ||||||
| 
 | 
 | ||||||
| <figure markdown> | <figure markdown> | ||||||
|    |    | ||||||
|   <figcaption>Phone number verification in the <a href="https://ntfy.sh/account">web app</a></figcaption> |   <figcaption>Phone number verification in the <a href="https://ntfy.sh/account">web app</a></figcaption> | ||||||
| </figure> | </figure> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -33,7 +33,7 @@ require ( | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	cloud.google.com/go v0.110.2 // indirect | 	cloud.google.com/go v0.110.2 // indirect | ||||||
| 	cloud.google.com/go/compute v1.19.2 // indirect | 	cloud.google.com/go/compute v1.19.3 // indirect | ||||||
| 	cloud.google.com/go/compute/metadata v0.2.3 // indirect | 	cloud.google.com/go/compute/metadata v0.2.3 // indirect | ||||||
| 	cloud.google.com/go/iam v1.0.1 // indirect | 	cloud.google.com/go/iam v1.0.1 // indirect | ||||||
| 	cloud.google.com/go/longrunning v0.4.2 // indirect | 	cloud.google.com/go/longrunning v0.4.2 // indirect | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -4,6 +4,8 @@ cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= | ||||||
| cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= | cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= | ||||||
| cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY= | cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY= | ||||||
| cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08= | cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08= | ||||||
|  | cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= | ||||||
|  | cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= | ||||||
| cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= | ||||||
| cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= | ||||||
| cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= | cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= | ||||||
|  |  | ||||||
|  | @ -198,6 +198,30 @@ func TestLog_LevelOverride_ManyOnSameField(t *testing.T) { | ||||||
| 	require.Equal(t, "", File()) | 	require.Equal(t, "", File()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestLog_FieldIf(t *testing.T) { | ||||||
|  | 	t.Cleanup(resetState) | ||||||
|  | 
 | ||||||
|  | 	var out bytes.Buffer | ||||||
|  | 	SetOutput(&out) | ||||||
|  | 	SetLevel(DebugLevel) | ||||||
|  | 	SetFormat(JSONFormat) | ||||||
|  | 
 | ||||||
|  | 	Time(time.Unix(11, 0).UTC()). | ||||||
|  | 		FieldIf("trace_field", "manager", TraceLevel). // This is not logged | ||||||
|  | 		Field("tag", "manager"). | ||||||
|  | 		Debug("trace_field is not logged") | ||||||
|  | 	SetLevel(TraceLevel) | ||||||
|  | 	Time(time.Unix(12, 0).UTC()). | ||||||
|  | 		FieldIf("trace_field", "manager", TraceLevel). // Now it is logged | ||||||
|  | 		Field("tag", "manager"). | ||||||
|  | 		Debug("trace_field is logged") | ||||||
|  | 
 | ||||||
|  | 	expected := `{"time":"1970-01-01T00:00:11Z","level":"DEBUG","message":"trace_field is not logged","tag":"manager"} | ||||||
|  | {"time":"1970-01-01T00:00:12Z","level":"DEBUG","message":"trace_field is logged","tag":"manager","trace_field":"manager"} | ||||||
|  | ` | ||||||
|  | 	require.Equal(t, expected, out.String()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestLog_UsingStdLogger_JSON(t *testing.T) { | func TestLog_UsingStdLogger_JSON(t *testing.T) { | ||||||
| 	t.Cleanup(resetState) | 	t.Cleanup(resetState) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -108,11 +108,12 @@ var ( | ||||||
| 	errHTTPBadRequestBillingSubscriptionExists       = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil} | 	errHTTPBadRequestBillingSubscriptionExists       = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil} | ||||||
| 	errHTTPBadRequestTierInvalid                     = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil} | 	errHTTPBadRequestTierInvalid                     = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil} | ||||||
| 	errHTTPBadRequestUserNotFound                    = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil} | 	errHTTPBadRequestUserNotFound                    = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil} | ||||||
| 	errHTTPBadRequestPhoneCallsDisabled              = &errHTTP{40032, http.StatusBadRequest, "invalid request: calling is disabled", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	errHTTPBadRequestPhoneCallsDisabled              = &errHTTP{40032, http.StatusBadRequest, "invalid request: calling is disabled", "https://ntfy.sh/docs/config/#phone-calls", nil} | ||||||
| 	errHTTPBadRequestPhoneNumberInvalid              = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	errHTTPBadRequestPhoneNumberInvalid              = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} | ||||||
| 	errHTTPBadRequestPhoneNumberNotVerified          = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	errHTTPBadRequestPhoneNumberNotVerified          = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil} | ||||||
| 	errHTTPBadRequestAnonymousCallsNotAllowed        = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	errHTTPBadRequestAnonymousCallsNotAllowed        = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} | ||||||
| 	errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil} | ||||||
|  | 	errHTTPBadRequestDelayNoCall                     = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil} | ||||||
| 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} | 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} | ||||||
| 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} | 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} | ||||||
| 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} | 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} | ||||||
|  |  | ||||||
|  | @ -937,6 +937,9 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi | ||||||
| 		if email != "" { | 		if email != "" { | ||||||
| 			return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) | 			return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) | ||||||
| 		} | 		} | ||||||
|  | 		if call != "" { | ||||||
|  | 			return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) | ||||||
|  | 		} | ||||||
| 		delay, err := util.ParseFutureTime(delayStr, time.Now()) | 		delay, err := util.ParseFutureTime(delayStr, time.Now()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return false, false, "", "", false, errHTTPBadRequestDelayCannotParse | 			return false, false, "", "", false, errHTTPBadRequestDelayCannotParse | ||||||
|  |  | ||||||
|  | @ -581,7 +581,7 @@ func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.R | ||||||
| 		return errHTTPBadRequestPhoneNumberInvalid | 		return errHTTPBadRequestPhoneNumberInvalid | ||||||
| 	} | 	} | ||||||
| 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number") | 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number") | ||||||
| 	if err := s.userManager.DeletePhoneNumber(u.ID, req.Number); err != nil { | 	if err := s.userManager.RemovePhoneNumber(u.ID, req.Number); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return s.writeJSON(w, newSuccessResponse()) | 	return s.writeJSON(w, newSuccessResponse()) | ||||||
|  |  | ||||||
|  | @ -1190,7 +1190,20 @@ func TestServer_PublishDelayedEmail_Fail(t *testing.T) { | ||||||
| 		"E-Mail": "test@example.com", | 		"E-Mail": "test@example.com", | ||||||
| 		"Delay":  "20 min", | 		"Delay":  "20 min", | ||||||
| 	}) | 	}) | ||||||
| 	require.Equal(t, 400, response.Code) | 	require.Equal(t, 40003, toHTTPError(t, response.Body.String()).Code) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestServer_PublishDelayedCall_Fail(t *testing.T) { | ||||||
|  | 	c := newTestConfig(t) | ||||||
|  | 	c.TwilioAccount = "AC1234567890" | ||||||
|  | 	c.TwilioAuthToken = "AAEAA1234567890" | ||||||
|  | 	c.TwilioFromNumber = "+1234567890" | ||||||
|  | 	s := newTestServer(t, c) | ||||||
|  | 	response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ | ||||||
|  | 		"Call":  "yes", | ||||||
|  | 		"Delay": "20 min", | ||||||
|  | 	}) | ||||||
|  | 	require.Equal(t, 40037, toHTTPError(t, response.Body.String()).Code) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestServer_PublishEmailNoMailer_Fail(t *testing.T) { | func TestServer_PublishEmailNoMailer_Fail(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { | ||||||
| 		require.Nil(t, err) | 		require.Nil(t, err) | ||||||
| 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | ||||||
| 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | ||||||
| 		require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | 		require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | ||||||
| 		called.Store(true) | 		called.Store(true) | ||||||
| 	})) | 	})) | ||||||
| 	defer twilioCallsServer.Close() | 	defer twilioCallsServer.Close() | ||||||
|  | @ -69,7 +69,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { | ||||||
| 	require.Nil(t, err) | 	require.Nil(t, err) | ||||||
| 
 | 
 | ||||||
| 	// Send verification code for phone number | 	// Send verification code for phone number | ||||||
| 	response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444"}`, map[string]string{ | 	response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{ | ||||||
| 		"authorization": util.BasicAuth("phil", "phil"), | 		"authorization": util.BasicAuth("phil", "phil"), | ||||||
| 	}) | 	}) | ||||||
| 	require.Equal(t, 200, response.Code) | 	require.Equal(t, 200, response.Code) | ||||||
|  | @ -122,7 +122,7 @@ func TestServer_Twilio_Call_Success(t *testing.T) { | ||||||
| 		require.Nil(t, err) | 		require.Nil(t, err) | ||||||
| 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | ||||||
| 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | ||||||
| 		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | 		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | ||||||
| 		called.Store(true) | 		called.Store(true) | ||||||
| 	})) | 	})) | ||||||
| 	defer twilioServer.Close() | 	defer twilioServer.Close() | ||||||
|  | @ -167,7 +167,7 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) { | ||||||
| 		require.Nil(t, err) | 		require.Nil(t, err) | ||||||
| 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | 		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) | ||||||
| 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | 		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) | ||||||
| 		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+notification+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+up+to+three+times.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | 		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) | ||||||
| 		called.Store(true) | 		called.Store(true) | ||||||
| 	})) | 	})) | ||||||
| 	defer twilioServer.Close() | 	defer twilioServer.Close() | ||||||
|  |  | ||||||
|  | @ -318,7 +318,7 @@ type apiAccountPhoneNumberVerifyRequest struct { | ||||||
| 
 | 
 | ||||||
| type apiAccountPhoneNumberAddRequest struct { | type apiAccountPhoneNumberAddRequest struct { | ||||||
| 	Number string `json:"number"` | 	Number string `json:"number"` | ||||||
| 	Code   string `json:"code,omitempty"` | 	Code   string `json:"code"` // Only set when adding a phone number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountTier struct { | type apiAccountTier struct { | ||||||
|  |  | ||||||
|  | @ -117,7 +117,6 @@ const ( | ||||||
| 			PRIMARY KEY (user_id, phone_number), | 			PRIMARY KEY (user_id, phone_number), | ||||||
| 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | ||||||
| 		); | 		); | ||||||
| 		CREATE UNIQUE INDEX idx_user_phone_number ON user_phone (phone_number); |  | ||||||
| 		CREATE TABLE IF NOT EXISTS schemaVersion ( | 		CREATE TABLE IF NOT EXISTS schemaVersion ( | ||||||
| 			id INT PRIMARY KEY, | 			id INT PRIMARY KEY, | ||||||
| 			version INT NOT NULL | 			version INT NOT NULL | ||||||
|  | @ -420,7 +419,6 @@ const ( | ||||||
| 			PRIMARY KEY (user_id, phone_number), | 			PRIMARY KEY (user_id, phone_number), | ||||||
| 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE | ||||||
| 		); | 		); | ||||||
| 		CREATE UNIQUE INDEX idx_user_phone_number ON user_phone (phone_number); |  | ||||||
| 	` | 	` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -694,8 +692,8 @@ func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeletePhoneNumber deletes a phone number from the user with the given user ID | // RemovePhoneNumber deletes a phone number from the user with the given user ID | ||||||
| func (a *Manager) DeletePhoneNumber(userID string, phoneNumber string) error { | func (a *Manager) RemovePhoneNumber(userID string, phoneNumber string) error { | ||||||
| 	_, err := a.db.Exec(deletePhoneNumberQuery, userID, phoneNumber) | 	_, err := a.db.Exec(deletePhoneNumberQuery, userID, phoneNumber) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -893,6 +893,38 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { | ||||||
| 	require.Nil(t, a.ResetTier("phil")) | 	require.Nil(t, a.ResetTier("phil")) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestUser_PhoneNumberAddListRemove(t *testing.T) { | ||||||
|  | 	a := newTestManager(t, PermissionDenyAll) | ||||||
|  | 
 | ||||||
|  | 	require.Nil(t, a.AddUser("phil", "phil", RoleUser)) | ||||||
|  | 	phil, err := a.User("phil") | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) | ||||||
|  | 
 | ||||||
|  | 	phoneNumbers, err := a.PhoneNumbers(phil.ID) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Equal(t, 1, len(phoneNumbers)) | ||||||
|  | 	require.Equal(t, "+1234567890", phoneNumbers[0]) | ||||||
|  | 
 | ||||||
|  | 	require.Nil(t, a.RemovePhoneNumber(phil.ID, "+1234567890")) | ||||||
|  | 	phoneNumbers, err = a.PhoneNumbers(phil.ID) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Equal(t, 0, len(phoneNumbers)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) { | ||||||
|  | 	a := newTestManager(t, PermissionDenyAll) | ||||||
|  | 
 | ||||||
|  | 	require.Nil(t, a.AddUser("phil", "phil", RoleUser)) | ||||||
|  | 	require.Nil(t, a.AddUser("ben", "ben", RoleUser)) | ||||||
|  | 	phil, err := a.User("phil") | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	ben, err := a.User("ben") | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) | ||||||
|  | 	require.Nil(t, a.AddPhoneNumber(ben.ID, "+1234567890")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestSqliteCache_Migration_From1(t *testing.T) { | func TestSqliteCache_Migration_From1(t *testing.T) { | ||||||
| 	filename := filepath.Join(t.TempDir(), "user.db") | 	filename := filepath.Join(t.TempDir(), "user.db") | ||||||
| 	db, err := sql.Open("sqlite3", filename) | 	db, err := sql.Open("sqlite3", filename) | ||||||
|  |  | ||||||
							
								
								
									
										202
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										202
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -3134,14 +3134,14 @@ | ||||||
|       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" |       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/base": { |     "node_modules/@mui/base": { | ||||||
|       "version": "5.0.0-beta.0", |       "version": "5.0.0-beta.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.0.tgz", |       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.1.tgz", | ||||||
|       "integrity": "sha512-ap+juKvt8R8n3cBqd/pGtZydQ4v2I/hgJKnvJRGjpSh3RvsvnDHO4rXov8MHQlH6VqpOekwgilFLGxMZjNTucA==", |       "integrity": "sha512-xrkDCeu3JQE+JjJUnJnOrdQJMXwKhbV4AW+FRjMIj5i9cHK3BAuatG/iqbf1M+jklVWLk0KdbgioKwK+03aYbA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/runtime": "^7.21.0", |         "@babel/runtime": "^7.21.0", | ||||||
|         "@emotion/is-prop-valid": "^1.2.0", |         "@emotion/is-prop-valid": "^1.2.0", | ||||||
|         "@mui/types": "^7.2.4", |         "@mui/types": "^7.2.4", | ||||||
|         "@mui/utils": "^5.12.3", |         "@mui/utils": "^5.13.1", | ||||||
|         "@popperjs/core": "^2.11.7", |         "@popperjs/core": "^2.11.7", | ||||||
|         "clsx": "^1.2.1", |         "clsx": "^1.2.1", | ||||||
|         "prop-types": "^15.8.1", |         "prop-types": "^15.8.1", | ||||||
|  | @ -3166,9 +3166,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/core-downloads-tracker": { |     "node_modules/@mui/core-downloads-tracker": { | ||||||
|       "version": "5.13.0", |       "version": "5.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.0.tgz", |       "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.1.tgz", | ||||||
|       "integrity": "sha512-5nXz2k8Rv2ZjtQY6kXirJVyn2+ODaQuAJmXSJtLDUQDKWp3PFUj6j3bILqR0JGOs9R5ejgwz3crLKsl6GwjwkQ==", |       "integrity": "sha512-qDHtNDO72NcBQMhaWBt9EZMvNiO+OXjPg5Sdk/6LgRDw6Zr3HdEZ5n2FJ/qtYsaT/okGyCuQavQkcZCOCEVf/g==", | ||||||
|       "funding": { |       "funding": { | ||||||
|         "type": "opencollective", |         "type": "opencollective", | ||||||
|         "url": "https://opencollective.com/mui" |         "url": "https://opencollective.com/mui" | ||||||
|  | @ -3200,16 +3200,16 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/material": { |     "node_modules/@mui/material": { | ||||||
|       "version": "5.13.0", |       "version": "5.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.0.tgz", |       "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.1.tgz", | ||||||
|       "integrity": "sha512-ckS+9tCpAzpdJdaTF+btF0b6mF9wbXg/EVKtnoAWYi0UKXoXBAVvEUMNpLGA5xdpCdf+A6fPbVUEHs9TsfU+Yw==", |       "integrity": "sha512-qSnbJZer8lIuDYFDv19/t3s0AXYY9SxcOdhCnGvetRSfOG4gy3TkiFXNCdW5OLNveTieiMpOuv46eXUmE3ZA6A==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/runtime": "^7.21.0", |         "@babel/runtime": "^7.21.0", | ||||||
|         "@mui/base": "5.0.0-beta.0", |         "@mui/base": "5.0.0-beta.1", | ||||||
|         "@mui/core-downloads-tracker": "^5.13.0", |         "@mui/core-downloads-tracker": "^5.13.1", | ||||||
|         "@mui/system": "^5.12.3", |         "@mui/system": "^5.13.1", | ||||||
|         "@mui/types": "^7.2.4", |         "@mui/types": "^7.2.4", | ||||||
|         "@mui/utils": "^5.12.3", |         "@mui/utils": "^5.13.1", | ||||||
|         "@types/react-transition-group": "^4.4.6", |         "@types/react-transition-group": "^4.4.6", | ||||||
|         "clsx": "^1.2.1", |         "clsx": "^1.2.1", | ||||||
|         "csstype": "^3.1.2", |         "csstype": "^3.1.2", | ||||||
|  | @ -3244,12 +3244,12 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/private-theming": { |     "node_modules/@mui/private-theming": { | ||||||
|       "version": "5.12.3", |       "version": "5.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.12.3.tgz", |       "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz", | ||||||
|       "integrity": "sha512-o1e7Z1Bp27n4x2iUHhegV4/Jp6H3T6iBKHJdLivS5GbwsuAE/5l4SnZ+7+K+e5u9TuhwcAKZLkjvqzkDe8zqfA==", |       "integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/runtime": "^7.21.0", |         "@babel/runtime": "^7.21.0", | ||||||
|         "@mui/utils": "^5.12.3", |         "@mui/utils": "^5.13.1", | ||||||
|         "prop-types": "^15.8.1" |         "prop-types": "^15.8.1" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|  | @ -3301,15 +3301,15 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/system": { |     "node_modules/@mui/system": { | ||||||
|       "version": "5.12.3", |       "version": "5.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.12.3.tgz", |       "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.1.tgz", | ||||||
|       "integrity": "sha512-JB/6sypHqeJCqwldWeQ1MKkijH829EcZAKKizxbU2MJdxGG5KSwZvTBa5D9qiJUA1hJFYYupjiuy9ZdJt6rV6w==", |       "integrity": "sha512-BsDUjhiO6ZVAvzKhnWBHLZ5AtPJcdT+62VjnRLyA4isboqDKLg4fmYIZXq51yndg/soDK9RkY5lYZwEDku13Ow==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/runtime": "^7.21.0", |         "@babel/runtime": "^7.21.0", | ||||||
|         "@mui/private-theming": "^5.12.3", |         "@mui/private-theming": "^5.13.1", | ||||||
|         "@mui/styled-engine": "^5.12.3", |         "@mui/styled-engine": "^5.12.3", | ||||||
|         "@mui/types": "^7.2.4", |         "@mui/types": "^7.2.4", | ||||||
|         "@mui/utils": "^5.12.3", |         "@mui/utils": "^5.13.1", | ||||||
|         "clsx": "^1.2.1", |         "clsx": "^1.2.1", | ||||||
|         "csstype": "^3.1.2", |         "csstype": "^3.1.2", | ||||||
|         "prop-types": "^15.8.1" |         "prop-types": "^15.8.1" | ||||||
|  | @ -3353,13 +3353,13 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@mui/utils": { |     "node_modules/@mui/utils": { | ||||||
|       "version": "5.12.3", |       "version": "5.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.12.3.tgz", |       "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz", | ||||||
|       "integrity": "sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA==", |       "integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/runtime": "^7.21.0", |         "@babel/runtime": "^7.21.0", | ||||||
|         "@types/prop-types": "^15.7.5", |         "@types/prop-types": "^15.7.5", | ||||||
|         "@types/react-is": "^16.7.1 || ^17.0.0", |         "@types/react-is": "^18.2.0", | ||||||
|         "prop-types": "^15.8.1", |         "prop-types": "^15.8.1", | ||||||
|         "react-is": "^18.2.0" |         "react-is": "^18.2.0" | ||||||
|       }, |       }, | ||||||
|  | @ -4016,9 +4016,9 @@ | ||||||
|       "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" |       "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/node": { |     "node_modules/@types/node": { | ||||||
|       "version": "20.1.4", |       "version": "20.1.7", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz", |       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.7.tgz", | ||||||
|       "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==" |       "integrity": "sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/parse-json": { |     "node_modules/@types/parse-json": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|  | @ -4061,21 +4061,11 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/react-is": { |     "node_modules/@types/react-is": { | ||||||
|       "version": "17.0.4", |       "version": "18.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.4.tgz", |       "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.0.tgz", | ||||||
|       "integrity": "sha512-FLzd0K9pnaEvKz4D1vYxK9JmgQPiGk1lu23o1kqGsLeT0iPbRSF7b76+S5T9fD8aRa0B8bY7I/3DebEj+1ysBA==", |       "integrity": "sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@types/react": "^17" |         "@types/react": "*" | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/@types/react-is/node_modules/@types/react": { |  | ||||||
|       "version": "17.0.59", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.59.tgz", |  | ||||||
|       "integrity": "sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "@types/prop-types": "*", |  | ||||||
|         "@types/scheduler": "*", |  | ||||||
|         "csstype": "^3.0.2" |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/react-transition-group": { |     "node_modules/@types/react-transition-group": { | ||||||
|  | @ -4175,14 +4165,14 @@ | ||||||
|       "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" |       "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/eslint-plugin": { |     "node_modules/@typescript-eslint/eslint-plugin": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz", | ||||||
|       "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", |       "integrity": "sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@eslint-community/regexpp": "^4.4.0", |         "@eslint-community/regexpp": "^4.4.0", | ||||||
|         "@typescript-eslint/scope-manager": "5.59.5", |         "@typescript-eslint/scope-manager": "5.59.6", | ||||||
|         "@typescript-eslint/type-utils": "5.59.5", |         "@typescript-eslint/type-utils": "5.59.6", | ||||||
|         "@typescript-eslint/utils": "5.59.5", |         "@typescript-eslint/utils": "5.59.6", | ||||||
|         "debug": "^4.3.4", |         "debug": "^4.3.4", | ||||||
|         "grapheme-splitter": "^1.0.4", |         "grapheme-splitter": "^1.0.4", | ||||||
|         "ignore": "^5.2.0", |         "ignore": "^5.2.0", | ||||||
|  | @ -4208,11 +4198,11 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/experimental-utils": { |     "node_modules/@typescript-eslint/experimental-utils": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.6.tgz", | ||||||
|       "integrity": "sha512-ArcSSBifznsKNA/p4h2w3Olt/T8AZf3bNglxD8OnuTsSDJbRpjPPmI8qpr6ijyvk1J/T3GMJHwRIluS/Kuz9kA==", |       "integrity": "sha512-UIVfEaaHggOuhgqdpFlFQ7IN9UFMCiBR/N7uPBUyUlwNdJzYfAu9m4wbOj0b59oI/HSPW1N63Q7lsvfwTQY13w==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/utils": "5.59.5" |         "@typescript-eslint/utils": "5.59.6" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" |         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||||||
|  | @ -4226,13 +4216,13 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/parser": { |     "node_modules/@typescript-eslint/parser": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz", | ||||||
|       "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", |       "integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/scope-manager": "5.59.5", |         "@typescript-eslint/scope-manager": "5.59.6", | ||||||
|         "@typescript-eslint/types": "5.59.5", |         "@typescript-eslint/types": "5.59.6", | ||||||
|         "@typescript-eslint/typescript-estree": "5.59.5", |         "@typescript-eslint/typescript-estree": "5.59.6", | ||||||
|         "debug": "^4.3.4" |         "debug": "^4.3.4" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|  | @ -4252,12 +4242,12 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/scope-manager": { |     "node_modules/@typescript-eslint/scope-manager": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz", | ||||||
|       "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", |       "integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "5.59.5", |         "@typescript-eslint/types": "5.59.6", | ||||||
|         "@typescript-eslint/visitor-keys": "5.59.5" |         "@typescript-eslint/visitor-keys": "5.59.6" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" |         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||||||
|  | @ -4268,12 +4258,12 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/type-utils": { |     "node_modules/@typescript-eslint/type-utils": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz", | ||||||
|       "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", |       "integrity": "sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/typescript-estree": "5.59.5", |         "@typescript-eslint/typescript-estree": "5.59.6", | ||||||
|         "@typescript-eslint/utils": "5.59.5", |         "@typescript-eslint/utils": "5.59.6", | ||||||
|         "debug": "^4.3.4", |         "debug": "^4.3.4", | ||||||
|         "tsutils": "^3.21.0" |         "tsutils": "^3.21.0" | ||||||
|       }, |       }, | ||||||
|  | @ -4294,9 +4284,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/types": { |     "node_modules/@typescript-eslint/types": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz", | ||||||
|       "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", |       "integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" |         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||||||
|       }, |       }, | ||||||
|  | @ -4306,12 +4296,12 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/typescript-estree": { |     "node_modules/@typescript-eslint/typescript-estree": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz", | ||||||
|       "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", |       "integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "5.59.5", |         "@typescript-eslint/types": "5.59.6", | ||||||
|         "@typescript-eslint/visitor-keys": "5.59.5", |         "@typescript-eslint/visitor-keys": "5.59.6", | ||||||
|         "debug": "^4.3.4", |         "debug": "^4.3.4", | ||||||
|         "globby": "^11.1.0", |         "globby": "^11.1.0", | ||||||
|         "is-glob": "^4.0.3", |         "is-glob": "^4.0.3", | ||||||
|  | @ -4332,16 +4322,16 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/utils": { |     "node_modules/@typescript-eslint/utils": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.6.tgz", | ||||||
|       "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", |       "integrity": "sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@eslint-community/eslint-utils": "^4.2.0", |         "@eslint-community/eslint-utils": "^4.2.0", | ||||||
|         "@types/json-schema": "^7.0.9", |         "@types/json-schema": "^7.0.9", | ||||||
|         "@types/semver": "^7.3.12", |         "@types/semver": "^7.3.12", | ||||||
|         "@typescript-eslint/scope-manager": "5.59.5", |         "@typescript-eslint/scope-manager": "5.59.6", | ||||||
|         "@typescript-eslint/types": "5.59.5", |         "@typescript-eslint/types": "5.59.6", | ||||||
|         "@typescript-eslint/typescript-estree": "5.59.5", |         "@typescript-eslint/typescript-estree": "5.59.6", | ||||||
|         "eslint-scope": "^5.1.1", |         "eslint-scope": "^5.1.1", | ||||||
|         "semver": "^7.3.7" |         "semver": "^7.3.7" | ||||||
|       }, |       }, | ||||||
|  | @ -4377,11 +4367,11 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/visitor-keys": { |     "node_modules/@typescript-eslint/visitor-keys": { | ||||||
|       "version": "5.59.5", |       "version": "5.59.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz", | ||||||
|       "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", |       "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "5.59.5", |         "@typescript-eslint/types": "5.59.6", | ||||||
|         "eslint-visitor-keys": "^3.3.0" |         "eslint-visitor-keys": "^3.3.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|  | @ -4956,9 +4946,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/axe-core": { |     "node_modules/axe-core": { | ||||||
|       "version": "4.7.0", |       "version": "4.7.1", | ||||||
|       "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", |       "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.1.tgz", | ||||||
|       "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", |       "integrity": "sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=4" |         "node": ">=4" | ||||||
|       } |       } | ||||||
|  | @ -5511,9 +5501,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/caniuse-lite": { |     "node_modules/caniuse-lite": { | ||||||
|       "version": "1.0.30001487", |       "version": "1.0.30001488", | ||||||
|       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz", |       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz", | ||||||
|       "integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==", |       "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==", | ||||||
|       "funding": [ |       "funding": [ | ||||||
|         { |         { | ||||||
|           "type": "opencollective", |           "type": "opencollective", | ||||||
|  | @ -6749,9 +6739,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/electron-to-chromium": { |     "node_modules/electron-to-chromium": { | ||||||
|       "version": "1.4.394", |       "version": "1.4.397", | ||||||
|       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.394.tgz", |       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.397.tgz", | ||||||
|       "integrity": "sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==" |       "integrity": "sha512-jwnPxhh350Q/aMatQia31KAIQdhEsYS0fFZ0BQQlN9tfvOEwShu6ZNwI4kL/xBabjcB/nTy6lSt17kNIluJZ8Q==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/emittery": { |     "node_modules/emittery": { | ||||||
|       "version": "0.8.1", |       "version": "0.8.1", | ||||||
|  | @ -9146,9 +9136,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/is-core-module": { |     "node_modules/is-core-module": { | ||||||
|       "version": "2.12.0", |       "version": "2.12.1", | ||||||
|       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", |       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", | ||||||
|       "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", |       "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "has": "^1.0.3" |         "has": "^1.0.3" | ||||||
|       }, |       }, | ||||||
|  | @ -13879,9 +13869,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/postcss-selector-parser": { |     "node_modules/postcss-selector-parser": { | ||||||
|       "version": "6.0.12", |       "version": "6.0.13", | ||||||
|       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz", |       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", | ||||||
|       "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==", |       "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "cssesc": "^3.0.0", |         "cssesc": "^3.0.0", | ||||||
|         "util-deprecate": "^1.0.2" |         "util-deprecate": "^1.0.2" | ||||||
|  | @ -15976,9 +15966,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/terser": { |     "node_modules/terser": { | ||||||
|       "version": "5.17.3", |       "version": "5.17.4", | ||||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", |       "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz", | ||||||
|       "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", |       "integrity": "sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@jridgewell/source-map": "^0.3.2", |         "@jridgewell/source-map": "^0.3.2", | ||||||
|         "acorn": "^8.5.0", |         "acorn": "^8.5.0", | ||||||
|  |  | ||||||
|  | @ -1,14 +1,16 @@ | ||||||
| import { | import { | ||||||
|     accountBillingPortalUrl, |     accountBillingPortalUrl, | ||||||
|     accountBillingSubscriptionUrl, |     accountBillingSubscriptionUrl, | ||||||
|     accountPasswordUrl, accountPhoneUrl, accountPhoneVerifyUrl, |     accountPasswordUrl, | ||||||
|  |     accountPhoneUrl, | ||||||
|  |     accountPhoneVerifyUrl, | ||||||
|     accountReservationSingleUrl, |     accountReservationSingleUrl, | ||||||
|     accountReservationUrl, |     accountReservationUrl, | ||||||
|     accountSettingsUrl, |     accountSettingsUrl, | ||||||
|     accountSubscriptionSingleUrl, |  | ||||||
|     accountSubscriptionUrl, |     accountSubscriptionUrl, | ||||||
|     accountTokenUrl, |     accountTokenUrl, | ||||||
|     accountUrl, maybeWithBearerAuth, |     accountUrl, | ||||||
|  |     maybeWithBearerAuth, | ||||||
|     tiersUrl, |     tiersUrl, | ||||||
|     withBasicAuth, |     withBasicAuth, | ||||||
|     withBearerAuth |     withBearerAuth | ||||||
|  | @ -18,7 +20,7 @@ import subscriptionManager from "./SubscriptionManager"; | ||||||
| import i18n from "i18next"; | import i18n from "i18next"; | ||||||
| import prefs from "./Prefs"; | import prefs from "./Prefs"; | ||||||
| import routes from "../components/routes"; | import routes from "../components/routes"; | ||||||
| import {fetchOrThrow, throwAppError, UnauthorizedError} from "./errors"; | import {fetchOrThrow, UnauthorizedError} from "./errors"; | ||||||
| 
 | 
 | ||||||
| const delayMillis = 45000; // 45 seconds
 | const delayMillis = 45000; // 45 seconds
 | ||||||
| const intervalMillis = 900000; // 15 minutes
 | const intervalMillis = 900000; // 15 minutes
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,17 @@ | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import {useContext, useState} from 'react'; | import {useContext, useState} from 'react'; | ||||||
| import { | import { | ||||||
|     Alert, ButtonGroup, |     Alert, | ||||||
|     CardActions, |     CardActions, | ||||||
|     CardContent, Chip, |     CardContent, | ||||||
|     FormControl, FormControlLabel, InputLabel, |     Chip, | ||||||
|  |     FormControl, | ||||||
|  |     FormControlLabel, | ||||||
|     LinearProgress, |     LinearProgress, | ||||||
|     Link, |     Link, | ||||||
|     Portal, Radio, RadioGroup, |     Portal, | ||||||
|  |     Radio, | ||||||
|  |     RadioGroup, | ||||||
|     Select, |     Select, | ||||||
|     Snackbar, |     Snackbar, | ||||||
|     Stack, |     Stack, | ||||||
|  | @ -47,14 +51,12 @@ import {AccountContext} from "./App"; | ||||||
| import DialogFooter from "./DialogFooter"; | import DialogFooter from "./DialogFooter"; | ||||||
| import {Paragraph} from "./styles"; | import {Paragraph} from "./styles"; | ||||||
| import CloseIcon from "@mui/icons-material/Close"; | import CloseIcon from "@mui/icons-material/Close"; | ||||||
| import {Check, ContentCopy, DeleteForever, Public} from "@mui/icons-material"; | import {ContentCopy, Public} from "@mui/icons-material"; | ||||||
| import MenuItem from "@mui/material/MenuItem"; | import MenuItem from "@mui/material/MenuItem"; | ||||||
| import DialogContentText from "@mui/material/DialogContentText"; | import DialogContentText from "@mui/material/DialogContentText"; | ||||||
| import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; | import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; | ||||||
| import {ProChip} from "./SubscriptionPopup"; | import {ProChip} from "./SubscriptionPopup"; | ||||||
| import AddIcon from "@mui/icons-material/Add"; | import AddIcon from "@mui/icons-material/Add"; | ||||||
| import ListItemIcon from "@mui/material/ListItemIcon"; |  | ||||||
| import ListItemText from "@mui/material/ListItemText"; |  | ||||||
| 
 | 
 | ||||||
| const Account = () => { | const Account = () => { | ||||||
|     if (!session.exists()) { |     if (!session.exists()) { | ||||||
|  | @ -427,6 +429,7 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         if (verificationCodeSent) { |         if (verificationCodeSent) { | ||||||
|             setVerificationCodeSent(false); |             setVerificationCodeSent(false); | ||||||
|  |             setCode(""); | ||||||
|         } else { |         } else { | ||||||
|             props.onClose(); |             props.onClose(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue