Mailgun integration

This commit is contained in:
Daniel Krol 2022-08-01 11:50:16 -04:00
parent 58cefa4c1b
commit 0e36bebdae
19 changed files with 504 additions and 127 deletions

86
README.md Normal file
View file

@ -0,0 +1,86 @@
# Running
Install Golang, at least version 1.17. (Please report any dependencies we seemed to have forgotten)
Check out the repo and run:
```
go run .
```
# Account Creation Settings
When running the server, we should set some environmental variables. These environmental variables determine how account creation is handled. If we do not set these, no users will be able to create an account.
## `ACCOUNT_VERIFICATION_MODE`
The allowed values are `AllowAll`, `Whitelist`, and `EmailVerify`.
### `ACCOUNT_VERIFICATION_MODE=AllowAll`
This should _only be used for development_. Unless you really just want anybody creating accounts on your server, hypothetically DOSing you, etc etc. This puts no restrictions on who can create an account and no process beyond simply pushing the "sign up button" (i.e. sending the "signup" request to the server).
### `ACCOUNT_VERIFICATION_MODE=Whitelist` (default)
With this option, only specifically whitelisted email addresses will be able to create an account. This is recommended for people who are self-hosting their wallet sync server for themself or maybe a few friends.
With this option, we should also specify the whitelist.
#### `ACCOUNT_WHITELIST`
This should be a comma separated list of email addresses with no spaces.
**NOTE**: If your email address has weird characters, unicode, what have you, don't forget to bash-escape it properly.
Single address example:
```
ACCOUNT_WHITELIST=alice@example.com
```
Multiple address example:
```
ACCOUNT_WHITELIST=alice@example.com,bob@example.com,satoshi@example.com
```
_Side note: Since `Whitelist` it is the default value for `ACCOUNT_VERIFICATION_MODE`, and since `ACCOUNT_WHITELIST` is empty by default, the server will by default have an empty whitelist, thus allowing nobody to create an account. This is the default because it's the safest (albeit most useless) configuration._
### `ACCOUNT_VERIFICATION_MODE=EmailVerify`
With this option, you need an account with [Mailgun](mailgun.com). Once registered, you'll end up setting up a domain (including adding DNS records), and getting a private API key. You'll also be able to use a "sandbox" domain just to check that the Mailgun configuration otherwise works before going through the process of setting up your real domain.
With this mode, we require the following additional settings:
#### `MAILGUN_SENDING_DOMAIN`
The address in the "from" field of your registration emails. Your Mailgun sandbox domain works here for testing.
#### `MAILGUN_SERVER_DOMAIN`
The server domain will determine what domain is used for the hyperlink you get in your registration confirmation email. You should generally put the domain you're using to host your wallet sync server.
Realistically, both sending and server domains will often end up being the same thing.
#### `MAILGUN_PRIVATE_API_KEY`
You'll get this in your Mailgun dashboard.
#### `MAILGUN_DOMAIN_IS_EU` (optional)
Whether your sending domain is in the EU. This is related to GDPR stuff I think. Valid values are `true` or `false`, defaulting to `false`.
# You could make a script
For now you could store the stuff in a script:
```
#!/usr/bin/bash
export ACCOUNT_WHITELIST="my-email@example.com"
go run .
```
**NOTE**: If you're using Mailgun, set the file permissions on this script such that only the administrator can read it, since it will contain the Mailgun private API key
_Side note: Eventually we'll create systemd configurations, at which point we will be able to put the env vars in an `EnvironmentFile` instead of a script like this._

35
env/env.go vendored
View file

@ -15,9 +15,15 @@ import (
// We'll replace this with a config file later.
const whitelistKey = "ACCOUNT_WHITELIST"
const verificationModeKey = "ACCOUNT_VERIFICATION_MODE"
const mailgunDomainKey = "MAILGUN_DOMAIN"
const mailgunIsDomainEUKey = "MAILGUN_SENDING_DOMAIN_IS_EU"
const mailgunPrivateAPIKeyKey = "MAILGUN_PRIVATE_API_KEY"
// for the "from" address
const mailgunSendingDomainKey = "MAILGUN_SENDING_DOMAIN"
// for links in the emails
const mailgunServerDomainKey = "MAILGUN_SERVER_DOMAIN"
type AccountVerificationMode string
// Everyone can make an account. Only use for dev purposes.
@ -49,8 +55,8 @@ func GetAccountWhitelist(e EnvInterface, mode AccountVerificationMode) (emails [
return getAccountWhitelist(e.Getenv(whitelistKey), mode)
}
func GetMailgunConfigs(e EnvInterface, mode AccountVerificationMode) (domain string, privateAPIKey string, err error) {
return getMailgunConfigs(e.Getenv(mailgunDomainKey), e.Getenv(mailgunPrivateAPIKeyKey), mode)
func GetMailgunConfigs(e EnvInterface, mode AccountVerificationMode) (sendingDomain string, serverDomain string, isDomainEU bool, privateAPIKey string, err error) {
return getMailgunConfigs(e.Getenv(mailgunSendingDomainKey), e.Getenv(mailgunServerDomainKey), e.Getenv(mailgunIsDomainEUKey), e.Getenv(mailgunPrivateAPIKeyKey), mode)
}
// Factor out the guts of the functions so we can test them by just passing in
@ -101,23 +107,30 @@ func getAccountWhitelist(whitelist string, mode AccountVerificationMode) (emails
return emails, nil
}
func getMailgunConfigs(domain string, privateAPIKey string, mode AccountVerificationMode) (string, string, error) {
if mode != AccountVerificationModeEmailVerify && (domain != "" || privateAPIKey != "") {
return "", "", fmt.Errorf("Do not specify %s or %s in env if %s is not %s",
mailgunDomainKey,
func getMailgunConfigs(sendingDomain string, serverDomain string, isDomainEUStr string, privateAPIKey string, mode AccountVerificationMode) (string, string, bool, string, error) {
if mode != AccountVerificationModeEmailVerify && (sendingDomain != "" || serverDomain != "" || isDomainEUStr != "" || privateAPIKey != "") {
return "", "", false, "", fmt.Errorf("Do not specify %s, %s, %s or %s in env if %s is not %s",
mailgunSendingDomainKey,
mailgunServerDomainKey,
mailgunIsDomainEUKey,
mailgunPrivateAPIKeyKey,
verificationModeKey,
AccountVerificationModeEmailVerify,
)
}
if mode == AccountVerificationModeEmailVerify && (domain == "" || privateAPIKey == "") {
return "", "", fmt.Errorf("Specify %s and %s in env if %s is %s",
mailgunDomainKey,
if mode == AccountVerificationModeEmailVerify && (sendingDomain == "" || serverDomain == "" || privateAPIKey == "") {
return "", "", false, "", fmt.Errorf("Specify %s, %s and %s in env if %s is %s",
mailgunSendingDomainKey,
mailgunServerDomainKey,
mailgunPrivateAPIKeyKey,
verificationModeKey,
AccountVerificationModeEmailVerify,
)
}
return domain, privateAPIKey, nil
if isDomainEUStr != "true" && isDomainEUStr != "false" && isDomainEUStr != "" {
return "", "", false, "", fmt.Errorf("%s must be 'true' or 'false'", mailgunIsDomainEUKey)
}
return sendingDomain, serverDomain, isDomainEUStr == "true", privateAPIKey, nil
}

61
env/env_test.go vendored
View file

@ -118,33 +118,63 @@ func TestMailgunConfigs(t *testing.T) {
tt := []struct {
name string
domain string
sendingDomain string
serverDomain string
privateAPIKey string
isDomainEUStr string
expectDomainEU bool
mode AccountVerificationMode
expectErr bool
}{
{
name: "success",
name: "success with domain eu set",
mode: AccountVerificationModeEmailVerify,
domain: "www.example.com",
sendingDomain: "sending.example.com",
serverDomain: "server.example.com",
privateAPIKey: "my-private-api-key",
isDomainEUStr: "true",
expectDomainEU: true,
expectErr: false,
},
{
name: "success without domain eu set",
mode: AccountVerificationModeEmailVerify,
sendingDomain: "sending.example.com",
serverDomain: "server.example.com",
privateAPIKey: "my-private-api-key",
expectErr: false,
},
{
name: "wrong mode with domain",
mode: AccountVerificationModeWhitelist,
domain: "www.example.com",
name: "invalid is domain eu",
mode: AccountVerificationModeEmailVerify,
sendingDomain: "sending.example.com",
serverDomain: "server.example.com",
privateAPIKey: "my-private-api-key",
isDomainEUStr: "invalid",
expectErr: true,
},
{
name: "wrong mode with private api key",
name: "wrong mode with domain keys set",
mode: AccountVerificationModeWhitelist,
sendingDomain: "sending.example.com",
serverDomain: "server.example.com",
expectErr: true,
},
{
name: "wrong mode with private api key key set",
mode: AccountVerificationModeWhitelist,
privateAPIKey: "my-private-api-key",
expectErr: true,
},
{
name: "missing domain",
name: "wrong mode with is domain eu key set",
mode: AccountVerificationModeWhitelist,
isDomainEUStr: "true",
expectErr: true,
},
{
name: "missing domains",
mode: AccountVerificationModeEmailVerify,
privateAPIKey: "my-private-api-key",
expectErr: true,
@ -152,25 +182,32 @@ func TestMailgunConfigs(t *testing.T) {
{
name: "missing private api key",
mode: AccountVerificationModeEmailVerify,
domain: "www.example.com",
sendingDomain: "sending.example.com",
serverDomain: "server.example.com",
expectErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
domain, privateAPIKey, err := getMailgunConfigs(tc.domain, tc.privateAPIKey, tc.mode)
sendingDomain, serverDomain, isDomainEu, privateAPIKey, err := getMailgunConfigs(tc.sendingDomain, tc.serverDomain, tc.isDomainEUStr, tc.privateAPIKey, tc.mode)
if tc.expectErr && err == nil {
t.Errorf("Expected err")
}
if !tc.expectErr && err != nil {
t.Errorf("Unexpected err: %s", err.Error())
}
if !tc.expectErr && tc.domain != domain {
t.Errorf("Expected domain to be set")
if !tc.expectErr && tc.sendingDomain != sendingDomain {
t.Errorf("Expected sendingDomain to be set")
}
if !tc.expectErr && tc.serverDomain != serverDomain {
t.Errorf("Expected serverDomain to be set")
}
if !tc.expectErr && tc.privateAPIKey != privateAPIKey {
t.Errorf("Expected privateAPIKey to be set")
}
if !tc.expectErr && tc.expectDomainEU != isDomainEu {
t.Errorf("Expected isDomainEu to be %v", tc.expectDomainEU)
}
})
}

6
go.mod
View file

@ -3,6 +3,7 @@ module lbryio/lbry-id
go 1.17
require (
github.com/mailgun/mailgun-go/v4 v4.8.1
github.com/mattn/go-sqlite3 v1.14.9
github.com/prometheus/client_golang v1.11.0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
@ -12,7 +13,12 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect

18
go.sum
View file

@ -11,7 +11,14 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@ -38,9 +45,12 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
@ -50,19 +60,25 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailgun/mailgun-go/v4 v4.8.1 h1:1+MdKakJuXnW2JJDbyPdO1ngAANOyHyVPxQvFF8Sq6c=
github.com/mailgun/mailgun-go/v4 v4.8.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@ -89,6 +105,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -146,4 +163,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -1,15 +1,107 @@
package mail
import (
"context"
"fmt"
"log"
"time"
"github.com/mailgun/mailgun-go/v4"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/env"
"lbryio/lbry-id/server/paths"
)
const MAILGUN_DEBUG = false
// useful with MAILGUN_DEBUG to see what gets called with what
const MAILGUN_DRY_RUN = false
type MailInterface interface {
SendVerificationEmail(email auth.Email, token auth.VerifyTokenString) error
SendVerificationEmail(auth.Email, auth.VerifyTokenString) error
}
type Mail struct{}
type Mail struct {
ServerPort int
Env env.EnvInterface
}
// Split out everything I can to make it testable. Right now
// mailgun.MailgunImpl is inspectable enough to test but
// mailgun.Message is not.
func (m *Mail) prepareMessage(token auth.VerifyTokenString) (
mg *mailgun.MailgunImpl,
sender string,
subject string,
text string,
html string,
err error,
) {
verificationMode, err := env.GetAccountVerificationMode(m.Env)
if err != nil {
return
}
sendingDomain, serverDomain, isDomainEU, privateAPIKey, err := env.GetMailgunConfigs(m.Env, verificationMode)
if err != nil {
return
}
// Create an instance of the Mailgun Client
mg = mailgun.NewMailgun(sendingDomain, privateAPIKey)
// see https://help.mailgun.com/hc/en-us/articles/360007512013-Can-I-migrate-my-domain-to-EU-
if isDomainEU {
mg.SetAPIBase("https://api.eu.mailgun.net/v3")
}
sender = fmt.Sprintf("wallet-sync@%s", sendingDomain)
subject = fmt.Sprintf("Verify your wallet sync account on %s", serverDomain)
url := fmt.Sprintf("https://%s:%d%s?verifyToken=%s", serverDomain, m.ServerPort, paths.PathVerify, token)
text = fmt.Sprintf("Click here to verify your account:\n\n%s", url)
html = fmt.Sprintf("Click here to verify your account:\n\n<a href=\"%s\">%s</a>", url, url)
if MAILGUN_DEBUG {
log.Printf(
"NewMessage\n\n%s\n\n%s\n\n%s\n\n%s",
sender, subject, text, html,
)
}
return
}
func (m *Mail) SendVerificationEmail(recipient auth.Email, token auth.VerifyTokenString) (err error) {
mg, sender, subject, text, html, err := m.prepareMessage(token)
if err != nil {
return err
}
message := mg.NewMessage(sender, subject, text, string(recipient))
message.SetHtml(html)
if MAILGUN_DEBUG {
log.Printf("Send\n\n%+v\n", message)
}
if !MAILGUN_DRY_RUN {
// Send the message with a 10 second timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
resp, id, err := mg.Send(ctx, message)
if err != nil {
log.Fatal(err)
}
if MAILGUN_DEBUG {
log.Printf("Sent Mailgun message. ID: %s Resp: %s\n", id, resp)
}
}
func (m *Mail) SendVerificationEmail(auth.Email, auth.VerifyTokenString) (err error) {
return
}

99
mail/mail_test.go Normal file
View file

@ -0,0 +1,99 @@
package mail
import (
"fmt"
"strings"
"testing"
"lbryio/lbry-id/auth"
)
type TestEnv struct {
env map[string]string
}
func (e *TestEnv) Getenv(key string) string {
return e.env[key]
}
func TestPrepareEmailNotEU(t *testing.T) {
const apiKey = "mg-api-key"
const sendingDomain = "sending.example.com"
const serverDomain = "server.example.com"
const port = 1234
serverDomainWithPort := serverDomain + ":" + fmt.Sprint(port)
const recipient = auth.Email("recipient@example.com")
const token = auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
"MAILGUN_PRIVATE_API_KEY": apiKey,
"MAILGUN_SENDING_DOMAIN": sendingDomain,
"MAILGUN_SERVER_DOMAIN": serverDomain,
}
m := Mail{port, &TestEnv{env}}
mg, sender, subject, text, html, err := m.prepareMessage(token)
if err != nil || mg == nil {
t.Errorf("Unexpected values from prepareMessage: %+v %s", mg, err.Error())
}
if got, want := mg.APIKey(), apiKey; want != got {
t.Errorf("Unexpected mg.APIKey(). Got: %s Want: %s", want, got)
}
if got, want := mg.APIBase(), "https://api.mailgun.net/v3"; want != got {
t.Errorf("Unexpected mg.APIBase(). Got: %s Want: %s", want, got)
}
if got, want := sender, "wallet-sync@sending.example.com"; want != got {
t.Errorf("Unexpected sender. Got: %s Want: %s", want, got)
}
if !strings.Contains(subject, serverDomain) {
t.Errorf("Expected subject to contain %s. Got: %s", serverDomain, subject)
}
if !strings.Contains(text, serverDomainWithPort) {
t.Errorf("Expected text to contain %s. Got: %s", serverDomainWithPort, text)
}
if !strings.Contains(html, serverDomainWithPort) {
t.Errorf("Expected html to contain %s. Got: %s", serverDomainWithPort, html)
}
}
func TestPrepareEmailEU(t *testing.T) {
const apiKey = "mg-api-key"
const sendingDomain = "sending.example.com"
const serverDomain = "server.example.com"
const recipient = auth.Email("recipient@example.com")
const token = auth.VerifyTokenString("abcd1234abcd1234abcd1234abcd1234")
const port = 1234
env := map[string]string{
"MAILGUN_SENDING_DOMAIN_IS_EU": "true",
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
"MAILGUN_PRIVATE_API_KEY": apiKey,
"MAILGUN_SENDING_DOMAIN": sendingDomain,
"MAILGUN_SERVER_DOMAIN": serverDomain,
}
m := Mail{port, &TestEnv{env}}
mg, _, _, _, _, err := m.prepareMessage(token)
if err != nil || mg == nil {
t.Errorf("Unexpected values from prepareMessage: %+v %s", mg, err.Error())
}
if got, want := mg.APIBase(), "https://api.eu.mailgun.net/v3"; want != got {
t.Errorf("Unexpected mg.APIBase(). Got: %s Want: %s", want, got)
}
}

20
main.go
View file

@ -26,7 +26,7 @@ func storeInit() (s store.Store) {
// Output information about the email verification mode so the user can confirm
// what they set. Also trigger an error on startup if there's a configuration
// problem.
func logEmailVerificationMode(e *env.Env) (err error) {
func logEmailVerificationConfigs(e *env.Env) (err error) {
verificationMode, err := env.GetAccountVerificationMode(e)
if err != nil {
return
@ -37,7 +37,7 @@ func logEmailVerificationMode(e *env.Env) (err error) {
}
// just to report config errors to the user on startup
_, _, err = env.GetMailgunConfigs(e, verificationMode)
sendingDomain, serverDomain, _, _, err := env.GetMailgunConfigs(e, verificationMode)
if err != nil {
return
}
@ -47,17 +47,29 @@ func logEmailVerificationMode(e *env.Env) (err error) {
} else {
log.Printf("Account verification mode: %s", verificationMode)
}
if verificationMode == env.AccountVerificationModeEmailVerify {
log.Printf("Mailgun domains: %s for sending addresses, %s for links in the email", sendingDomain, serverDomain)
}
return
}
func main() {
e := env.Env{}
if err := logEmailVerificationMode(&e); err != nil {
if err := logEmailVerificationConfigs(&e); err != nil {
log.Fatal(err.Error())
}
store := storeInit()
srv := server.Init(&auth.Auth{}, &store, &e, &mail.Mail{})
// The port that the sync server serves from.
internalPort := 8090
// The port that the webserver (Caddy recommended), which reverse proxies to
// the sync server, should use to serve to the outside world. This will be
// used for links in emails.
externalPort := 8091
srv := server.Init(&auth.Auth{}, &store, &e, &mail.Mail{externalPort, &e}, internalPort)
srv.Serve()
}

View file

@ -218,7 +218,7 @@ func (s *Server) verify(w http.ResponseWriter, req *http.Request) {
err := s.store.VerifyAccount(token)
if err == store.ErrNoTokenForUser {
http.Error(w, "The verification token was not found, or it expired. Try generating a new one from your app.", http.StatusForbidden)
http.Error(w, "The verification token was not found, already used, or expired. If you want to try again, generate a new one from your app.", http.StatusForbidden)
return
} else if err != nil {
http.Error(w, "Something went wrong trying to verify your account.", http.StatusInternalServerError)

View file

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
)
@ -21,11 +22,11 @@ func TestServerRegisterSuccess(t *testing.T) {
}
testMail := TestMail{}
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
s := Server{&testAuth, testStore, &TestEnv{env}, &testMail}
s := Server{&testAuth, testStore, &TestEnv{env}, &testMail, TestPort}
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
req := httptest.NewRequest(http.MethodPost, PathRegister, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.register(w, req)
@ -128,11 +129,11 @@ func TestServerRegisterErrors(t *testing.T) {
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234", FailGenToken: tc.failGenToken}
testMail := TestMail{SendVerificationEmailError: tc.mailError}
testStore := TestStore{Errors: tc.storeErrors}
s := Server{&testAuth, &testStore, &TestEnv{env}, &testMail}
s := Server{&testAuth, &testStore, &TestEnv{env}, &testMail, TestPort}
// Make request
requestBody := fmt.Sprintf(`{"email": "%s", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"}`, tc.email)
req := httptest.NewRequest(http.MethodPost, PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
w := httptest.NewRecorder()
s.register(w, req)
@ -226,11 +227,11 @@ func TestServerRegisterAccountVerification(t *testing.T) {
testStore := &TestStore{}
testAuth := TestAuth{TestNewVerifyTokenString: "abcd1234abcd1234abcd1234abcd1234"}
testMail := TestMail{}
s := Server{&testAuth, testStore, &TestEnv{tc.env}, &testMail}
s := Server{&testAuth, testStore, &TestEnv{tc.env}, &testMail, TestPort}
requestBody := []byte(`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" }`)
req := httptest.NewRequest(http.MethodPost, PathRegister, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathRegister, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.register(w, req)
@ -331,10 +332,10 @@ func TestServerResendVerifyEmailSuccess(t *testing.T) {
env := map[string]string{
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
}
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail}
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort}
requestBody := []byte(`{"email": "abc@example.com"}`)
req := httptest.NewRequest(http.MethodPost, PathVerify, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathVerify, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.resendVerifyEmail(w, req)
@ -428,7 +429,7 @@ func TestServerResendVerifyEmailErrors(t *testing.T) {
// Set this up to fail according to specification
testStore := TestStore{Errors: tc.storeErrors}
testMail := TestMail{SendVerificationEmailError: tc.mailError}
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail}
s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail, TestPort}
// Make request
var requestBody []byte
@ -437,7 +438,7 @@ func TestServerResendVerifyEmailErrors(t *testing.T) {
} else {
requestBody = []byte(`{"email": "abc@example.com"}`)
}
req := httptest.NewRequest(http.MethodPost, PathVerify, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathVerify, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.resendVerifyEmail(w, req)
@ -467,9 +468,9 @@ func TestServerResendVerifyEmailErrors(t *testing.T) {
func TestServerVerifyAccountSuccess(t *testing.T) {
testStore := TestStore{}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
q := req.URL.Query()
q.Add("verifyToken", "abcd1234abcd1234abcd1234abcd1234")
req.URL.RawQuery = q.Encode()
@ -510,7 +511,7 @@ func TestServerVerifyAccountErrors(t *testing.T) {
name: "token not found", // including expired
token: "abcd1234abcd1234abcd1234abcd1234",
expectedStatusCode: http.StatusForbidden,
expectedBody: "The verification token was not found, or it expired. Try generating a new one from your app.",
expectedBody: "The verification token was not found, already used, or expired. If you want to try again, generate a new one from your app.",
storeErrors: TestStoreFunctionsErrors{VerifyAccount: store.ErrNoTokenForUser},
expectedCallVerifyAccount: true,
},
@ -528,10 +529,10 @@ func TestServerVerifyAccountErrors(t *testing.T) {
// Set this up to fail according to specification
testStore := TestStore{Errors: tc.storeErrors}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
// Make request
req := httptest.NewRequest(http.MethodGet, PathVerify, nil)
req := httptest.NewRequest(http.MethodGet, paths.PathVerify, nil)
q := req.URL.Query()
q.Add("verifyToken", tc.token)
req.URL.RawQuery = q.Encode()

View file

@ -11,17 +11,18 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
)
func TestServerAuthHandlerSuccess(t *testing.T) {
testAuth := TestAuth{TestNewAuthTokenString: auth.AuthTokenString("seekrit")}
testStore := TestStore{}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`)
req := httptest.NewRequest(http.MethodPost, PathAuthToken, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.getAuthToken(w, req)
@ -103,12 +104,12 @@ func TestServerAuthHandlerErrors(t *testing.T) {
if tc.authFailGenToken { // TODO - TestAuth{Errors:authErrors}
testAuth.FailGenToken = true
}
server := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
server := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
// Make request
// So long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out
requestBody := fmt.Sprintf(`{"deviceId": "dev-1", "email": "%s", "password": "123"}`, tc.email)
req := httptest.NewRequest(http.MethodPost, PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
req := httptest.NewRequest(http.MethodPost, paths.PathAuthToken, bytes.NewBuffer([]byte(requestBody)))
w := httptest.NewRecorder()
server.getAuthToken(w, req)

View file

@ -10,6 +10,7 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
)
@ -66,9 +67,9 @@ func TestServerGetClientSalt(t *testing.T) {
Errors: tc.storeErrors,
}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
req := httptest.NewRequest(http.MethodGet, PathClientSaltSeed, nil)
req := httptest.NewRequest(http.MethodGet, paths.PathClientSaltSeed, nil)
q := req.URL.Query()
q.Add("email", string(tc.emailGetParam))
req.URL.RawQuery = q.Encode()

View file

@ -14,6 +14,7 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
"lbryio/lbry-id/wallet"
)
@ -100,7 +101,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
env := map[string]string{
"ACCOUNT_WHITELIST": "abc@example.com",
}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort}
////////////////////
t.Log("Request: Register email address - any device")
@ -111,7 +112,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodPost,
s.register,
PathRegister,
paths.PathRegister,
&registerResponse,
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
)
@ -127,7 +128,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken1,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
)
@ -155,7 +156,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken2,
`{"deviceId": "dev-2", "email": "abc@example.com", "password": "123"}`,
)
@ -175,7 +176,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodPost,
s.postWallet,
PathWallet,
paths.PathWallet,
&walletPostResponse,
fmt.Sprintf(`{
"token": "%s",
@ -196,7 +197,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken2.Token),
fmt.Sprintf("%s?token=%s", paths.PathWallet, authToken2.Token),
&walletGetResponse,
"",
)
@ -221,7 +222,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodPost,
s.postWallet,
PathWallet,
paths.PathWallet,
&walletPostResponse,
fmt.Sprintf(`{
"token": "%s",
@ -241,7 +242,7 @@ func TestIntegrationWalletUpdates(t *testing.T) {
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken1.Token),
fmt.Sprintf("%s?token=%s", paths.PathWallet, authToken1.Token),
&walletGetResponse,
"",
)
@ -270,7 +271,7 @@ func TestIntegrationChangePassword(t *testing.T) {
env := map[string]string{
"ACCOUNT_WHITELIST": "abc@example.com",
}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &TestMail{}, TestPort}
////////////////////
t.Log("Request: Register email address")
@ -281,7 +282,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.register,
PathRegister,
paths.PathRegister,
&registerResponse,
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
)
@ -297,7 +298,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodGet,
s.getClientSaltSeed,
fmt.Sprintf("%s?email=%s", PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
fmt.Sprintf("%s?email=%s", paths.PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
&clientSaltSeedResponse,
"",
)
@ -319,7 +320,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
)
@ -347,7 +348,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.changePassword,
PathPassword,
paths.PathPassword,
&changePasswordResponse,
`{"email": "abc@example.com", "oldPassword": "123", "newPassword": "456", "clientSaltSeed": "8678def95678def98678def95678def98678def95678def98678def95678def9"}`,
)
@ -362,7 +363,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodGet,
s.getClientSaltSeed,
fmt.Sprintf("%s?email=%s", PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
fmt.Sprintf("%s?email=%s", paths.PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
&clientSaltSeedResponse,
"",
)
@ -382,7 +383,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.postWallet,
PathWallet,
paths.PathWallet,
&walletPostResponse,
fmt.Sprintf(`{
"token": "%s",
@ -402,7 +403,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "456"}`,
)
@ -429,7 +430,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.postWallet,
PathWallet,
paths.PathWallet,
&walletPostResponse,
fmt.Sprintf(`{
"token": "%s",
@ -449,7 +450,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.changePassword,
PathPassword,
paths.PathPassword,
&changePasswordResponse,
fmt.Sprintf(`{
"encryptedWallet": "my-encrypted-wallet-2",
@ -472,7 +473,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodGet,
s.getClientSaltSeed,
fmt.Sprintf("%s?email=%s", PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
fmt.Sprintf("%s?email=%s", paths.PathClientSaltSeed, base64.StdEncoding.EncodeToString([]byte("abc@example.com"))),
&clientSaltSeedResponse,
"",
)
@ -492,7 +493,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken.Token),
fmt.Sprintf("%s?token=%s", paths.PathWallet, authToken.Token),
&walletGetResponse,
"",
)
@ -507,7 +508,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "789"}`,
)
@ -534,7 +535,7 @@ func TestIntegrationChangePassword(t *testing.T) {
t,
http.MethodGet,
s.getWallet,
fmt.Sprintf("%s?token=%s", PathWallet, authToken.Token),
fmt.Sprintf("%s?token=%s", paths.PathWallet, authToken.Token),
&walletGetResponse,
"",
)
@ -561,7 +562,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
"ACCOUNT_VERIFICATION_MODE": "EmailVerify",
}
testMail := TestMail{}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &testMail}
s := Server{&auth.Auth{}, &st, &TestEnv{env}, &testMail, TestPort}
////////////////////
t.Log("Request: Register email address")
@ -572,7 +573,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
t,
http.MethodPost,
s.register,
PathRegister,
paths.PathRegister,
&registerResponse,
`{"email": "abc@example.com", "password": "123", "clientSaltSeed": "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"}`,
)
@ -594,7 +595,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
t,
http.MethodPost,
s.resendVerifyEmail,
PathResendVerify,
paths.PathResendVerify,
&resendVerifyResponse,
`{"email": "abc@example.com"}`,
)
@ -616,7 +617,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
)
@ -631,7 +632,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
t,
http.MethodGet,
s.verify,
PathVerify+"?verifyToken="+string(testMail.SendVerificationEmailCall.Token),
paths.PathVerify+"?verifyToken="+string(testMail.SendVerificationEmailCall.Token),
nil,
``,
)
@ -649,7 +650,7 @@ func TestIntegrationVerifyAccount(t *testing.T) {
t,
http.MethodPost,
s.getAuthToken,
PathAuthToken,
paths.PathAuthToken,
&authToken,
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
)

View file

@ -10,6 +10,7 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
"lbryio/lbry-id/wallet"
)
@ -168,7 +169,7 @@ func TestServerChangePassword(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
testStore := TestStore{Errors: tc.storeErrors}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
// Whether we passed in wallet fields (these test cases should be passing
// in all of them or none of them, so we only test EncryptedWallet). This
@ -192,7 +193,7 @@ func TestServerChangePassword(t *testing.T) {
}`, tc.newEncryptedWallet, tc.newSequence, tc.newHmac, tc.email, oldPassword, newPassword, clientSaltSeed),
)
req := httptest.NewRequest(http.MethodPost, PathPassword, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathPassword, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
s.changePassword(w, req)

19
server/paths/paths.go Normal file
View file

@ -0,0 +1,19 @@
package paths
// TODO proper doc comments!
const ApiVersion = "3"
const PathPrefix = "/api/" + ApiVersion
const PathAuthToken = PathPrefix + "/auth/full"
const PathWallet = PathPrefix + "/wallet"
const PathRegister = PathPrefix + "/signup"
const PathPassword = PathPrefix + "/password"
const PathVerify = PathPrefix + "/verify"
const PathResendVerify = PathPrefix + "/verify/resend"
const PathClientSaltSeed = PathPrefix + "/client-salt-seed"
const PathUnknownEndpoint = PathPrefix + "/"
const PathWrongApiVersion = "/api/"
const PathPrometheus = "/metrics"

View file

@ -2,6 +2,7 @@ package server
import (
"encoding/json"
"fmt"
"log"
"net/http"
@ -10,32 +11,16 @@ import (
"lbryio/lbry-id/auth"
"lbryio/lbry-id/env"
"lbryio/lbry-id/mail"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
)
// TODO proper doc comments!
const ApiVersion = "3"
const PathPrefix = "/api/" + ApiVersion
const PathAuthToken = PathPrefix + "/auth/full"
const PathWallet = PathPrefix + "/wallet"
const PathRegister = PathPrefix + "/signup"
const PathPassword = PathPrefix + "/password"
const PathVerify = PathPrefix + "/verify"
const PathResendVerify = PathPrefix + "/verify/resend"
const PathClientSaltSeed = PathPrefix + "/client-salt-seed"
const PathUnknownEndpoint = PathPrefix + "/"
const PathWrongApiVersion = "/api/"
const PathPrometheus = "/metrics"
type Server struct {
auth auth.AuthInterface
store store.StoreInterface
env env.EnvInterface
mail mail.MailInterface
port int
}
// TODO If I capitalize the `auth` `store` and `env` fields of Store{} I can
@ -45,8 +30,9 @@ func Init(
store store.StoreInterface,
env env.EnvInterface,
mail mail.MailInterface,
port int,
) *Server {
return &Server{auth, store, env, mail}
return &Server{auth, store, env, mail, port}
}
type ErrorResponse struct {
@ -182,24 +168,24 @@ func (s *Server) unknownEndpoint(w http.ResponseWriter, req *http.Request) {
}
func (s *Server) wrongApiVersion(w http.ResponseWriter, req *http.Request) {
errorJson(w, http.StatusNotFound, "Wrong API version. Current version is "+ApiVersion+".")
errorJson(w, http.StatusNotFound, "Wrong API version. Current version is "+paths.ApiVersion+".")
return
}
func (s *Server) Serve() {
http.HandleFunc(PathAuthToken, s.getAuthToken)
http.HandleFunc(PathWallet, s.handleWallet)
http.HandleFunc(PathRegister, s.register)
http.HandleFunc(PathPassword, s.changePassword)
http.HandleFunc(PathVerify, s.verify)
http.HandleFunc(PathResendVerify, s.resendVerifyEmail)
http.HandleFunc(PathClientSaltSeed, s.getClientSaltSeed)
http.HandleFunc(paths.PathAuthToken, s.getAuthToken)
http.HandleFunc(paths.PathWallet, s.handleWallet)
http.HandleFunc(paths.PathRegister, s.register)
http.HandleFunc(paths.PathPassword, s.changePassword)
http.HandleFunc(paths.PathVerify, s.verify)
http.HandleFunc(paths.PathResendVerify, s.resendVerifyEmail)
http.HandleFunc(paths.PathClientSaltSeed, s.getClientSaltSeed)
http.HandleFunc(PathUnknownEndpoint, s.unknownEndpoint)
http.HandleFunc(PathWrongApiVersion, s.wrongApiVersion)
http.HandleFunc(paths.PathUnknownEndpoint, s.unknownEndpoint)
http.HandleFunc(paths.PathWrongApiVersion, s.wrongApiVersion)
http.Handle(PathPrometheus, promhttp.Handler())
http.Handle(paths.PathPrometheus, promhttp.Handler())
log.Println("Serving at localhost:8090")
http.ListenAndServe("localhost:8090", nil)
log.Printf("Serving at localhost:%d\n", s.port)
http.ListenAndServe(fmt.Sprintf("localhost:%d", s.port), nil)
}

View file

@ -11,10 +11,13 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
"lbryio/lbry-id/wallet"
)
const TestPort = 8090
// Implementing interfaces for stubbed out packages
type SendVerificationEmailCall struct {
@ -321,7 +324,7 @@ func TestServerHelperCheckAuth(t *testing.T) {
Errors: tc.storeErrors,
TestAuthToken: auth.AuthToken{Token: auth.AuthTokenString("seekrit"), Scope: tc.userScope},
}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}, TestPort}
w := httptest.NewRecorder()
authToken := s.checkAuth(w, testStore.TestAuthToken.Token, tc.requiredScope)
@ -418,7 +421,7 @@ func TestServerHelperGetPostDataErrors(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
// Make request
req := httptest.NewRequest(tc.method, PathAuthToken, bytes.NewBuffer([]byte(tc.requestBody)))
req := httptest.NewRequest(tc.method, paths.PathAuthToken, bytes.NewBuffer([]byte(tc.requestBody)))
w := httptest.NewRecorder()
success := getPostData(w, req, &TestReqStruct{})

View file

@ -11,6 +11,7 @@ import (
"testing"
"lbryio/lbry-id/auth"
"lbryio/lbry-id/server/paths"
"lbryio/lbry-id/store"
"lbryio/lbry-id/wallet"
)
@ -77,9 +78,9 @@ func TestServerGetWallet(t *testing.T) {
}
testEnv := TestEnv{}
s := Server{&testAuth, &testStore, &testEnv, &TestMail{}}
s := Server{&testAuth, &testStore, &testEnv, &TestMail{}, TestPort}
req := httptest.NewRequest(http.MethodGet, PathWallet, nil)
req := httptest.NewRequest(http.MethodGet, paths.PathWallet, nil)
q := req.URL.Query()
q.Add("token", string(testStore.TestAuthToken.Token))
req.URL.RawQuery = q.Encode()
@ -235,7 +236,7 @@ func TestServerPostWallet(t *testing.T) {
Errors: tc.storeErrors,
}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}}
s := Server{&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort}
requestBody := []byte(
fmt.Sprintf(`{
@ -246,7 +247,7 @@ func TestServerPostWallet(t *testing.T) {
}`, testStore.TestAuthToken.Token, tc.newEncryptedWallet, tc.newSequence, tc.newHmac),
)
req := httptest.NewRequest(http.MethodPost, PathWallet, bytes.NewBuffer(requestBody))
req := httptest.NewRequest(http.MethodPost, paths.PathWallet, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
// test handleWallet while we're at it, which is a dispatch for get and post

View file

@ -405,7 +405,7 @@ class Client():
# In a real client, this is where you may consider
# a) Offering to have the user change their password
# b) Try update_derived_secrets() and get_auth_token() silently, for the unlikely case that the user changed their password back and forth
print ("Failed to get the auth token. Do you need to update this client's password (set_local_password())?")
print ("Failed to get the auth token. Do you need to verify your email address? Or update this client's password (set_local_password())?")
print ("Or, in the off-chance the user changed their password back and forth, try updating secrets (update_derived_secrets()) to get the latest salt seed.")
return
self.auth_token = token