From 2c81773d01094be3e4fbe10add54c166469acb37 Mon Sep 17 00:00:00 2001 From: binwiederhier <philipp.heckel@gmail.com> Date: Tue, 16 May 2023 22:27:48 -0400 Subject: [PATCH] Add call verification --- docs/config.md | 4 +-- docs/publish.md | 48 ++++++++++++++++---------- docs/static/img/web-phone-verify.png | Bin 0 -> 22959 bytes go.sum | 18 ---------- server/errors.go | 1 + server/server_account.go | 13 +++---- server/server_twilio.go | 13 +++---- server/types.go | 9 +++-- web/public/static/langs/en.json | 7 ++-- web/src/app/AccountApi.js | 5 +-- web/src/components/Account.js | 49 +++++++++++++++++---------- 11 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 docs/static/img/web-phone-verify.png diff --git a/docs/config.md b/docs/config.md index 353a9d03..d6f6e408 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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 diff --git a/docs/publish.md b/docs/publish.md index 98f3e876..3cca6fc6 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -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 | diff --git a/docs/static/img/web-phone-verify.png b/docs/static/img/web-phone-verify.png new file mode 100644 index 0000000000000000000000000000000000000000..335aeef13848541002456ebbf93ebb17669d04e9 GIT binary patch literal 22959 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 literal 0 HcmV?d00001 diff --git a/go.sum b/go.sum index ccb1ae4d..ff2d580f 100644 --- a/go.sum +++ b/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= diff --git a/server/errors.go b/server/errors.go index a42641b4..b67558db 100644 --- a/server/errors.go +++ b/server/errors.go @@ -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} diff --git a/server/server_account.go b/server/server_account.go index 2330eab8..b9997ef3 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -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 } diff --git a/server/server_twilio.go b/server/server_twilio.go index f8067490..11f58aa7 100644 --- a/server/server_twilio.go +++ b/server/server_twilio.go @@ -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 { diff --git a/server/types.go b/server/types.go index a1d18926..3b733678 100644 --- a/server/types.go +++ b/server/types.go @@ -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 { diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index f2120e5f..588a1f9f 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -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", diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index b5bfcd29..8908f306 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -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 }) }); } diff --git a/web/src/components/Account.js b/web/src/components/Account.js index b4a378e6..b480ea6b 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -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>