Add call verification
This commit is contained in:
		
							parent
							
								
									496d6e74b0
								
							
						
					
					
						commit
						2c81773d01
					
				
					 11 changed files with 93 additions and 74 deletions
				
			
		|  | @ -868,8 +868,8 @@ are the easiest), and then configure the following options: | |||
| * `twilio-from-number` is the outgoing phone number you purchased, e.g. +18775132586  | ||||
| * `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 | ||||
| 
 | ||||
| After you have configured phone calls, create a [tier](#tiers) with a call limit, and then assign it to a user. | ||||
| Users may then use the `X-Call` header to receive a phone call when publishing a message. | ||||
| After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`), | ||||
| and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message. | ||||
| 
 | ||||
| ## Rate limiting | ||||
| !!! info | ||||
|  |  | |||
|  | @ -2702,16 +2702,26 @@ You can use ntfy to call a phone and **read the message out loud using text-to-s | |||
| Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have  | ||||
| the ntfy app installed on their phone. | ||||
| 
 | ||||
| **Phone numbers have to be previously verified** (via the web app), so this feature is **only available to authenticated users**.  | ||||
| To forward a message as a voice call, pass a phone number in the `X-Call` header (or its alias: `Call`), prefixed with a | ||||
| plus sign and the country code, e.g. `+12223334444`. You may also simply pass `yes` as a value to pick the first of your  | ||||
| verified phone numbers. | ||||
| **Phone numbers have to be previously verified** (via the [web app](https://ntfy.sh/account)), so this feature is  | ||||
| **only available to authenticated users** (no anonymous phone calls). To forward a message as a voice call, pass a phone | ||||
| number in the `X-Call` header (or its alias: `Call`), prefixed with a plus sign and the country code, e.g. `+12223334444`.  | ||||
| You may also simply pass `yes` as a value to pick the first of your verified phone numbers.  | ||||
| On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. | ||||
| 
 | ||||
| <figure markdown> | ||||
|    | ||||
|   <figcaption>Phone number verification in the <a href="https://ntfy.sh/account">web app</a></figcaption> | ||||
| </figure> | ||||
| 
 | ||||
| As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll | ||||
| be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). | ||||
| 
 | ||||
| !!! info | ||||
|     As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll | ||||
|     be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). | ||||
|     You are responsible for the message content, and **you must abide by the [Twilio Acceptable Use Policy](https://www.twilio.com/en-us/legal/aup)**. | ||||
|     This particularly means that you must not use this feature to send unsolicited messages, or messages that are illegal or | ||||
|     violate the rights of others. Please read the policy for details. Failure to do so may result in your account being suspended or terminated. | ||||
| 
 | ||||
| On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. | ||||
| Here's how you use it: | ||||
| 
 | ||||
| === "Command line (curl)" | ||||
|     ``` | ||||
|  | @ -3431,17 +3441,18 @@ There are a few limitations to the API to prevent abuse and to keep the server h | |||
| are configurable via the server side [rate limiting settings](config.md#rate-limiting). Most of these limits you won't run into, | ||||
| but just in case, let's list them all: | ||||
| 
 | ||||
| | Limit                           | Description                                                                                                                                                                                                             | | ||||
| |---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||
| | **Message length**              | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments).                                                                                                                 | | ||||
| | **Requests**                    | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds.                                                | | ||||
| | **Daily messages**              | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250.                                                                               | | ||||
| | **E-mails**                     | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5.                      | | ||||
| | **Subscription limit**          | By default, the server allows each visitor to keep 30 connections to the server open.                                                                                                                                   | | ||||
| | **Attachment size limit**       | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. | | ||||
| | **Attachment expiry**           | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit.                                                                                            | | ||||
| | **Attachment bandwidth**        | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB.                      | | ||||
| | **Total number of topics**      | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though.                                                                                                               | | ||||
| | Limit                      | Description                                                                                                                                                                                                             | | ||||
| |----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||
| | **Message length**         | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments).                                                                                                                 | | ||||
| | **Requests**               | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds.                                                | | ||||
| | **Daily messages**         | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250.                                                                               | | ||||
| | **E-mails**                | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5.                      | | ||||
| | **Phone calls**            | By default, the server does not allow any phone calls, except for users with a tier that has a call limit.                                                                                                              | | ||||
| | **Subscription limit**     | By default, the server allows each visitor to keep 30 connections to the server open.                                                                                                                                   | | ||||
| | **Attachment size limit**  | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. | | ||||
| | **Attachment expiry**      | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit.                                                                                            | | ||||
| | **Attachment bandwidth**   | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB.                      | | ||||
| | **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though.                                                                                                               | | ||||
| 
 | ||||
| These limits can be changed on a per-user basis using [tiers](config.md#tiers). If [payments](config.md#payments) are enabled, a user tier can be changed by purchasing | ||||
| a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above.  | ||||
|  | @ -3470,6 +3481,7 @@ table in their canonical form. | |||
| | `X-Icon`        | `Icon`                                     | URL to use as notification [icon](#icons)                                                     | | ||||
| | `X-Filename`    | `Filename`, `file`, `f`                    | Optional [attachment](#attachments) filename, as it appears in the client                     | | ||||
| | `X-Email`       | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications)                              | | ||||
| | `X-Call`        | `Call`                                     | Phone number for [phone calls](#phone-calls)                                                  | | ||||
| | `X-Cache`       | `Cache`                                    | Allows disabling [message caching](#message-caching)                                          | | ||||
| | `X-Firebase`    | `Firebase`                                 | Allows disabling [sending to Firebase](#disable-firebase)                                     | | ||||
| | `X-UnifiedPush` | `UnifiedPush`, `up`                        | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps               | | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								docs/static/img/web-phone-verify.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/static/img/web-phone-verify.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										18
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -1,23 +1,15 @@ | |||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| cloud.google.com/go v0.110.1 h1:oDJ19Fu9TX9Xs06iyCw4yifSqZ7JQ8BeuVHcTmWQlOA= | ||||
| cloud.google.com/go v0.110.1/go.mod h1:uc+V/WjzxQ7vpkxfJhgW4Q4axWXyfAerpQOuSNDZyFw= | ||||
| 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/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= | ||||
| cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= | ||||
| 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/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= | ||||
| 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/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= | ||||
| cloud.google.com/go/iam v1.0.0 h1:hlQJMovyJJwYjZcTohUH4o1L8Z8kYz+E+W/zktiLCBc= | ||||
| cloud.google.com/go/iam v1.0.0/go.mod h1:ikbQ4f1r91wTmBmmOtBCOtuEOei6taatNXytzB7Cxew= | ||||
| cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= | ||||
| cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= | ||||
| cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= | ||||
| cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= | ||||
| cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= | ||||
| cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= | ||||
| cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= | ||||
|  | @ -147,8 +139,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ | |||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stripe/stripe-go/v74 v74.17.0 h1:qVWSzmADr6gudznuAcPjB9ewzgxfyIhBCkyTbkxJcCw= | ||||
| github.com/stripe/stripe-go/v74 v74.17.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= | ||||
| github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw= | ||||
| github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= | ||||
| github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= | ||||
|  | @ -163,8 +153,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk | |||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||
| golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= | ||||
| golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= | ||||
| golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= | ||||
| golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
|  | @ -186,14 +174,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v | |||
| golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= | ||||
| golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= | ||||
| golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= | ||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||
| golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= | ||||
| golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= | ||||
| golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= | ||||
| golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
|  | @ -241,8 +225,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T | |||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= | ||||
| golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= | ||||
| google.golang.org/api v0.121.0 h1:8Oopoo8Vavxx6gt+sgs8s8/X60WBAtKQq6JqnkF+xow= | ||||
| google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= | ||||
| google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= | ||||
| google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= | ||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||
|  |  | |||
|  | @ -112,6 +112,7 @@ var ( | |||
| 	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} | ||||
| 	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} | ||||
| 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", 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} | ||||
|  |  | |||
|  | @ -523,12 +523,13 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi | |||
| 
 | ||||
| func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	u := v.User() | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if !phoneNumberRegex.MatchString(req.Number) { | ||||
| 	} else if !phoneNumberRegex.MatchString(req.Number) { | ||||
| 		return errHTTPBadRequestPhoneNumberInvalid | ||||
| 	} else if req.Channel != "sms" && req.Channel != "call" { | ||||
| 		return errHTTPBadRequestPhoneNumberVerifyChannelInvalid | ||||
| 	} | ||||
| 	// Check user is allowed to add phone numbers | ||||
| 	if u == nil || (u.IsUser() && u.Tier == nil) { | ||||
|  | @ -545,7 +546,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R | |||
| 	} | ||||
| 	// Actually add the unverified number, and send verification | ||||
| 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification") | ||||
| 	if err := s.verifyPhoneNumber(v, r, req.Number); err != nil { | ||||
| 	if err := s.verifyPhoneNumber(v, r, req.Number, req.Channel); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return s.writeJSON(w, newSuccessResponse()) | ||||
|  | @ -553,7 +554,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R | |||
| 
 | ||||
| func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	u := v.User() | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -572,7 +573,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ | |||
| 
 | ||||
| func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	u := v.User() | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -18,13 +18,14 @@ const ( | |||
| <Response> | ||||
| 	<Pause length="1"/> | ||||
| 	<Say loop="3"> | ||||
| 		You have a notification from notify on topic %s. Message: | ||||
| 		You have a message from notify on topic %s. Message: | ||||
| 		<break time="1s"/> | ||||
| 		%s | ||||
| 		<break time="1s"/> | ||||
| 		End message. | ||||
| 		End of message. | ||||
| 		<break time="1s"/> | ||||
| 		This message was sent by user %s. It will be repeated up to three times. | ||||
| 		This message was sent by user %s. It will be repeated three times. | ||||
| 		To unsubscribe from calls like this, remove your phone number in the notify web app. | ||||
| 		<break time="3s"/> | ||||
| 	</Say> | ||||
| 	<Say>Goodbye.</Say> | ||||
|  | @ -97,11 +98,11 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) { | |||
| 	return string(response), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber string) error { | ||||
| 	ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification") | ||||
| func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error { | ||||
| 	ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification") | ||||
| 	data := url.Values{} | ||||
| 	data.Set("To", phoneNumber) | ||||
| 	data.Set("Channel", "sms") | ||||
| 	data.Set("Channel", channel) | ||||
| 	requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) | ||||
| 	req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -311,9 +311,14 @@ type apiAccountTokenResponse struct { | |||
| 	Expires    int64  `json:"expires,omitempty"` // Unix timestamp | ||||
| } | ||||
| 
 | ||||
| type apiAccountPhoneNumberRequest struct { | ||||
| type apiAccountPhoneNumberVerifyRequest struct { | ||||
| 	Number  string `json:"number"` | ||||
| 	Channel string `json:"channel"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountPhoneNumberAddRequest struct { | ||||
| 	Number string `json:"number"` | ||||
| 	Code   string `json:"code,omitempty"` // Only supplied in "verify" call | ||||
| 	Code   string `json:"code,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type apiAccountTier struct { | ||||
|  |  | |||
|  | @ -188,17 +188,20 @@ | |||
|   "account_basics_password_dialog_button_submit": "Change password", | ||||
|   "account_basics_password_dialog_current_password_incorrect": "Password incorrect", | ||||
|   "account_basics_phone_numbers_title": "Phone numbers", | ||||
|   "account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Adding it will send a verification SMS to your phone.", | ||||
|   "account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Verification can be done via SMS or a phone call.", | ||||
|   "account_basics_phone_numbers_description": "For phone call notifications", | ||||
|   "account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet", | ||||
|   "account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard", | ||||
|   "account_basics_phone_numbers_dialog_title": "Add phone number", | ||||
|   "account_basics_phone_numbers_dialog_number_label": "Phone number", | ||||
|   "account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444", | ||||
|   "account_basics_phone_numbers_dialog_send_verification_button": "Send verification", | ||||
|   "account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS", | ||||
|   "account_basics_phone_numbers_dialog_verify_button_call": "Call me", | ||||
|   "account_basics_phone_numbers_dialog_code_label": "Verification code", | ||||
|   "account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456", | ||||
|   "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", | ||||
|   "account_basics_phone_numbers_dialog_channel_sms": "SMS", | ||||
|   "account_basics_phone_numbers_dialog_channel_call": "Call", | ||||
|   "account_usage_title": "Usage", | ||||
|   "account_usage_of_limit": "of {{limit}}", | ||||
|   "account_usage_unlimited": "Unlimited", | ||||
|  |  | |||
|  | @ -299,14 +299,15 @@ class AccountApi { | |||
|         return await response.json(); // May throw SyntaxError
 | ||||
|     } | ||||
| 
 | ||||
|     async verifyPhoneNumber(phoneNumber) { | ||||
|     async verifyPhoneNumber(phoneNumber, channel) { | ||||
|         const url = accountPhoneVerifyUrl(config.base_url); | ||||
|         console.log(`[AccountApi] Sending phone verification ${url}`); | ||||
|         await fetchOrThrow(url, { | ||||
|             method: "PUT", | ||||
|             headers: withBearerAuth({}, session.token()), | ||||
|             body: JSON.stringify({ | ||||
|                 number: phoneNumber | ||||
|                 number: phoneNumber, | ||||
|                 channel: channel | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| import * as React from 'react'; | ||||
| import {useContext, useState} from 'react'; | ||||
| import { | ||||
|     Alert, | ||||
|     Alert, ButtonGroup, | ||||
|     CardActions, | ||||
|     CardContent, Chip, | ||||
|     FormControl, | ||||
|     FormControl, FormControlLabel, InputLabel, | ||||
|     LinearProgress, | ||||
|     Link, | ||||
|     Portal, | ||||
|     Portal, Radio, RadioGroup, | ||||
|     Select, | ||||
|     Snackbar, | ||||
|     Stack, | ||||
|  | @ -47,12 +47,14 @@ import {AccountContext} from "./App"; | |||
| import DialogFooter from "./DialogFooter"; | ||||
| import {Paragraph} from "./styles"; | ||||
| import CloseIcon from "@mui/icons-material/Close"; | ||||
| import {ContentCopy, Public} from "@mui/icons-material"; | ||||
| import {Check, ContentCopy, DeleteForever, Public} from "@mui/icons-material"; | ||||
| import MenuItem from "@mui/material/MenuItem"; | ||||
| import DialogContentText from "@mui/material/DialogContentText"; | ||||
| import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; | ||||
| import {ProChip} from "./SubscriptionPopup"; | ||||
| import AddIcon from "@mui/icons-material/Add"; | ||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | ||||
| import ListItemText from "@mui/material/ListItemText"; | ||||
| 
 | ||||
| const Account = () => { | ||||
|     if (!session.exists()) { | ||||
|  | @ -408,6 +410,7 @@ const AddPhoneNumberDialog = (props) => { | |||
|     const { t } = useTranslation(); | ||||
|     const [error, setError] = useState(""); | ||||
|     const [phoneNumber, setPhoneNumber] = useState(""); | ||||
|     const [channel, setChannel] = useState("sms"); | ||||
|     const [code, setCode] = useState(""); | ||||
|     const [sending, setSending] = useState(false); | ||||
|     const [verificationCodeSent, setVerificationCodeSent] = useState(false); | ||||
|  | @ -432,7 +435,7 @@ const AddPhoneNumberDialog = (props) => { | |||
|     const verifyPhone = async () => { | ||||
|         try { | ||||
|             setSending(true); | ||||
|             await accountApi.verifyPhoneNumber(phoneNumber); | ||||
|             await accountApi.verifyPhoneNumber(phoneNumber, channel); | ||||
|             setVerificationCodeSent(true); | ||||
|         } catch (e) { | ||||
|             console.log(`[Account] Error sending verification`, e); | ||||
|  | @ -471,18 +474,26 @@ const AddPhoneNumberDialog = (props) => { | |||
|                     {t("account_basics_phone_numbers_dialog_description")} | ||||
|                 </DialogContentText> | ||||
|                 {!verificationCodeSent && | ||||
|                     <TextField | ||||
|                         margin="dense" | ||||
|                         label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|                         aria-label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|                         placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")} | ||||
|                         type="tel" | ||||
|                         value={phoneNumber} | ||||
|                         onChange={ev => setPhoneNumber(ev.target.value)} | ||||
|                         fullWidth | ||||
|                         inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} | ||||
|                         variant="standard" | ||||
|                     /> | ||||
|                     <div style={{display: "flex"}}> | ||||
|                         <TextField | ||||
|                             margin="dense" | ||||
|                             label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|                             aria-label={t("account_basics_phone_numbers_dialog_number_label")} | ||||
|                             placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")} | ||||
|                             type="tel" | ||||
|                             value={phoneNumber} | ||||
|                             onChange={ev => setPhoneNumber(ev.target.value)} | ||||
|                             inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} | ||||
|                             variant="standard" | ||||
|                             sx={{ flexGrow: 1 }} | ||||
|                         /> | ||||
|                         <FormControl sx={{ flexWrap: "nowrap" }}> | ||||
|                             <RadioGroup row sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}> | ||||
|                                 <FormControlLabel value="sms" control={<Radio checked={channel === "sms"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_sms")} /> | ||||
|                                 <FormControlLabel value="call" control={<Radio checked={channel === "call"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_call")} sx={{ marginRight: 0 }} /> | ||||
|                             </RadioGroup> | ||||
|                         </FormControl> | ||||
|                     </div> | ||||
|                 } | ||||
|                 {verificationCodeSent && | ||||
|                     <TextField | ||||
|  | @ -502,7 +513,9 @@ const AddPhoneNumberDialog = (props) => { | |||
|             <DialogFooter status={error}> | ||||
|                 <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> | ||||
|                 <Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}> | ||||
|                     {verificationCodeSent ?t("account_basics_phone_numbers_dialog_check_verification_button")  : t("account_basics_phone_numbers_dialog_send_verification_button")} | ||||
|                     {!verificationCodeSent && channel === "sms" && t("account_basics_phone_numbers_dialog_verify_button_sms")} | ||||
|                     {!verificationCodeSent && channel === "call" && t("account_basics_phone_numbers_dialog_verify_button_call")} | ||||
|                     {verificationCodeSent && t("account_basics_phone_numbers_dialog_check_verification_button")} | ||||
|                 </Button> | ||||
|             </DialogFooter> | ||||
|         </Dialog> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue