Customize app URLs in the emails (#290)

* Allow to customize app url in emails

* Validate env var

* Test

* Add license

* Add guide
This commit is contained in:
Sung Won Cho 2019-10-29 20:21:08 -07:00 committed by GitHub
commit 5d6ad342f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 427 additions and 197 deletions

View file

@ -12,7 +12,13 @@ The following log documentes the history of the server project.
### [Unreleased]
- N/A
#### Upgrade Guide
* Please define a new environment variable `WebURL` whose value is the URL to your Dnote server, without the trailing slash. (e.g. `https://my-server.com`) (Please see #290)
#### Fixed
- Allow to customize the app URL in the emails (#290)
### 0.2.0 - 2019-10-28
@ -24,6 +30,9 @@ The following log documentes the history of the server project.
- Treat a linebreak as a new line in the preview (#261)
- Allow to have multiple editor states for adding and editing notes (#260)
#### Fixed
- Fix jumping focus on editor (#265)
### 0.1.1 - 2019-09-30

View file

@ -23,10 +23,13 @@ DBPort=5432 \
DBName=dnote \
DBUser=$user \
DBPassword=$password \
WebURL=$webURL
dnote-server start
```
Replace $user and $password with the credentials of the Postgres user that owns the `dnote` database.
Replace `$user`, `$password` with the credentials of the Postgres user that owns the `dnote` database.
Replace `$webURL` with the full URL to your server, without a trailing slash (e.g. `https://your.server`).
By default, dnote server will run on the port 3000.
@ -91,6 +94,7 @@ Environment=GO_ENV=PRODUCTION
Environment=DBHost=localhost
Environment=DBPort=5432
Environment=DBName=dnote
Environment=WebURL=$WebURL
Environment=DBUser=$DBUser
Environment=DBPassword=$DBPassword
Environment=SmtpHost=
@ -101,9 +105,9 @@ Environment=SmtpPassword=
WantedBy=multi-user.target
```
Replace `$user`, `$DBUser`, and `$DBPassword` with the actual values.
Replace `$user`, `$WebURL`, `$DBUser`, and `$DBPassword` with the actual values.
Optionally, if you would like to send email digests, populate `SmtpHost`, `SmtpUsername`, and `SmtpPassword`.
Optionally, if you would like to send spaced repetitions throught email, populate `SmtpHost`, `SmtpUsername`, and `SmtpPassword`.
2. Reload the change by running `sudo systemctl daemon-reload`.
3. Enable the Daemon by running `sudo systemctl enable dnote`.`
@ -144,3 +148,7 @@ e.g.
editor: nvim
apiEndpoint: my-dnote-server.com/api
```
#### Browser extension
Navigate into the 'Settings' tab and set the values for 'API URL', and 'Web URL'.

View file

@ -9,3 +9,5 @@ DBPassword=
SmtpUsername=mock-SmtpUsername
SmtpPassword=mock-SmtpPassword
SmtpHost=mock-SmtpHost
WebURL=http://localhost:3000

View file

@ -9,3 +9,5 @@ DBPassword=
SmtpUsername=mock-SmtpUsername
SmtpPassword=mock-SmtpPassword
SmtpHost=mock-SmtpHost
WebURL=http://localhost:3000

View file

@ -133,12 +133,10 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
}
subject := "Reset your password"
data := struct {
Subject string
Token string
}{
subject,
resetToken,
data := mailer.EmailResetPasswordTmplData{
Subject: subject,
Token: resetToken,
WebURL: a.WebURL,
}
email := mailer.NewEmail("noreply@getdnote.com", []string{params.Email}, subject)
if err := email.ParseTemplate(mailer.EmailTypeResetPassword, data); err != nil {

View file

@ -20,7 +20,6 @@ package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"time"
@ -36,9 +35,9 @@ func TestGetMe(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -64,9 +63,9 @@ func TestCreateResetToken(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -97,9 +96,9 @@ func TestCreateResetToken(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -126,9 +125,9 @@ func TestResetPassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -172,9 +171,9 @@ func TestResetPassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -210,9 +209,9 @@ func TestResetPassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -247,9 +246,9 @@ func TestResetPassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -295,9 +294,9 @@ func TestResetPassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/dnote/dnote/pkg/assert"
@ -78,9 +77,9 @@ func TestClassicPresignin(t *testing.T) {
t.Run(fmt.Sprintf("presignin %s", tc.email), func(t *testing.T) {
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
endpoint := fmt.Sprintf("/classic/presignin?email=%s", tc.email)
@ -106,9 +105,9 @@ func TestClassicPresignin_MissingParams(t *testing.T) {
defer testutils.ClearData()
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
req := testutils.MakeReq(server, "GET", "/classic/presignin", "")
@ -129,9 +128,9 @@ func TestClassicSignin(t *testing.T) {
testutils.MustExec(t, db.Save(&alice), "saving alice")
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := fmt.Sprintf(`{"email": "%s", "auth_key": "%s"}`, "alice@example.com", "/XCYisXJ6/o+vf6NUEtmrdYzJYPz+T9oAUCtMpOjhzc=")
@ -227,9 +226,9 @@ func TestClassicSignin_Failure(t *testing.T) {
t.Run(fmt.Sprintf("signin %s %s", tc.email, tc.authKey), func(t *testing.T) {
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := fmt.Sprintf(`{"email": "%s", "auth_key": "%s"}`, tc.email, tc.authKey)

View file

@ -20,7 +20,6 @@ package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/dnote/dnote/pkg/assert"
@ -32,9 +31,9 @@ func TestCheckHealth(t *testing.T) {
defer testutils.ClearData()
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
// Execute

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
@ -44,9 +43,9 @@ func TestGetNotes(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -187,9 +186,9 @@ func TestGetNote(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
@ -43,9 +42,9 @@ func TestGetRepetitionRule(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -117,9 +116,9 @@ func TestGetRepetitionRules(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -225,9 +224,10 @@ func TestCreateRepetitionRules(t *testing.T) {
c := clock.NewMock()
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: c,
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -282,9 +282,10 @@ func TestCreateRepetitionRules(t *testing.T) {
c := clock.NewMock()
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: c,
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -345,9 +346,9 @@ func TestUpdateRepetitionRules(t *testing.T) {
c := clock.NewMock()
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: c,
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -419,9 +420,9 @@ func TestDeleteRepetitionRules(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -544,9 +545,9 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -588,9 +589,9 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
}
testutils.MustExec(t, db.Save(&b1), "preparing book1")
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
// Execute
@ -627,9 +628,9 @@ func TestCreateRepetitionRules_BadRequest(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()

View file

@ -351,20 +351,37 @@ func applyMiddleware(h http.HandlerFunc, rateLimit bool) http.Handler {
type App struct {
Clock clock.Clock
StripeAPIBackend stripe.Backend
WebURL string
}
func (a *App) validate() error {
if a.WebURL == "" {
return errors.New("WebURL is empty")
}
return nil
}
// init sets up the application based on the configuration
func (a *App) init() {
func (a *App) init() error {
if err := a.validate(); err != nil {
return errors.Wrap(err, "validating the app parameters")
}
stripe.Key = os.Getenv("StripeSecretKey")
if a.StripeAPIBackend != nil {
stripe.SetBackend(stripe.APIBackend, a.StripeAPIBackend)
}
return nil
}
// NewRouter creates and returns a new router
func NewRouter(app *App) *mux.Router {
app.init()
func NewRouter(app *App) (*mux.Router, error) {
if err := app.init(); err != nil {
return nil, errors.Wrap(err, "initializing app")
}
proOnly := authMiddlewareParams{ProOnly: true}
@ -436,5 +453,5 @@ func NewRouter(app *App) *mux.Router {
Handler(applyMiddleware(handler, route.RateLimit))
}
return router
return router, nil
}

View file

@ -681,9 +681,9 @@ func TestNotSupportedVersions(t *testing.T) {
}
// setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
for _, tc := range testCases {

View file

@ -0,0 +1,42 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package handlers
import (
"net/http/httptest"
"os"
"testing"
"github.com/pkg/errors"
)
// mustNewServer is a test utility function to initialize a new server
// with the given app paratmers
func mustNewServer(t *testing.T, app *App) *httptest.Server {
app.WebURL = os.Getenv("WebURL")
r, err := NewRouter(app)
if err != nil {
t.Fatal(errors.Wrap(err, "initializing server"))
}
server := httptest.NewServer(r)
return server
}

View file

@ -188,12 +188,10 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
}
subject := "Verify your email"
data := struct {
Subject string
Token string
}{
subject,
tokenValue,
data := mailer.EmailVerificationTmplData{
Subject: subject,
Token: tokenValue,
WebURL: a.WebURL,
}
email := mailer.NewEmail("noreply@getdnote.com", []string{account.Email.String}, subject)
if err := email.ParseTemplate(mailer.EmailTypeEmailVerification, data); err != nil {

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
@ -47,9 +46,9 @@ func TestUpdatePassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -75,9 +74,9 @@ func TestUpdatePassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -101,9 +100,9 @@ func TestUpdatePassword(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -135,9 +134,9 @@ func TestCreateVerificationToken(t *testing.T) {
templatePath := fmt.Sprintf("%s/mailer/templates/src", testutils.ServerPath)
mailer.InitTemplates(&templatePath)
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -168,9 +167,9 @@ func TestCreateVerificationToken(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -201,9 +200,9 @@ func TestVerifyEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -242,9 +241,9 @@ func TestVerifyEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -286,9 +285,9 @@ func TestVerifyEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -328,9 +327,9 @@ func TestVerifyEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -373,9 +372,9 @@ func TestUpdateEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -407,9 +406,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -433,9 +432,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -473,9 +472,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -507,9 +506,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -543,9 +542,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -579,9 +578,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -605,9 +604,9 @@ func TestUpdateEmailPreference(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -641,9 +640,9 @@ func TestGetEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
@ -91,9 +90,9 @@ func TestRegister(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := fmt.Sprintf(`{"email": "%s", "password": "%s"}`, tc.email, tc.password)
@ -134,9 +133,9 @@ func TestRegisterMissingParams(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := fmt.Sprintf(`{"password": %s}`, "SLMZFM5RmSjA5vfXnG5lPOnrpZSbtmV76cnAcrlr2yU")
@ -161,9 +160,9 @@ func TestRegisterMissingParams(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := fmt.Sprintf(`{"email": "%s"}`, "alice@example.com")
@ -189,9 +188,9 @@ func TestRegisterDuplicateEmail(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -226,9 +225,9 @@ func TestSignIn(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -256,9 +255,9 @@ func TestSignIn(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -287,9 +286,9 @@ func TestSignIn(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
u := testutils.SetupUserData()
@ -318,9 +317,9 @@ func TestSignIn(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
dat := `{"email": "nonexistent@example.com", "password": "pass1234"}`
@ -361,9 +360,9 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, db.Save(&session2), "preparing session2")
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
// Execute
@ -412,9 +411,9 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, db.Save(&session2), "preparing session2")
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
// Execute

View file

@ -22,7 +22,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
@ -43,9 +42,9 @@ func TestGetBooks(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -122,9 +121,9 @@ func TestGetBooksByName(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -207,9 +206,9 @@ func TestDeleteBook(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -357,9 +356,9 @@ func TestCreateBook(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -418,9 +417,9 @@ func TestCreateBookDuplicate(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -497,9 +496,9 @@ func TestUpdateBook(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()

View file

@ -21,7 +21,6 @@ package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/dnote/dnote/pkg/assert"
@ -39,9 +38,9 @@ func TestCreateNote(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -172,9 +171,9 @@ func TestUpdateNote(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()
@ -265,9 +264,9 @@ func TestDeleteNote(t *testing.T) {
db := database.DBConn
// Setup
server := httptest.NewServer(NewRouter(&App{
server := mustNewServer(t, &App{
Clock: clock.NewMock(),
}))
})
defer server.Close()
user := testutils.SetupUserData()

View file

@ -20,6 +20,7 @@ package job
import (
"log"
"os"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/job/repetition"
@ -36,10 +37,15 @@ func scheduleJob(c *cron.Cron, spec string, cmd func()) {
c.Schedule(s, cron.FuncJob(cmd))
}
// Run starts the background tasks and blocks forever.
func Run() {
log.Println("Started background tasks")
func checkEnvironment() error {
if os.Getenv("WebURL") == "" {
return errors.New("WebURL is empty")
}
return nil
}
func schedule(ch chan error) {
cl := clock.New()
// Schedule jobs
@ -47,6 +53,25 @@ func Run() {
scheduleJob(c, "* * * * *", func() { repetition.Do(cl) })
c.Start()
ch <- nil
// Block forever
select {}
}
// Run starts the background tasks in a separate goroutine that runs forever
func Run() error {
if err := checkEnvironment(); err != nil {
return errors.Wrap(err, "checking environment variables")
}
ch := make(chan error)
go schedule(ch)
if err := <-ch; err != nil {
return errors.Wrap(err, "scheduling jobs")
}
log.Println("Started background tasks")
return nil
}

View file

@ -20,6 +20,7 @@ package repetition
import (
"fmt"
"os"
"time"
"github.com/dnote/dnote/pkg/clock"
@ -74,6 +75,7 @@ func BuildEmail(now time.Time, user database.User, emailAddr string, digest data
EmailSessionToken: tok.Value,
RuleUUID: rule.UUID,
RuleTitle: rule.Title,
WebURL: os.Getenv("WebURL"),
}
email := mailer.NewEmail("noreply@getdnote.com", []string{emailAddr}, subject)

View file

@ -161,12 +161,12 @@ func (e *Email) ParseTemplate(templateName string, data interface{}) error {
buf := new(bytes.Buffer)
if err := t.Execute(buf, data); err != nil {
return err
return errors.Wrap(err, "executing the template")
}
html, err := inliner.Inline(buf.String())
if err != nil {
return err
return errors.Wrap(err, "inlining the css rules")
}
e.Body = html

View file

@ -0,0 +1,113 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package mailer
import (
"fmt"
"strings"
"testing"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
)
func init() {
testutils.InitTestDB()
templatePath := fmt.Sprintf("%s/mailer/templates/src", testutils.ServerPath)
InitTemplates(&templatePath)
}
func TestEmailVerificationEmail(t *testing.T) {
testCases := []struct {
token string
webURL string
}{
{
token: "someRandomToken1",
webURL: "http://localhost:3000",
},
{
token: "someRandomToken2",
webURL: "http://localhost:3001",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("with WebURL %s", tc.webURL), func(t *testing.T) {
m := NewEmail("alice@example.com", []string{"bob@example.com"}, "Test email")
dat := EmailVerificationTmplData{
Subject: "Test email verification email",
Token: tc.token,
WebURL: tc.webURL,
}
err := m.ParseTemplate(EmailTypeEmailVerification, dat)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
if ok := strings.Contains(m.Body, tc.webURL); !ok {
t.Errorf("email body did not contain %s", tc.webURL)
}
if ok := strings.Contains(m.Body, tc.token); !ok {
t.Errorf("email body did not contain %s", tc.token)
}
})
}
}
func TestResetPasswordEmail(t *testing.T) {
testCases := []struct {
token string
webURL string
}{
{
token: "someRandomToken1",
webURL: "http://localhost:3000",
},
{
token: "someRandomToken2",
webURL: "http://localhost:3001",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("with WebURL %s", tc.webURL), func(t *testing.T) {
m := NewEmail("alice@example.com", []string{"bob@example.com"}, "Test email")
dat := EmailVerificationTmplData{
Subject: "Test reset passowrd email",
Token: tc.token,
WebURL: tc.webURL,
}
err := m.ParseTemplate(EmailTypeResetPassword, dat)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
if ok := strings.Contains(m.Body, tc.webURL); !ok {
t.Errorf("email body did not contain %s", tc.webURL)
}
if ok := strings.Contains(m.Body, tc.token); !ok {
t.Errorf("email body did not contain %s", tc.token)
}
})
}
}

View file

@ -392,7 +392,7 @@
</tr>
<tr>
<td class="note-meta">
<a target="_blank" rel="noreferrer noopener" href="https://app.getdnote.com/notes/{{ .UUID }}"><span class="meta-label">Open</span></a>
<a target="_blank" rel="noreferrer noopener" href="{{ $.WebURL }}/notes/{{ .UUID }}"><span class="meta-label">Open</span></a>
</td>
</tr>
<tr>
@ -413,9 +413,7 @@
</td>
</tr>
</table>
</td>
</tr>
@ -436,7 +434,7 @@
</tr>
<tr>
<td>
<a href="https://app.getdnote.com/preferences/repetitions/{{ .RuleUUID }}?token={{ .EmailSessionToken }}">Change digest settings</a>
<a href="{{ .WebURL }}/preferences/repetitions/{{ .RuleUUID }}?token={{ .EmailSessionToken }}">Change digest settings</a>
</td>
</tr>
</table>

View file

@ -318,7 +318,7 @@
<tbody>
<tr>
<td>
<a href="https://app.getdnote.com/verify-email/{{ .Token }}" target="_blank">Verify email</a>
<a href="{{ .WebURL }}/{{ .Token }}" target="_blank">Verify email</a>
</td>
</tr>
</tbody>
@ -332,7 +332,7 @@
<tr>
<td class="content-block">
Alternatively you can manually go to the following URL: <a href="https://app.getdnote.com/verify-email/{{ .Token }}">https://app.getdnote.com/verify-email/{{ .Token }}</a>
Alternatively you can manually go to the following URL: <a href="{{ .WebURL }}/{{ .Token }}">{{ .WebURL }}/verify-email/{{ .Token }}</a>
</td>
</tr>

View file

@ -319,7 +319,7 @@
<tbody>
<tr>
<td>
<a href="https://app.getdnote.com/password-reset/{{ .Token }}" target="_blank">Reset Password</a>
<a href="{{ .WebURL }}/password-reset/{{ .Token }}" target="_blank">Reset Password</a>
</td>
</tr>
</tbody>

View file

@ -34,17 +34,6 @@ type DigestNoteInfo struct {
Stage int
}
// DigestTmplData is a template data for digest emails
type DigestTmplData struct {
Subject string
NoteInfo []DigestNoteInfo
ActiveBookCount int
ActiveNoteCount int
EmailSessionToken string
RuleUUID string
RuleTitle string
}
// NewNoteInfo returns a new NoteInfo
func NewNoteInfo(note database.Note, stage int) DigestNoteInfo {
tm := time.Unix(0, int64(note.AddedOn))
@ -57,3 +46,29 @@ func NewNoteInfo(note database.Note, stage int) DigestNoteInfo {
Stage: stage,
}
}
// DigestTmplData is a template data for digest emails
type DigestTmplData struct {
Subject string
NoteInfo []DigestNoteInfo
ActiveBookCount int
ActiveNoteCount int
EmailSessionToken string
RuleUUID string
RuleTitle string
WebURL string
}
// EmailVerificationTmplData is a template data for email verification emails
type EmailVerificationTmplData struct {
Subject string
Token string
WebURL string
}
// EmailResetPasswordTmplData is a template data for reset password emails
type EmailResetPasswordTmplData struct {
Subject string
Token string
WebURL string
}

View file

@ -38,7 +38,6 @@ import (
var versionTag = "master"
var port = flag.String("port", "3000", "port to connect to")
var rootBox *packr.Box
func init() {
@ -89,13 +88,17 @@ func getSWHandler() http.HandlerFunc {
}
}
func initServer() *mux.Router {
func initServer() (*mux.Router, error) {
srv := mux.NewRouter()
apiRouter := handlers.NewRouter(&handlers.App{
apiRouter, err := handlers.NewRouter(&handlers.App{
Clock: clock.New(),
StripeAPIBackend: nil,
WebURL: os.Getenv("WebURL"),
})
if err != nil {
return nil, errors.Wrap(err, "initializing router")
}
srv.PathPrefix("/api").Handler(http.StripPrefix("/api", apiRouter))
srv.PathPrefix("/static").Handler(getStaticHandler())
@ -105,49 +108,45 @@ func initServer() *mux.Router {
// For all other requests, serve the index.html file
srv.PathPrefix("/").Handler(getRootHandler())
return srv
return srv, nil
}
func startCmd() {
c := database.Config{
mailer.InitTemplates(nil)
database.Open(database.Config{
Host: os.Getenv("DBHost"),
Port: os.Getenv("DBPort"),
Name: os.Getenv("DBName"),
User: os.Getenv("DBUser"),
Password: os.Getenv("DBPassword"),
}
database.Open(c)
})
database.InitSchema()
defer database.Close()
mailer.InitTemplates(nil)
// Perform database migration
if err := database.Migrate(); err != nil {
panic(errors.Wrap(err, "running migrations"))
}
if err := job.Run(); err != nil {
panic(errors.Wrap(err, "running job"))
}
// Run job in the background
go job.Run()
srv := initServer()
srv, err := initServer()
if err != nil {
panic(errors.Wrap(err, "initializing server"))
}
log.Printf("Dnote version %s is running on port %s", versionTag, *port)
addr := fmt.Sprintf(":%s", *port)
log.Println(http.ListenAndServe(addr, srv))
http.ListenAndServe(addr, srv)
}
func versionCmd() {
fmt.Printf("dnote-server-%s\n", versionTag)
}
func main() {
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "":
fmt.Printf(`Dnote Server - A simple notebook for developers
func rootCmd() {
fmt.Printf(`Dnote Server - A simple notebook for developers
Usage:
dnote-server [command]
@ -156,6 +155,15 @@ Available commands:
start: Start the server
version: Print the version
`)
}
func main() {
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "":
rootCmd()
case "start":
startCmd()
case "version":