From 5d6ad342f35ed343973eaed6ac22af89cec5aace Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Tue, 29 Oct 2019 20:21:08 -0700 Subject: [PATCH] Customize app URLs in the emails (#290) * Allow to customize app url in emails * Validate env var * Test * Add license * Add guide --- CHANGELOG.md | 11 +- SELF_HOSTING.md | 14 ++- pkg/server/.env.dev | 2 + pkg/server/.env.test | 2 + pkg/server/api/handlers/auth.go | 10 +- pkg/server/api/handlers/auth_test.go | 33 +++-- pkg/server/api/handlers/classic_test.go | 17 ++- pkg/server/api/handlers/health_test.go | 5 +- pkg/server/api/handlers/notes_test.go | 9 +- .../api/handlers/repetition_rules_test.go | 39 +++--- pkg/server/api/handlers/routes.go | 25 +++- pkg/server/api/handlers/routes_test.go | 4 +- pkg/server/api/handlers/testutils.go | 42 +++++++ pkg/server/api/handlers/user.go | 10 +- pkg/server/api/handlers/user_test.go | 73 ++++++----- pkg/server/api/handlers/v3_auth_test.go | 41 ++++--- pkg/server/api/handlers/v3_books_test.go | 25 ++-- pkg/server/api/handlers/v3_notes_test.go | 13 +- pkg/server/job/job.go | 31 ++++- pkg/server/job/repetition/repetition.go | 2 + pkg/server/mailer/mailer.go | 4 +- pkg/server/mailer/mailer_test.go | 113 ++++++++++++++++++ pkg/server/mailer/templates/src/digest.html | 6 +- .../templates/src/email_verification.html | 4 +- .../mailer/templates/src/reset_password.html | 2 +- pkg/server/mailer/types.go | 37 ++++-- pkg/server/main.go | 52 ++++---- 27 files changed, 428 insertions(+), 198 deletions(-) create mode 100644 pkg/server/api/handlers/testutils.go create mode 100644 pkg/server/mailer/mailer_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e3a24c..68ab3708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index c688b42f..892d2860 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -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'. diff --git a/pkg/server/.env.dev b/pkg/server/.env.dev index 2be2070a..9e9644f0 100644 --- a/pkg/server/.env.dev +++ b/pkg/server/.env.dev @@ -9,3 +9,5 @@ DBPassword= SmtpUsername=mock-SmtpUsername SmtpPassword=mock-SmtpPassword SmtpHost=mock-SmtpHost + +WebURL=http://localhost:3000 diff --git a/pkg/server/.env.test b/pkg/server/.env.test index 17a64fc7..06585243 100644 --- a/pkg/server/.env.test +++ b/pkg/server/.env.test @@ -9,3 +9,5 @@ DBPassword= SmtpUsername=mock-SmtpUsername SmtpPassword=mock-SmtpPassword SmtpHost=mock-SmtpHost + +WebURL=http://localhost:3000 diff --git a/pkg/server/api/handlers/auth.go b/pkg/server/api/handlers/auth.go index eea6f93b..f2397fb2 100644 --- a/pkg/server/api/handlers/auth.go +++ b/pkg/server/api/handlers/auth.go @@ -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 { diff --git a/pkg/server/api/handlers/auth_test.go b/pkg/server/api/handlers/auth_test.go index a5aeb394..cb47f727 100644 --- a/pkg/server/api/handlers/auth_test.go +++ b/pkg/server/api/handlers/auth_test.go @@ -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() diff --git a/pkg/server/api/handlers/classic_test.go b/pkg/server/api/handlers/classic_test.go index 01dec8f4..05b88469 100644 --- a/pkg/server/api/handlers/classic_test.go +++ b/pkg/server/api/handlers/classic_test.go @@ -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) diff --git a/pkg/server/api/handlers/health_test.go b/pkg/server/api/handlers/health_test.go index 61af9e63..c778755c 100644 --- a/pkg/server/api/handlers/health_test.go +++ b/pkg/server/api/handlers/health_test.go @@ -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 diff --git a/pkg/server/api/handlers/notes_test.go b/pkg/server/api/handlers/notes_test.go index d5a27cdc..afed2591 100644 --- a/pkg/server/api/handlers/notes_test.go +++ b/pkg/server/api/handlers/notes_test.go @@ -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() diff --git a/pkg/server/api/handlers/repetition_rules_test.go b/pkg/server/api/handlers/repetition_rules_test.go index 3c3f00ab..665955e2 100644 --- a/pkg/server/api/handlers/repetition_rules_test.go +++ b/pkg/server/api/handlers/repetition_rules_test.go @@ -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() diff --git a/pkg/server/api/handlers/routes.go b/pkg/server/api/handlers/routes.go index a49249b1..d92a9a64 100644 --- a/pkg/server/api/handlers/routes.go +++ b/pkg/server/api/handlers/routes.go @@ -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 } diff --git a/pkg/server/api/handlers/routes_test.go b/pkg/server/api/handlers/routes_test.go index 8d5bb90c..b4ca5f4d 100644 --- a/pkg/server/api/handlers/routes_test.go +++ b/pkg/server/api/handlers/routes_test.go @@ -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 { diff --git a/pkg/server/api/handlers/testutils.go b/pkg/server/api/handlers/testutils.go new file mode 100644 index 00000000..dde58712 --- /dev/null +++ b/pkg/server/api/handlers/testutils.go @@ -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 . + */ + +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 +} diff --git a/pkg/server/api/handlers/user.go b/pkg/server/api/handlers/user.go index 3077205a..9160e1ca 100644 --- a/pkg/server/api/handlers/user.go +++ b/pkg/server/api/handlers/user.go @@ -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 { diff --git a/pkg/server/api/handlers/user_test.go b/pkg/server/api/handlers/user_test.go index 69b5c150..809fea3e 100644 --- a/pkg/server/api/handlers/user_test.go +++ b/pkg/server/api/handlers/user_test.go @@ -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() diff --git a/pkg/server/api/handlers/v3_auth_test.go b/pkg/server/api/handlers/v3_auth_test.go index c2d8ff75..8faa482d 100644 --- a/pkg/server/api/handlers/v3_auth_test.go +++ b/pkg/server/api/handlers/v3_auth_test.go @@ -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 diff --git a/pkg/server/api/handlers/v3_books_test.go b/pkg/server/api/handlers/v3_books_test.go index cd7d4bc9..01184947 100644 --- a/pkg/server/api/handlers/v3_books_test.go +++ b/pkg/server/api/handlers/v3_books_test.go @@ -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() diff --git a/pkg/server/api/handlers/v3_notes_test.go b/pkg/server/api/handlers/v3_notes_test.go index 912b6c14..19e311c4 100644 --- a/pkg/server/api/handlers/v3_notes_test.go +++ b/pkg/server/api/handlers/v3_notes_test.go @@ -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() diff --git a/pkg/server/job/job.go b/pkg/server/job/job.go index 56085e8d..5be2867e 100644 --- a/pkg/server/job/job.go +++ b/pkg/server/job/job.go @@ -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 +} diff --git a/pkg/server/job/repetition/repetition.go b/pkg/server/job/repetition/repetition.go index b0df34e1..8401eb6a 100644 --- a/pkg/server/job/repetition/repetition.go +++ b/pkg/server/job/repetition/repetition.go @@ -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) diff --git a/pkg/server/mailer/mailer.go b/pkg/server/mailer/mailer.go index 0c9187f2..9d4942a8 100644 --- a/pkg/server/mailer/mailer.go +++ b/pkg/server/mailer/mailer.go @@ -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 diff --git a/pkg/server/mailer/mailer_test.go b/pkg/server/mailer/mailer_test.go new file mode 100644 index 00000000..8d596de0 --- /dev/null +++ b/pkg/server/mailer/mailer_test.go @@ -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 . + */ + +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) + } + }) + } +} diff --git a/pkg/server/mailer/templates/src/digest.html b/pkg/server/mailer/templates/src/digest.html index bb7db9d7..f141f5de 100644 --- a/pkg/server/mailer/templates/src/digest.html +++ b/pkg/server/mailer/templates/src/digest.html @@ -392,7 +392,7 @@ - Open + Open @@ -413,9 +413,7 @@ - - @@ -436,7 +434,7 @@ - Change digest settings + Change digest settings diff --git a/pkg/server/mailer/templates/src/email_verification.html b/pkg/server/mailer/templates/src/email_verification.html index c25bd174..ec05d79f 100644 --- a/pkg/server/mailer/templates/src/email_verification.html +++ b/pkg/server/mailer/templates/src/email_verification.html @@ -318,7 +318,7 @@ - Verify email + Verify email @@ -332,7 +332,7 @@ - Alternatively you can manually go to the following URL: https://app.getdnote.com/verify-email/{{ .Token }} + Alternatively you can manually go to the following URL: {{ .WebURL }}/verify-email/{{ .Token }} diff --git a/pkg/server/mailer/templates/src/reset_password.html b/pkg/server/mailer/templates/src/reset_password.html index 7b56893c..682dc27b 100644 --- a/pkg/server/mailer/templates/src/reset_password.html +++ b/pkg/server/mailer/templates/src/reset_password.html @@ -319,7 +319,7 @@ - Reset Password + Reset Password diff --git a/pkg/server/mailer/types.go b/pkg/server/mailer/types.go index 0555aa08..b4604d73 100644 --- a/pkg/server/mailer/types.go +++ b/pkg/server/mailer/types.go @@ -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 +} diff --git a/pkg/server/main.go b/pkg/server/main.go index b870b0ba..8ac5bb65 100644 --- a/pkg/server/main.go +++ b/pkg/server/main.go @@ -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":