mirror of
https://github.com/dnote/dnote
synced 2026-03-17 16:00:08 +01:00
Allow to send email verification email
This commit is contained in:
parent
9c1c812ea1
commit
b6572a5d13
14 changed files with 35 additions and 485 deletions
|
|
@ -75,7 +75,7 @@
|
|||
.setting-row-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
// align-items: flex-start;
|
||||
|
||||
@include breakpoint(md) {
|
||||
flex-direction: row;
|
||||
|
|
@ -109,11 +109,13 @@
|
|||
|
||||
.setting-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-break: break-all;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include breakpoint(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,4 +138,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-verification-form {
|
||||
margin-left: rem(12px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -673,37 +673,37 @@ func (u *Users) CreateEmailVerificationToken(w http.ResponseWriter, r *http.Requ
|
|||
|
||||
user := context.User(r.Context())
|
||||
if user == nil {
|
||||
handleHTMLError(w, r, app.ErrLoginRequired, "No authenticated user found", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, app.ErrLoginRequired, "No authenticated user found", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
err := u.app.DB.Where("user_id = ?", user.ID).First(&account).Error
|
||||
if err != nil {
|
||||
handleHTMLError(w, r, err, "finding account", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, err, "finding account", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
if account.EmailVerified {
|
||||
handleHTMLError(w, r, app.ErrEmailAlreadyVerified, "email is already verified.", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, app.ErrEmailAlreadyVerified, "email is already verified.", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
if account.Email.String == "" {
|
||||
handleHTMLError(w, r, app.ErrEmailRequired, "email is empty.", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, app.ErrEmailRequired, "email is empty.", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
tok, err := token.Create(u.app.DB, account.UserID, database.TokenTypeEmailVerification)
|
||||
if err != nil {
|
||||
handleHTMLError(w, r, err, "saving token", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, err, "saving token", u.SettingView, vd)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.app.SendVerificationEmail(account.Email.String, tok.Value); err != nil {
|
||||
if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
|
||||
handleHTMLError(w, r, app.ErrInvalidSMTPConfig, "SMTP config is not configured correctly.", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, app.ErrInvalidSMTPConfig, "SMTP config is not configured correctly.", u.SettingView, vd)
|
||||
} else {
|
||||
handleHTMLError(w, r, err, "sending verification email", u.EmailVerificationView, vd)
|
||||
handleHTMLError(w, r, err, "sending verification email", u.SettingView, vd)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import (
|
|||
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/job/remind"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -102,7 +100,6 @@ func scheduleJob(c *cron.Cron, spec string, cmd func()) {
|
|||
func (r *Runner) schedule(ch chan error) {
|
||||
// Schedule jobs
|
||||
cr := cron.New()
|
||||
scheduleJob(cr, "0 8 * * *", func() { r.RemindNoRecentNotes() })
|
||||
cr.Start()
|
||||
|
||||
ch <- nil
|
||||
|
|
@ -128,26 +125,3 @@ func (r *Runner) Do() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemindNoRecentNotes remind users if no notes have been added recently
|
||||
func (r *Runner) RemindNoRecentNotes() {
|
||||
c := remind.Context{
|
||||
DB: r.DB,
|
||||
Clock: r.Clock,
|
||||
EmailTmpl: r.EmailTmpl,
|
||||
EmailBackend: r.EmailBackend,
|
||||
Config: r.Config,
|
||||
}
|
||||
|
||||
result, err := remind.DoInactive(c)
|
||||
m := log.WithFields(log.Fields{
|
||||
"success_count": result.SuccessCount,
|
||||
"failed_user_ids": result.FailedUserIDs,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
m.Info("successfully processed no recent note reminder job")
|
||||
} else {
|
||||
m.ErrorWrap(err, "error processing no recent note reminder job")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 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 remind
|
||||
|
||||
import (
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/app"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Context holds data that repetition job needs in order to perform
|
||||
type Context struct {
|
||||
DB *gorm.DB
|
||||
Clock clock.Clock
|
||||
EmailTmpl mailer.Templates
|
||||
EmailBackend mailer.Backend
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
type inactiveUserInfo struct {
|
||||
userID int
|
||||
email string
|
||||
sampleNoteUUID string
|
||||
}
|
||||
|
||||
func (c *Context) sampleUserNote(userID int) (database.Note, error) {
|
||||
var ret database.Note
|
||||
// FIXME: ordering by random() requires a sequential scan on the whole table and does not scale
|
||||
if err := c.DB.Where("user_id = ?", userID).Order("random() DESC").First(&ret).Error; err != nil {
|
||||
return ret, errors.Wrap(err, "getting a random note")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (c *Context) getInactiveUserInfo() ([]inactiveUserInfo, error) {
|
||||
ret := []inactiveUserInfo{}
|
||||
|
||||
threshold := c.Clock.Now().AddDate(0, 0, -14).Unix()
|
||||
|
||||
rows, err := c.DB.Raw(`
|
||||
SELECT
|
||||
notes.user_id AS user_id,
|
||||
accounts.email,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN notes.created_at > to_timestamp(?) THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) AS recent_note_count,
|
||||
COUNT(*) AS total_note_count
|
||||
FROM notes
|
||||
INNER JOIN accounts ON accounts.user_id = notes.user_id
|
||||
WHERE accounts.email IS NOT NULL AND accounts.email_verified IS TRUE
|
||||
GROUP BY notes.user_id, accounts.email`, threshold).Rows()
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "executing note count SQL query")
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var userID, recentNoteCount, totalNoteCount int
|
||||
var email string
|
||||
if err := rows.Scan(&userID, &email, &recentNoteCount, &totalNoteCount); err != nil {
|
||||
return nil, errors.Wrap(err, "scanning a row")
|
||||
}
|
||||
|
||||
if recentNoteCount == 0 && totalNoteCount > 0 {
|
||||
note, err := c.sampleUserNote(userID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sampling user note")
|
||||
}
|
||||
|
||||
ret = append(ret, inactiveUserInfo{
|
||||
userID: userID,
|
||||
email: email,
|
||||
sampleNoteUUID: note.UUID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (c *Context) canNotify(info inactiveUserInfo) (bool, error) {
|
||||
var pref database.EmailPreference
|
||||
if err := c.DB.Where("user_id = ?", info.userID).First(&pref).Error; err != nil {
|
||||
return false, errors.Wrap(err, "getting email preference")
|
||||
}
|
||||
|
||||
if !pref.InactiveReminder {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var notif database.Notification
|
||||
conn := c.DB.Where("user_id = ? AND type = ?", info.userID, mailer.EmailTypeInactiveReminder).Order("created_at DESC").First(¬if)
|
||||
|
||||
if conn.RecordNotFound() {
|
||||
return true, nil
|
||||
} else if err := conn.Error; err != nil {
|
||||
return false, errors.Wrap(err, "checking cooldown")
|
||||
}
|
||||
|
||||
t := c.Clock.Now().AddDate(0, 0, -14)
|
||||
if notif.CreatedAt.Before(t) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Context) process(info inactiveUserInfo) error {
|
||||
ok, err := c.canNotify(info)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "checking if user can be notified")
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
sender, err := app.GetSenderEmail(c.Config, "noreply@getdnote.com")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting sender email")
|
||||
}
|
||||
|
||||
tok, err := mailer.GetToken(c.DB, info.userID, database.TokenTypeEmailPreference)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting email token")
|
||||
}
|
||||
|
||||
tmplData := mailer.InactiveReminderTmplData{
|
||||
WebURL: c.Config.WebURL,
|
||||
SampleNoteUUID: info.sampleNoteUUID,
|
||||
Token: tok.Value,
|
||||
}
|
||||
body, err := c.EmailTmpl.Execute(mailer.EmailTypeInactiveReminder, mailer.EmailKindText, tmplData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "executing inactive email template")
|
||||
}
|
||||
|
||||
if err := c.EmailBackend.Queue("Your Dnote stopped growing", sender, []string{info.email}, mailer.EmailKindText, body); err != nil {
|
||||
return errors.Wrap(err, "queueing email")
|
||||
}
|
||||
|
||||
if err := c.DB.Create(&database.Notification{
|
||||
Type: mailer.EmailTypeInactiveReminder,
|
||||
UserID: info.userID,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(err, "creating notification")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Result holds the result of the job
|
||||
type Result struct {
|
||||
SuccessCount int
|
||||
FailedUserIDs []int
|
||||
}
|
||||
|
||||
// DoInactive sends reminder for users with no recent notes
|
||||
func DoInactive(c Context) (Result, error) {
|
||||
log.Info("performing reminder for no recent notes")
|
||||
|
||||
result := Result{}
|
||||
items, err := c.getInactiveUserInfo()
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "getting inactive user information")
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"user_count": len(items),
|
||||
}).Info("counted inactive users")
|
||||
|
||||
for _, item := range items {
|
||||
err := c.process(item)
|
||||
|
||||
if err == nil {
|
||||
result.SuccessCount = result.SuccessCount + 1
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"user_id": item.userID,
|
||||
}).ErrorWrap(err, "Could not process no recent notes reminder")
|
||||
|
||||
result.FailedUserIDs = append(result.FailedUserIDs, item.userID)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 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 remind
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func getTestContext(c clock.Clock, be *testutils.MockEmailbackendImplementation) Context {
|
||||
emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR")
|
||||
|
||||
con := Context{
|
||||
DB: testutils.DB,
|
||||
Clock: c,
|
||||
EmailTmpl: mailer.NewTemplates(&emailTmplDir),
|
||||
EmailBackend: be,
|
||||
}
|
||||
|
||||
return con
|
||||
}
|
||||
|
||||
func TestDoInactive(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
t1 := time.Now()
|
||||
|
||||
// u1 is an active user
|
||||
u1 := testutils.SetupUserData()
|
||||
a1 := testutils.SetupAccountData(u1, "alice@example.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&a1).Update("email_verified", true), "setting email verified")
|
||||
testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u1.ID, InactiveReminder: true}), "preparing email preference")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: u1.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
n1 := database.Note{
|
||||
BookUUID: b1.UUID,
|
||||
UserID: u1.ID,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
|
||||
|
||||
// u2 is an inactive user
|
||||
u2 := testutils.SetupUserData()
|
||||
a2 := testutils.SetupAccountData(u2, "bob@example.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&a2).Update("email_verified", true), "setting email verified")
|
||||
testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u2.ID, InactiveReminder: true}), "preparing email preference")
|
||||
|
||||
b2 := database.Book{
|
||||
UserID: u2.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
n2 := database.Note{
|
||||
UserID: u2.ID,
|
||||
BookUUID: b2.UUID,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2")
|
||||
testutils.MustExec(t, testutils.DB.Model(&n2).Update("created_at", t1.AddDate(0, 0, -15)), "preparing n2")
|
||||
|
||||
// u3 is an inactive user with inactive alert email preference disabled
|
||||
u3 := testutils.SetupUserData()
|
||||
a3 := testutils.SetupAccountData(u3, "alice@example.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&a3).Update("email_verified", true), "setting email verified")
|
||||
emailPref3 := database.EmailPreference{UserID: u3.ID}
|
||||
testutils.MustExec(t, testutils.DB.Save(&emailPref3), "preparing email preference")
|
||||
testutils.MustExec(t, testutils.DB.Model(&emailPref3).Update(map[string]interface{}{"inactive_reminder": false}), "updating email preference")
|
||||
|
||||
b3 := database.Book{
|
||||
UserID: u3.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
n3 := database.Note{
|
||||
BookUUID: b3.UUID,
|
||||
UserID: u3.ID,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3")
|
||||
testutils.MustExec(t, testutils.DB.Model(&n3).Update("created_at", t1.AddDate(0, 0, -15)), "preparing n3")
|
||||
|
||||
c := clock.NewMock()
|
||||
c.SetNow(t1)
|
||||
be := &testutils.MockEmailbackendImplementation{}
|
||||
|
||||
con := getTestContext(c, be)
|
||||
if _, err := DoInactive(con); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "performing"))
|
||||
}
|
||||
|
||||
assert.Equalf(t, len(be.Emails), 1, "email queue count mismatch")
|
||||
assert.DeepEqual(t, be.Emails[0].To, []string{a2.Email.String}, "email address mismatch")
|
||||
}
|
||||
|
||||
func TestDoInactive_Cooldown(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// setup sets up an inactive user
|
||||
setup := func(t *testing.T, now time.Time, email string) database.User {
|
||||
u := testutils.SetupUserData()
|
||||
a := testutils.SetupAccountData(u, email, "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&a).Update("email_verified", true), "setting email verified")
|
||||
testutils.MustExec(t, testutils.DB.Save(&database.EmailPreference{UserID: u.ID, InactiveReminder: true}), "preparing email preference")
|
||||
|
||||
b := database.Book{
|
||||
UserID: u.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b), "preparing book")
|
||||
n := database.Note{
|
||||
UserID: u.ID,
|
||||
BookUUID: b.UUID,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n), "preparing note")
|
||||
testutils.MustExec(t, testutils.DB.Model(&n).Update("created_at", now.AddDate(0, 0, -15)), "preparing note")
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// Set up
|
||||
now := time.Now()
|
||||
|
||||
setup(t, now, "alice@example.com")
|
||||
|
||||
bob := setup(t, now, "bob@example.com")
|
||||
bobNotif := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: bob.ID}
|
||||
testutils.MustExec(t, testutils.DB.Create(&bobNotif), "preparing inactive notification for bob")
|
||||
testutils.MustExec(t, testutils.DB.Model(&bobNotif).Update("created_at", now.AddDate(0, 0, -7)), "preparing created_at for inactive notification for bob")
|
||||
|
||||
chuck := setup(t, now, "chuck@example.com")
|
||||
chuckNotif := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: chuck.ID}
|
||||
testutils.MustExec(t, testutils.DB.Create(&chuckNotif), "preparing inactive notification for chuck")
|
||||
testutils.MustExec(t, testutils.DB.Model(&chuckNotif).Update("created_at", now.AddDate(0, 0, -15)), "preparing created_at for inactive notification for chuck")
|
||||
|
||||
dan := setup(t, now, "dan@example.com")
|
||||
danNotif1 := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: dan.ID}
|
||||
testutils.MustExec(t, testutils.DB.Create(&danNotif1), "preparing inactive notification 1 for dan")
|
||||
testutils.MustExec(t, testutils.DB.Model(&danNotif1).Update("created_at", now.AddDate(0, 0, -10)), "preparing created_at for inactive notification for dan")
|
||||
danNotif2 := database.Notification{Type: mailer.EmailTypeInactiveReminder, UserID: dan.ID}
|
||||
testutils.MustExec(t, testutils.DB.Create(&danNotif2), "preparing inactive notification 2 for dan")
|
||||
testutils.MustExec(t, testutils.DB.Model(&danNotif2).Update("created_at", now.AddDate(0, 0, -15)), "preparing created_at for inactive notification for dan")
|
||||
|
||||
c := clock.NewMock()
|
||||
c.SetNow(now)
|
||||
be := &testutils.MockEmailbackendImplementation{}
|
||||
|
||||
// Execute
|
||||
con := getTestContext(c, be)
|
||||
if _, err := DoInactive(con); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "performing"))
|
||||
}
|
||||
|
||||
// Test
|
||||
assert.Equalf(t, len(be.Emails), 2, "email queue count mismatch")
|
||||
|
||||
var recipients []string
|
||||
for _, email := range be.Emails {
|
||||
recipients = append(recipients, email.To[0])
|
||||
}
|
||||
sort.SliceStable(recipients, func(i, j int) bool {
|
||||
r1 := recipients[i]
|
||||
r2 := recipients[j]
|
||||
|
||||
return r1 < r2
|
||||
})
|
||||
|
||||
assert.DeepEqual(t, recipients, []string{"alice@example.com", "chuck@example.com"}, "email address mismatch")
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 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 remind
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutils.InitTestDB()
|
||||
|
||||
code := m.Run()
|
||||
testutils.ClearData(testutils.DB)
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
|
@ -6,4 +6,4 @@ Please click on the following link, or paste this into your browser to complete
|
|||
|
||||
You can reply to this message, if you have questions.
|
||||
|
||||
- Sung (Maker of Dnote)
|
||||
- Dnote team
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ If you did not initiate this password change, please notify us by replying, and
|
|||
|
||||
Thanks.
|
||||
|
||||
- Sung (Maker of Dnote)
|
||||
- Dnote team
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ Hi, thanks for signing up for Dnote Pro.
|
|||
Now you can take your notes with you wherever you go!
|
||||
|
||||
* Synchronize data among an unlimited number of machines.
|
||||
* Access notes anywhere via the web interface.
|
||||
* Manage notes via REST API.
|
||||
|
||||
Your account is "{{ .AccountEmail }}". Log in at {{ .WebURL }}/login
|
||||
|
||||
Thank you for using Dnote. Your support makes it possible to develop it for developers around the world.
|
||||
|
||||
- Sung (Maker of Dnote)
|
||||
- Dnote team
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
Hi.
|
||||
|
||||
Welcome to Dnote! To verify your email so that you can automate spaced reptition, visit the following link:
|
||||
Welcome to Dnote! To verify your email, visit the following link:
|
||||
|
||||
{{ .WebURL }}/verify-email/{{ .Token }}
|
||||
{{ .WebURL }}/verify-email?token={{ .Token }}
|
||||
|
||||
Thanks for using my software.
|
||||
Thanks for using Dnote.
|
||||
|
||||
- Sung (Maker of Dnote)
|
||||
- Dnote team
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ Dnote is open source and you can see the source code at https://github.com/dnote
|
|||
|
||||
Feel free to reply anytime. Thanks for using Dnote.
|
||||
|
||||
- Sung (Maker of Dnote)
|
||||
- Dnote team
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -40,6 +40,15 @@
|
|||
Yes
|
||||
{{else}}
|
||||
No
|
||||
|
||||
<form action="/verification-token" method="POST" class="email-verification-form">
|
||||
<button
|
||||
class="button button-second button-small"
|
||||
type="submit"
|
||||
>
|
||||
Send verification email
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue