Improve email and signup for self-hosting users (#355)

* Add app abstraction

* Abstract out email sending

* Get sender

* Test

* Move operations to app

* Test

* Test

* Add license

* Fix lint
This commit is contained in:
Sung Won Cho 2019-12-14 12:10:48 +07:00 committed by GitHub
commit 891be61031
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1408 additions and 567 deletions

72
pkg/server/app/app.go Normal file
View file

@ -0,0 +1,72 @@
/* 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 app
import (
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
)
var (
// ErrEmptyDB is an error for missing database connection in the app configuration
ErrEmptyDB = errors.New("No database connection was provided")
// ErrEmptyClock is an error for missing clock in the app configuration
ErrEmptyClock = errors.New("No clock was provided")
// ErrEmptyWebURL is an error for missing WebURL content in the app configuration
ErrEmptyWebURL = errors.New("No WebURL was provided")
// ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
// ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
)
// App is an application configuration
type App struct {
DB *gorm.DB
Clock clock.Clock
StripeAPIBackend stripe.Backend
EmailTemplates mailer.Templates
EmailBackend mailer.Backend
WebURL string
OnPremise bool
}
// Validate validates the app configuration
func (a *App) Validate() error {
if a.WebURL == "" {
return ErrEmptyWebURL
}
if a.Clock == nil {
return ErrEmptyClock
}
if a.EmailTemplates == nil {
return ErrEmptyEmailTemplates
}
if a.EmailBackend == nil {
return ErrEmptyEmailBackend
}
if a.DB == nil {
return ErrEmptyDB
}
return nil
}

113
pkg/server/app/app_test.go Normal file
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 app
import (
"fmt"
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
func TestValidate(t *testing.T) {
testCases := []struct {
app App
expectedErr error
}{
{
app: App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
EmailTemplates: mailer.Templates{},
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: nil,
},
{
app: App{
DB: nil,
Clock: clock.NewMock(),
StripeAPIBackend: nil,
EmailTemplates: mailer.Templates{},
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyDB,
},
{
app: App{
DB: &gorm.DB{},
Clock: nil,
StripeAPIBackend: nil,
EmailTemplates: mailer.Templates{},
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyClock,
},
{
app: App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
EmailTemplates: nil,
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyEmailTemplates,
},
{
app: App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
EmailTemplates: mailer.Templates{},
EmailBackend: nil,
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyEmailBackend,
},
{
app: App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
EmailTemplates: mailer.Templates{},
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "",
},
expectedErr: ErrEmptyWebURL,
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
err := tc.app.Validate()
assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
})
}
}

View file

@ -16,10 +16,9 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/jinzhu/gorm"
@ -27,8 +26,8 @@ import (
)
// CreateBook creates a book with the next usn and updates the user's max_usn
func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string) (database.Book, error) {
tx := db.Begin()
func (a *App) CreateBook(user database.User, name string) (database.Book, error) {
tx := a.DB.Begin()
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
@ -45,7 +44,7 @@ func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string)
UUID: uuid,
UserID: user.ID,
Label: name,
AddedOn: clock.Now().UnixNano(),
AddedOn: a.Clock.Now().UnixNano(),
USN: nextUSN,
Encrypted: false,
}
@ -60,7 +59,7 @@ func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string)
}
// DeleteBook marks a book deleted with the next usn and updates the user's max_usn
func DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.Book, error) {
func (a *App) DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.Book, error) {
if user.ID != book.UserID {
return book, errors.New("Not allowed")
}
@ -83,7 +82,7 @@ func DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.B
}
// UpdateBook updaates the book, the usn and the user's max_usn
func UpdateBook(tx *gorm.DB, c clock.Clock, user database.User, book database.Book, label *string) (database.Book, error) {
func (a *App) UpdateBook(tx *gorm.DB, user database.User, book database.Book, label *string) (database.Book, error) {
if user.ID != book.UserID {
return book, errors.New("Not allowed")
}
@ -98,7 +97,7 @@ func UpdateBook(tx *gorm.DB, c clock.Clock, user database.User, book database.Bo
}
book.USN = nextUSN
book.EditedOn = c.Now().UnixNano()
book.EditedOn = a.Clock.Now().UnixNano()
book.Deleted = false
// TODO: remove after all users have been migrated
book.Encrypted = false

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"fmt"
@ -62,9 +62,11 @@ func TestCreateBook(t *testing.T) {
anotherUser := testutils.SetupUserData()
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 55), fmt.Sprintf("preparing user max_usn for test case %d", idx))
c := clock.NewMock()
a := NewTest(&App{
Clock: clock.NewMock(),
})
book, err := CreateBook(testutils.DB, user, c, tc.label)
book, err := a.CreateBook(user, tc.label)
if err != nil {
t.Fatal(errors.Wrap(err, "creating book"))
}
@ -130,7 +132,8 @@ func TestDeleteBook(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&book), fmt.Sprintf("preparing book for test case %d", idx))
tx := testutils.DB.Begin()
ret, err := DeleteBook(tx, user, book)
a := NewTest(nil)
ret, err := a.DeleteBook(tx, user, book)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting book"))
@ -203,14 +206,16 @@ func TestUpdateBook(t *testing.T) {
anotherUser := testutils.SetupUserData()
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 55), fmt.Sprintf("preparing user max_usn for test case %d", idx))
c := clock.NewMock()
b := database.Book{UserID: user.ID, Deleted: false, Label: tc.expectedLabel}
testutils.MustExec(t, testutils.DB.Save(&b), fmt.Sprintf("preparing book for test case %d", idx))
tx := testutils.DB.Begin()
c := clock.NewMock()
a := NewTest(&App{
Clock: c,
})
book, err := UpdateBook(tx, c, user, b, tc.payloadLabel)
tx := testutils.DB.Begin()
book, err := a.UpdateBook(tx, user, b, tc.payloadLabel)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "updating book"))

137
pkg/server/app/email.go Normal file
View file

@ -0,0 +1,137 @@
/* 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 app
import (
"fmt"
"net/url"
"strings"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/pkg/errors"
)
var defaultSender = "sung@getdnote.com"
// getSenderEmail returns the sender email
func (a *App) getSenderEmail(want string) (string, error) {
if !a.OnPremise {
return want, nil
}
addr, err := a.getNoreplySender()
if err != nil {
return "", errors.Wrap(err, "getting sender email address")
}
return addr, nil
}
func getDomainFromURL(rawURL string) (string, error) {
u, err := url.Parse(rawURL)
if err != nil {
return "", errors.Wrap(err, "parsing url")
}
host := u.Hostname()
parts := strings.Split(host, ".")
if len(parts) < 2 {
return host, nil
}
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
return domain, nil
}
func (a *App) getNoreplySender() (string, error) {
domain, err := getDomainFromURL(a.WebURL)
if err != nil {
return "", errors.Wrap(err, "parsing web url")
}
addr := fmt.Sprintf("noreply@%s", domain)
return addr, nil
}
// SendVerificationEmail sends verification email
func (a *App) SendVerificationEmail(email, tokenValue string) error {
body, err := a.EmailTemplates.Execute(mailer.EmailTypeEmailVerification, mailer.EmailKindText, mailer.EmailVerificationTmplData{
Token: tokenValue,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset verification template for %s", email)
}
from, err := a.getSenderEmail(defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Verify your Dnote email address", from, []string{email}, "text/plain", body); err != nil {
return errors.Wrapf(err, "queueing email for %s", email)
}
return nil
}
// SendWelcomeEmail sends welcome email
func (a *App) SendWelcomeEmail(email string) error {
body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
AccountEmail: email,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset verification template for %s", email)
}
from, err := a.getSenderEmail(defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Welcome to Dnote!", from, []string{email}, "text/plain", body); err != nil {
return errors.Wrapf(err, "queueing email for %s", email)
}
return nil
}
// SendPasswordResetEmail sends verification email
func (a *App) SendPasswordResetEmail(email, tokenValue string) error {
body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPassword, mailer.EmailKindText, mailer.EmailResetPasswordTmplData{
AccountEmail: email,
Token: tokenValue,
WebURL: a.WebURL,
})
if err != nil {
return errors.Wrapf(err, "executing reset verification template for %s", email)
}
from, err := a.getSenderEmail(defaultSender)
if err != nil {
return errors.Wrap(err, "getting the sender email")
}
if err := a.EmailBackend.Queue("Reset your password", from, []string{email}, "text/plain", body); err != nil {
return errors.Wrapf(err, "queueing email for %s", email)
}
return nil
}

View file

@ -0,0 +1,132 @@
/* 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 app
import (
"fmt"
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestSendVerificationEmail(t *testing.T) {
testCases := []struct {
onPremise bool
expectedSender string
}{
{
onPremise: false,
expectedSender: "sung@getdnote.com",
},
{
onPremise: true,
expectedSender: "noreply@example.com",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
emailBackend := testutils.MockEmailbackendImplementation{}
a := NewTest(&App{
OnPremise: tc.onPremise,
WebURL: "http://example.com",
EmailBackend: &emailBackend,
})
if err := a.SendVerificationEmail("alice@example.com", "mockTokenValue"); err != nil {
t.Fatal(err, "failed to perform")
}
assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
})
}
}
func TestSendWelcomeEmail(t *testing.T) {
testCases := []struct {
onPremise bool
expectedSender string
}{
{
onPremise: false,
expectedSender: "sung@getdnote.com",
},
{
onPremise: true,
expectedSender: "noreply@example.com",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
emailBackend := testutils.MockEmailbackendImplementation{}
a := NewTest(&App{
OnPremise: tc.onPremise,
WebURL: "http://example.com",
EmailBackend: &emailBackend,
})
if err := a.SendWelcomeEmail("alice@example.com"); err != nil {
t.Fatal(err, "failed to perform")
}
assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
})
}
}
func TestSendPasswordResetEmail(t *testing.T) {
testCases := []struct {
onPremise bool
expectedSender string
}{
{
onPremise: false,
expectedSender: "sung@getdnote.com",
},
{
onPremise: true,
expectedSender: "noreply@example.com",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
emailBackend := testutils.MockEmailbackendImplementation{}
a := NewTest(&App{
OnPremise: tc.onPremise,
WebURL: "http://example.com",
EmailBackend: &emailBackend,
})
if err := a.SendPasswordResetEmail("alice@example.com", "mockTokenValue"); err != nil {
t.Fatal(err, "failed to perform")
}
assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
})
}
}

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"github.com/dnote/dnote/pkg/server/database"

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"fmt"

View file

@ -0,0 +1,35 @@
/* 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 app
import (
"os"
"testing"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestMain(m *testing.M) {
testutils.InitTestDB()
code := m.Run()
testutils.ClearData()
os.Exit(code)
}

View file

@ -16,10 +16,9 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/permissions"
@ -29,8 +28,8 @@ import (
// CreateNote creates a note with the next usn and updates the user's max_usn.
// It returns the created note.
func CreateNote(db *gorm.DB, user database.User, clock clock.Clock, bookUUID, content string, addedOn *int64, editedOn *int64, public bool) (database.Note, error) {
tx := db.Begin()
func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *int64, editedOn *int64, public bool) (database.Note, error) {
tx := a.DB.Begin()
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
@ -40,7 +39,7 @@ func CreateNote(db *gorm.DB, user database.User, clock clock.Clock, bookUUID, co
var noteAddedOn int64
if addedOn == nil {
noteAddedOn = clock.Now().UnixNano()
noteAddedOn = a.Clock.Now().UnixNano()
} else {
noteAddedOn = *addedOn
}
@ -113,7 +112,7 @@ func (r UpdateNoteParams) GetPublic() bool {
}
// UpdateNote creates a note with the next usn and updates the user's max_usn
func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note database.Note, p *UpdateNoteParams) (database.Note, error) {
func (a *App) UpdateNote(tx *gorm.DB, user database.User, note database.Note, p *UpdateNoteParams) (database.Note, error) {
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
return note, errors.Wrap(err, "incrementing user max_usn")
@ -130,7 +129,7 @@ func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note databas
}
note.USN = nextUSN
note.EditedOn = clock.Now().UnixNano()
note.EditedOn = a.Clock.Now().UnixNano()
note.Deleted = false
// TODO: remove after all users are migrated
note.Encrypted = false
@ -143,7 +142,7 @@ func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note databas
}
// DeleteNote marks a note deleted with the next usn and updates the user's max_usn
func DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.Note, error) {
func (a *App) DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.Note, error) {
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
return note, errors.Wrap(err, "incrementing user max_usn")
@ -162,13 +161,13 @@ func DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.N
}
// GetNote retrieves a note for the given user
func GetNote(db *gorm.DB, uuid string, user database.User) (database.Note, bool, error) {
func (a *App) GetNote(uuid string, user database.User) (database.Note, bool, error) {
zeroNote := database.Note{}
if !helpers.ValidateUUID(uuid) {
return zeroNote, false, nil
}
conn := db.Where("notes.uuid = ? AND deleted = ?", uuid, false)
conn := a.DB.Where("notes.uuid = ? AND deleted = ?", uuid, false)
conn = database.PreloadNote(conn)
var note database.Note

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"fmt"
@ -85,8 +85,12 @@ func TestCreateNote(t *testing.T) {
b1 := database.Book{UserID: user.ID, Label: "js", Deleted: false}
testutils.MustExec(t, testutils.DB.Save(&b1), fmt.Sprintf("preparing b1 for test case %d", idx))
a := NewTest(&App{
Clock: mockClock,
})
tx := testutils.DB.Begin()
if _, err := CreateNote(testutils.DB, user, mockClock, b1.UUID, "note content", tc.addedOn, tc.editedOn, false); err != nil {
if _, err := a.CreateNote(user, b1.UUID, "note content", tc.addedOn, tc.editedOn, false); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting note"))
}
@ -151,8 +155,12 @@ func TestUpdateNote(t *testing.T) {
content := "updated test content"
public := true
a := NewTest(&App{
Clock: c,
})
tx := testutils.DB.Begin()
if _, err := UpdateNote(tx, user, c, note, &UpdateNoteParams{
if _, err := a.UpdateNote(tx, user, note, &UpdateNoteParams{
Content: &content,
Public: &public,
}); err != nil {
@ -218,8 +226,10 @@ func TestDeleteNote(t *testing.T) {
note := database.Note{UserID: user.ID, Deleted: false, Body: "test content", BookUUID: b1.UUID}
testutils.MustExec(t, testutils.DB.Save(&note), fmt.Sprintf("preparing note for test case %d", idx))
a := NewTest(nil)
tx := testutils.DB.Begin()
ret, err := DeleteNote(tx, user, note)
ret, err := a.DeleteNote(tx, user, note)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting note"))
@ -330,7 +340,8 @@ func TestGetNote(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
note, ok, err := GetNote(testutils.DB, tc.note.UUID, tc.user)
a := NewTest(nil)
note, ok, err := a.GetNote(tc.note.UUID, tc.user)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
@ -363,8 +374,9 @@ func TestGetNote_nonexistent(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
a := NewTest(nil)
nonexistentUUID := "4fd19336-671e-4ff3-8f22-662b80e22edd"
note, ok, err := GetNote(testutils.DB, nonexistentUUID, user)
note, ok, err := a.GetNote(nonexistentUUID, user)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}

View file

@ -16,19 +16,18 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"time"
"github.com/dnote/dnote/pkg/server/crypt"
"github.com/dnote/dnote/pkg/server/database"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
// CreateSession returns a new session for the user of the given id
func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
func (a *App) CreateSession(userID int) (database.Session, error) {
key, err := crypt.GetRandomStr(32)
if err != nil {
return database.Session{}, errors.Wrap(err, "generating key")
@ -41,7 +40,7 @@ func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
ExpiresAt: time.Now().Add(24 * 100 * time.Hour),
}
if err := db.Save(&session).Error; err != nil {
if err := a.DB.Save(&session).Error; err != nil {
return database.Session{}, errors.Wrap(err, "saving session")
}
@ -50,8 +49,8 @@ func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
// DeleteUserSessions deletes all existing sessions for the given user. It effectively
// invalidates all existing sessions.
func DeleteUserSessions(db *gorm.DB, userID int) error {
if err := db.Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
func (a *App) DeleteUserSessions(userID int) error {
if err := a.DB.Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting sessions")
}
@ -59,8 +58,8 @@ func DeleteUserSessions(db *gorm.DB, userID int) error {
}
// DeleteSession deletes the session that match the given info
func DeleteSession(db *gorm.DB, sessionKey string) error {
if err := db.Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
func (a *App) DeleteSession(sessionKey string) error {
if err := a.DB.Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting the session")
}

View file

@ -16,13 +16,12 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/pkg/errors"
"github.com/jinzhu/gorm"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/sub"
)
@ -32,7 +31,7 @@ import (
var ErrSubscriptionActive = errors.New("The subscription is currently active")
// CancelSub cancels the subscription of the given user
func CancelSub(subscriptionID string, user database.User) error {
func (a *App) CancelSub(subscriptionID string, user database.User) error {
updateParams := &stripe.SubscriptionParams{
CancelAtPeriodEnd: stripe.Bool(true),
}
@ -46,7 +45,7 @@ func CancelSub(subscriptionID string, user database.User) error {
}
// ReactivateSub reactivates the subscription of the given user
func ReactivateSub(subscriptionID string, user database.User) error {
func (a *App) ReactivateSub(subscriptionID string, user database.User) error {
s, err := sub.Get(subscriptionID, nil)
if err != nil {
return errors.Wrap(err, "fetching subscription")
@ -67,13 +66,13 @@ func ReactivateSub(subscriptionID string, user database.User) error {
}
// MarkUnsubscribed marks the user unsubscribed
func MarkUnsubscribed(db *gorm.DB, stripeCustomerID string) error {
func (a *App) MarkUnsubscribed(stripeCustomerID string) error {
var user database.User
if err := db.Where("stripe_customer_id = ?", stripeCustomerID).First(&user).Error; err != nil {
if err := a.DB.Where("stripe_customer_id = ?", stripeCustomerID).First(&user).Error; err != nil {
return errors.Wrap(err, "finding user")
}
if err := db.Model(&user).Update("cloud", false).Error; err != nil {
if err := a.DB.Model(&user).Update("cloud", false).Error; err != nil {
return errors.Wrap(err, "updating user")
}

View file

@ -0,0 +1,64 @@
/* 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 app
import (
"os"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
)
// NewTest returns an app for a testing environment
func NewTest(appParams *App) App {
emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR")
a := App{
DB: testutils.DB,
WebURL: os.Getenv("WebURL"),
Clock: clock.NewMock(),
EmailTemplates: mailer.NewTemplates(&emailTmplDir),
EmailBackend: &testutils.MockEmailbackendImplementation{},
StripeAPIBackend: nil,
OnPremise: false,
}
// Allow to override with appParams
if appParams != nil && appParams.EmailBackend != nil {
a.EmailBackend = appParams.EmailBackend
}
if appParams != nil && appParams.Clock != nil {
a.Clock = appParams.Clock
}
if appParams != nil && appParams.EmailTemplates != nil {
a.EmailTemplates = appParams.EmailTemplates
}
if appParams != nil && appParams.StripeAPIBackend != nil {
a.StripeAPIBackend = appParams.StripeAPIBackend
}
if appParams != nil && appParams.OnPremise {
a.OnPremise = appParams.OnPremise
}
if appParams != nil && appParams.WebURL != "" {
a.WebURL = appParams.WebURL
}
return a
}

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package operations
package app
import (
"time"
@ -47,7 +47,7 @@ func generateVerificationCode() (string, error) {
}
// TouchLastLoginAt updates the last login timestamp
func TouchLastLoginAt(user database.User, tx *gorm.DB) error {
func (a *App) TouchLastLoginAt(user database.User, tx *gorm.DB) error {
t := time.Now()
if err := tx.Model(&user).Update(database.User{LastLoginAt: &t}).Error; err != nil {
return errors.Wrap(err, "updating last_login_at")
@ -104,8 +104,8 @@ func createDefaultRepetitionRule(user database.User, tx *gorm.DB) error {
}
// CreateUser creates a user
func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
tx := db.Begin()
func (a *App) CreateUser(email, password string) (database.User, error) {
tx := a.DB.Begin()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
@ -113,7 +113,17 @@ func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
return database.User{}, errors.Wrap(err, "hashing password")
}
user := database.User{}
// Grant all privileges if self-hosting
var pro bool
if a.OnPremise {
pro = true
} else {
pro = false
}
user := database.User{
Cloud: pro,
}
if err = tx.Save(&user).Error; err != nil {
tx.Rollback()
return database.User{}, errors.Wrap(err, "saving user")
@ -140,7 +150,7 @@ func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
tx.Rollback()
return database.User{}, errors.Wrap(err, "creating default repetition rule")
}
if err := TouchLastLoginAt(user, tx); err != nil {
if err := a.TouchLastLoginAt(user, tx); err != nil {
tx.Rollback()
return database.User{}, errors.Wrap(err, "updating last login")
}

View file

@ -0,0 +1,66 @@
/* 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 app
import (
"fmt"
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
)
func TestCreateUser(t *testing.T) {
testCases := []struct {
onPremise bool
expectedPro bool
}{
{
onPremise: true,
expectedPro: true,
},
{
onPremise: false,
expectedPro: false,
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("self hosting %t", tc.onPremise), func(t *testing.T) {
defer testutils.ClearData()
a := NewTest(&App{
OnPremise: tc.onPremise,
})
if _, err := a.CreateUser("alice@example.com", "pass1234"); err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
var userCount int
var userRecord database.User
testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
testutils.MustExec(t, testutils.DB.First(&userRecord), "finding user")
assert.Equal(t, userCount, 1, "book count mismatch")
assert.Equal(t, userRecord.Cloud, tc.expectedPro, "user pro mismatch")
})
}
}

View file

@ -1,3 +1,21 @@
/* 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 dbconn
import (

View file

@ -27,7 +27,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
@ -53,7 +52,7 @@ func makeSession(user database.User, account database.Account) Session {
}
}
func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
func (a *API) getMe(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -61,7 +60,7 @@ func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@ -74,8 +73,8 @@ func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
User: session,
}
tx := a.DB.Begin()
if err := operations.TouchLastLoginAt(user, tx); err != nil {
tx := a.App.DB.Begin()
if err := a.App.TouchLastLoginAt(user, tx); err != nil {
tx.Rollback()
// In case of an error, gracefully continue to avoid disturbing the service
log.Println("error touching last_login_at", err.Error())
@ -89,7 +88,7 @@ type createResetTokenPayload struct {
Email string `json:"email"`
}
func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
func (a *API) createResetToken(w http.ResponseWriter, r *http.Request) {
var params createResetTokenPayload
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@ -97,7 +96,7 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
conn := a.DB.Where("email = ?", params.Email).First(&account)
conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
return
}
@ -123,22 +122,19 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
Type: database.TokenTypeResetPassword,
}
if err := a.DB.Save(&token).Error; err != nil {
if err := a.App.DB.Save(&token).Error; err != nil {
HandleError(w, errors.Wrap(err, "saving token").Error(), nil, http.StatusInternalServerError)
return
}
body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPassword, mailer.EmailKindText, mailer.EmailResetPasswordTmplData{
AccountEmail: account.Email.String,
Token: resetToken,
WebURL: a.WebURL,
})
if err != nil {
HandleError(w, errors.Wrap(err, "executing reset password email template").Error(), nil, http.StatusInternalServerError)
}
if err := a.App.SendPasswordResetEmail(account.Email.String, resetToken); err != nil {
if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
respondInvalidSMTPConfig(w)
} else {
HandleError(w, errors.Wrap(err, "sending password reset email").Error(), nil, http.StatusInternalServerError)
}
if err := a.EmailBackend.Queue("Reset your password", "sung@getdnote.com", []string{params.Email}, "text/plain", body); err != nil {
HandleError(w, errors.Wrap(err, "queueing email").Error(), nil, http.StatusInternalServerError)
return
}
}
@ -147,7 +143,7 @@ type resetPasswordPayload struct {
Token string `json:"token"`
}
func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
var params resetPasswordPayload
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@ -155,7 +151,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
}
var token database.Token
conn := a.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token)
conn := a.App.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token)
if conn.RecordNotFound() {
http.Error(w, "invalid token", http.StatusBadRequest)
return
@ -176,7 +172,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost)
if err != nil {
@ -186,7 +182,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
tx.Rollback()
HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
@ -206,10 +202,10 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
tx.Commit()
var user database.User
if err := a.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil {
if err := a.App.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil {
HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
}
respondWithSession(a.DB, w, user.ID, http.StatusOK)
a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
}

View file

@ -25,6 +25,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"golang.org/x/crypto/bcrypt"
@ -35,7 +36,7 @@ func TestGetMe(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -62,7 +63,7 @@ func TestCreateResetToken(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -96,7 +97,7 @@ func TestCreateResetToken(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -126,7 +127,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -173,7 +174,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -212,7 +213,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -250,7 +251,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -299,7 +300,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})

View file

@ -26,13 +26,12 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
func (a *API) classicMigrate(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -40,12 +39,12 @@ func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
if err := a.DB.Model(&account).
if err := a.App.DB.Model(&account).
Update(map[string]interface{}{
"salt": "",
"auth_key_hash": "",
@ -63,7 +62,7 @@ type PresigninResponse struct {
Iteration int `json:"iteration"`
}
func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
func (a *API) classicPresignin(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
email := q.Get("email")
if email == "" {
@ -72,7 +71,7 @@ func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
conn := a.DB.Where("email = ?", email).First(&account)
conn := a.App.DB.Where("email = ?", email).First(&account)
if !conn.RecordNotFound() && conn.Error != nil {
HandleError(w, "getting user", conn.Error, http.StatusInternalServerError)
return
@ -101,7 +100,7 @@ type classicSigninPayload struct {
AuthKey string `json:"auth_key"`
}
func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
func (a *API) classicSignin(w http.ResponseWriter, r *http.Request) {
var params classicSigninPayload
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
HandleError(w, "decoding payload", err, http.StatusInternalServerError)
@ -114,7 +113,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
conn := a.DB.Where("email = ?", params.Email).First(&account)
conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
@ -132,7 +131,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
return
}
session, err := operations.CreateSession(a.DB, account.UserID)
session, err := a.App.CreateSession(account.UserID)
if err != nil {
HandleError(w, "creating session", nil, http.StatusBadRequest)
return
@ -156,7 +155,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
}
}
func (a *App) classicGetMe(w http.ResponseWriter, r *http.Request) {
func (a *API) classicGetMe(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -164,7 +163,7 @@ func (a *App) classicGetMe(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@ -214,7 +213,7 @@ type classicSetPasswordPayload struct {
Password string
}
func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
func (a *API) classicSetPassword(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -228,7 +227,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
@ -239,7 +238,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
return
}
if err := a.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError)
return
}
@ -247,7 +246,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (a *App) classicGetNotes(w http.ResponseWriter, r *http.Request) {
func (a *API) classicGetNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -255,7 +254,7 @@ func (a *App) classicGetNotes(w http.ResponseWriter, r *http.Request) {
}
var notes []database.Note
if err := a.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(&notes).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(&notes).Error; err != nil {
HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}

View file

@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
@ -68,7 +69,7 @@ func TestClassicPresignin(t *testing.T) {
t.Run(fmt.Sprintf("presignin %s", tc.email), func(t *testing.T) {
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -96,7 +97,7 @@ func TestClassicPresignin_MissingParams(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -118,7 +119,7 @@ func TestClassicSignin(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&alice), "saving alice")
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -216,7 +217,7 @@ func TestClassicSignin_Failure(t *testing.T) {
t.Run(fmt.Sprintf("signin %s %s", tc.email, tc.authKey), func(t *testing.T) {
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()

View file

@ -22,7 +22,7 @@ import (
"net/http"
)
func (a *App) checkHealth(w http.ResponseWriter, r *http.Request) {
func (a *API) checkHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}

View file

@ -24,13 +24,14 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/jinzhu/gorm"
)
func TestCheckHealth(t *testing.T) {
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
})

View file

@ -136,7 +136,7 @@ func respondJSON(w http.ResponseWriter, statusCode int, payload interface{}) {
}
// notSupported is the handler for the route that is no longer supported
func (a *App) notSupported(w http.ResponseWriter, r *http.Request) {
func (a *API) notSupported(w http.ResponseWriter, r *http.Request) {
http.Error(w, "API version is not supported. Please upgrade your client.", http.StatusGone)
return
}
@ -155,3 +155,7 @@ func respondUnauthorized(w http.ResponseWriter) {
func RespondNotFound(w http.ResponseWriter) {
http.Error(w, "not found", http.StatusNotFound)
}
func respondInvalidSMTPConfig(w http.ResponseWriter) {
http.Error(w, "SMTP is not configured", http.StatusInternalServerError)
}

View file

@ -1,3 +1,21 @@
/* 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 (

View file

@ -28,7 +28,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
@ -99,8 +98,8 @@ func getNoteBaseQuery(db *gorm.DB, noteUUID string, search string) *gorm.DB {
return conn
}
func (a *App) getNote(w http.ResponseWriter, r *http.Request) {
user, _, err := AuthWithSession(a.DB, r, nil)
func (a *API) getNote(w http.ResponseWriter, r *http.Request) {
user, _, err := AuthWithSession(a.App.DB, r, nil)
if err != nil {
HandleError(w, "authenticating", err, http.StatusInternalServerError)
return
@ -109,7 +108,7 @@ func (a *App) getNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
note, ok, err := operations.GetNote(a.DB, noteUUID, user)
note, ok, err := a.App.GetNote(noteUUID, user)
if !ok {
RespondNotFound(w)
return
@ -135,7 +134,7 @@ type dateRange struct {
upper int64
}
func (a *App) getNotes(w http.ResponseWriter, r *http.Request) {
func (a *API) getNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -143,7 +142,7 @@ func (a *App) getNotes(w http.ResponseWriter, r *http.Request) {
}
query := r.URL.Query()
respondGetNotes(a.DB, user.ID, query, w)
respondGetNotes(a.App.DB, user.ID, query, w)
}
func respondGetNotes(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) {
@ -305,7 +304,7 @@ func escapeSearchQuery(searchQuery string) string {
return strings.Join(strings.Fields(searchQuery), "&")
}
func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
func (a *API) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -313,7 +312,7 @@ func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
}
var notes []database.Note
if err := a.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(&notes).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(&notes).Error; err != nil {
HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}

View file

@ -28,6 +28,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@ -59,7 +60,7 @@ func TestGetNotes(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -171,7 +172,7 @@ func TestGetNote(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})

View file

@ -30,7 +30,7 @@ import (
"github.com/pkg/errors"
)
func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *API) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -46,7 +46,7 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
}
var repetitionRule database.RepetitionRule
if err := a.DB.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil {
HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@ -55,7 +55,7 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, resp)
}
func (a *App) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
func (a *API) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -63,7 +63,7 @@ func (a *App) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
}
var repetitionRules []database.RepetitionRule
if err := a.DB.Where("user_id = ?", user.ID).Preload("Books").Order("last_active DESC").Find(&repetitionRules).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).Preload("Books").Order("last_active DESC").Find(&repetitionRules).Error; err != nil {
HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@ -273,7 +273,7 @@ func calcNextActive(now time.Time, p calcNextActiveParams) int64 {
return t0 + p.Frequency
}
func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *API) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -287,12 +287,12 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
}
var books []database.Book
if err := a.DB.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil {
HandleError(w, "finding books", nil, http.StatusInternalServerError)
return
}
nextActive := calcNextActive(a.Clock.Now(), calcNextActiveParams{
nextActive := calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: params.GetHour(),
Minute: params.GetMinute(),
Frequency: params.GetFrequency(),
@ -310,7 +310,7 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
NoteCount: params.GetNoteCount(),
Enabled: params.GetEnabled(),
}
if err := a.DB.Create(&record).Error; err != nil {
if err := a.App.DB.Create(&record).Error; err != nil {
HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
return
}
@ -333,7 +333,7 @@ func parseUpdateDigestParams(r *http.Request) (repetitionRuleParams, error) {
return ret, nil
}
func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *API) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -344,7 +344,7 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
repetitionRuleUUID := vars["repetitionRuleUUID"]
var rule database.RepetitionRule
conn := a.DB.Where("uuid = ? AND user_id = ?", repetitionRuleUUID, user.ID).First(&rule)
conn := a.App.DB.Where("uuid = ? AND user_id = ?", repetitionRuleUUID, user.ID).First(&rule)
if conn.RecordNotFound() {
http.Error(w, "Not found", http.StatusNotFound)
@ -354,14 +354,14 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
return
}
if err := a.DB.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil {
if err := a.App.DB.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil {
HandleError(w, "deleting the repetition rule", err, http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *API) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -377,7 +377,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
var repetitionRule database.RepetitionRule
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").First(&repetitionRule).Error; err != nil {
@ -393,7 +393,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
repetitionRule.Enabled = enabled
if enabled && !repetitionRule.Enabled {
repetitionRule.NextActive = calcNextActive(a.Clock.Now(), calcNextActiveParams{
repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: repetitionRule.Hour,
Minute: repetitionRule.Minute,
Frequency: repetitionRule.Frequency,
@ -412,7 +412,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
frequency := params.GetFrequency()
repetitionRule.Frequency = frequency
repetitionRule.NextActive = calcNextActive(a.Clock.Now(), calcNextActiveParams{
repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: repetitionRule.Hour,
Minute: repetitionRule.Minute,
Frequency: frequency,

View file

@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@ -34,12 +35,12 @@ import (
)
func TestGetRepetitionRule(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -109,12 +110,12 @@ func TestGetRepetitionRule(t *testing.T) {
}
func TestGetRepetitionRules(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -215,7 +216,7 @@ func TestGetRepetitionRules(t *testing.T) {
func TestCreateRepetitionRules(t *testing.T) {
t.Run("all books", func(t *testing.T) {
defer testutils.ClearData()
// Setup
@ -223,8 +224,8 @@ func TestCreateRepetitionRules(t *testing.T) {
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: c,
})
defer server.Close()
@ -274,7 +275,7 @@ func TestCreateRepetitionRules(t *testing.T) {
}
for _, tc := range bookDomainTestCases {
t.Run(tc, func(t *testing.T) {
defer testutils.ClearData()
// Setup
@ -282,8 +283,8 @@ func TestCreateRepetitionRules(t *testing.T) {
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: c,
})
defer server.Close()
@ -339,15 +340,15 @@ func TestCreateRepetitionRules(t *testing.T) {
}
func TestUpdateRepetitionRules(t *testing.T) {
defer testutils.ClearData()
// Setup
c := clock.NewMock()
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: c,
})
defer server.Close()
@ -417,12 +418,12 @@ func TestUpdateRepetitionRules(t *testing.T) {
}
func TestDeleteRepetitionRules(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -543,12 +544,12 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case - create %d", idx), func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -568,7 +569,7 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
})
t.Run(fmt.Sprintf("test case %d - update", idx), func(t *testing.T) {
defer testutils.ClearData()
// Setup
@ -592,8 +593,8 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1")
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -628,12 +629,12 @@ func TestCreateRepetitionRules_BadRequest(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()

View file

@ -26,30 +26,16 @@ import (
"strings"
"time"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
)
var (
// ErrEmptyDB is an error for missing database connection in the app configuration
ErrEmptyDB = errors.New("No database connection was provided")
// ErrEmptyClock is an error for missing clock in the app configuration
ErrEmptyClock = errors.New("No clock was provided")
// ErrEmptyWebURL is an error for missing WebURL content in the app configuration
ErrEmptyWebURL = errors.New("No WebURL was provided")
// ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
// ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
)
// Route represents a single route
type Route struct {
Method string
@ -199,9 +185,9 @@ type AuthMiddlewareParams struct {
ProOnly bool
}
func (a *App) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerFunc {
func (a *API) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok, err := AuthWithSession(a.DB, r, p)
user, ok, err := AuthWithSession(a.App.DB, r, p)
if !ok {
respondUnauthorized(w)
return
@ -223,9 +209,9 @@ func (a *App) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerF
})
}
func (a *App) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewareParams) http.HandlerFunc {
func (a *API) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewareParams) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, token, ok, err := authWithToken(a.DB, r, tokenType, p)
user, token, ok, err := authWithToken(a.App.DB, r, tokenType, p)
if err != nil {
// log the error and continue
log.ErrorWrap(err, "authenticating with token")
@ -237,7 +223,7 @@ func (a *App) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewa
ctx = context.WithValue(ctx, helpers.KeyToken, token)
} else {
// If token-based auth fails, fall back to session-based auth
user, ok, err = AuthWithSession(a.DB, r, p)
user, ok, err = AuthWithSession(a.App.DB, r, p)
if err != nil {
HandleError(w, "authenticating with session", err, http.StatusInternalServerError)
return
@ -316,54 +302,29 @@ func applyMiddleware(h http.HandlerFunc, rateLimit bool) http.Handler {
return ret
}
// App is an application configuration
type App struct {
DB *gorm.DB
Clock clock.Clock
StripeAPIBackend stripe.Backend
EmailTemplates mailer.Templates
EmailBackend mailer.Backend
WebURL string
}
func (a *App) validate() error {
if a.WebURL == "" {
return ErrEmptyWebURL
}
if a.Clock == nil {
return ErrEmptyClock
}
if a.EmailTemplates == nil {
return ErrEmptyEmailTemplates
}
if a.EmailBackend == nil {
return ErrEmptyEmailBackend
}
if a.DB == nil {
return ErrEmptyDB
}
return nil
// API is a web API configuration
type API struct {
App *app.App
}
// init sets up the application based on the configuration
func (a *App) init() error {
if err := a.validate(); err != nil {
func (a *API) init() error {
if err := a.App.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)
if a.App.StripeAPIBackend != nil {
stripe.SetBackend(stripe.APIBackend, a.App.StripeAPIBackend)
}
return nil
}
// NewRouter creates and returns a new router
func NewRouter(app *App) (*mux.Router, error) {
if err := app.init(); err != nil {
func (a *API) NewRouter() (*mux.Router, error) {
if err := a.init(); err != nil {
return nil, errors.Wrap(err, "initializing app")
}
@ -371,61 +332,61 @@ func NewRouter(app *App) (*mux.Router, error) {
var routes = []Route{
// internal
{"GET", "/health", app.checkHealth, false},
{"GET", "/me", app.auth(app.getMe, nil), true},
{"POST", "/verification-token", app.auth(app.createVerificationToken, nil), true},
{"PATCH", "/verify-email", app.verifyEmail, true},
{"POST", "/reset-token", app.createResetToken, true},
{"PATCH", "/reset-password", app.resetPassword, true},
{"PATCH", "/account/profile", app.auth(app.updateProfile, nil), true},
{"PATCH", "/account/password", app.auth(app.updatePassword, nil), true},
{"GET", "/account/email-preference", app.tokenAuth(app.getEmailPreference, database.TokenTypeEmailPreference, nil), true},
{"PATCH", "/account/email-preference", app.tokenAuth(app.updateEmailPreference, database.TokenTypeEmailPreference, nil), true},
{"POST", "/subscriptions", app.auth(app.createSub, nil), true},
{"PATCH", "/subscriptions", app.auth(app.updateSub, nil), true},
{"POST", "/webhooks/stripe", app.stripeWebhook, true},
{"GET", "/subscriptions", app.auth(app.getSub, nil), true},
{"GET", "/stripe_source", app.auth(app.getStripeSource, nil), true},
{"PATCH", "/stripe_source", app.auth(app.updateStripeSource, nil), true},
{"GET", "/notes", app.auth(app.getNotes, nil), false},
{"GET", "/notes/{noteUUID}", app.getNote, true},
{"GET", "/calendar", app.auth(app.getCalendar, nil), true},
{"GET", "/repetition_rules", app.auth(app.getRepetitionRules, nil), true},
{"GET", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
{"POST", "/repetition_rules", app.auth(app.createRepetitionRule, &proOnly), true},
{"PATCH", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
{"DELETE", "/repetition_rules/{repetitionRuleUUID}", app.auth(app.deleteRepetitionRule, &proOnly), true},
{"GET", "/health", a.checkHealth, false},
{"GET", "/me", a.auth(a.getMe, nil), true},
{"POST", "/verification-token", a.auth(a.createVerificationToken, nil), true},
{"PATCH", "/verify-email", a.verifyEmail, true},
{"POST", "/reset-token", a.createResetToken, true},
{"PATCH", "/reset-password", a.resetPassword, true},
{"PATCH", "/account/profile", a.auth(a.updateProfile, nil), true},
{"PATCH", "/account/password", a.auth(a.updatePassword, nil), true},
{"GET", "/account/email-preference", a.tokenAuth(a.getEmailPreference, database.TokenTypeEmailPreference, nil), true},
{"PATCH", "/account/email-preference", a.tokenAuth(a.updateEmailPreference, database.TokenTypeEmailPreference, nil), true},
{"POST", "/subscriptions", a.auth(a.createSub, nil), true},
{"PATCH", "/subscriptions", a.auth(a.updateSub, nil), true},
{"POST", "/webhooks/stripe", a.stripeWebhook, true},
{"GET", "/subscriptions", a.auth(a.getSub, nil), true},
{"GET", "/stripe_source", a.auth(a.getStripeSource, nil), true},
{"PATCH", "/stripe_source", a.auth(a.updateStripeSource, nil), true},
{"GET", "/notes", a.auth(a.getNotes, nil), false},
{"GET", "/notes/{noteUUID}", a.getNote, true},
{"GET", "/calendar", a.auth(a.getCalendar, nil), true},
{"GET", "/repetition_rules", a.auth(a.getRepetitionRules, nil), true},
{"GET", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
{"POST", "/repetition_rules", a.auth(a.createRepetitionRule, &proOnly), true},
{"PATCH", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
{"DELETE", "/repetition_rules/{repetitionRuleUUID}", a.auth(a.deleteRepetitionRule, &proOnly), true},
// migration of classic users
{"GET", "/classic/presignin", cors(app.classicPresignin), true},
{"POST", "/classic/signin", cors(app.classicSignin), true},
{"PATCH", "/classic/migrate", app.auth(app.classicMigrate, &proOnly), true},
{"GET", "/classic/notes", app.auth(app.classicGetNotes, nil), true},
{"PATCH", "/classic/set-password", app.auth(app.classicSetPassword, nil), true},
{"GET", "/classic/presignin", cors(a.classicPresignin), true},
{"POST", "/classic/signin", cors(a.classicSignin), true},
{"PATCH", "/classic/migrate", a.auth(a.classicMigrate, &proOnly), true},
{"GET", "/classic/notes", a.auth(a.classicGetNotes, nil), true},
{"PATCH", "/classic/set-password", a.auth(a.classicSetPassword, nil), true},
// v3
{"GET", "/v3/sync/fragment", cors(app.auth(app.GetSyncFragment, nil)), false},
{"GET", "/v3/sync/state", cors(app.auth(app.GetSyncState, nil)), false},
{"OPTIONS", "/v3/books", cors(app.BooksOptions), true},
{"GET", "/v3/books", cors(app.auth(app.GetBooks, nil)), true},
{"GET", "/v3/books/{bookUUID}", cors(app.auth(app.GetBook, nil)), true},
{"POST", "/v3/books", cors(app.auth(app.CreateBook, nil)), false},
{"PATCH", "/v3/books/{bookUUID}", cors(app.auth(app.UpdateBook, nil)), false},
{"DELETE", "/v3/books/{bookUUID}", cors(app.auth(app.DeleteBook, nil)), false},
{"OPTIONS", "/v3/notes", cors(app.NotesOptions), true},
{"POST", "/v3/notes", cors(app.auth(app.CreateNote, nil)), false},
{"PATCH", "/v3/notes/{noteUUID}", app.auth(app.UpdateNote, nil), false},
{"DELETE", "/v3/notes/{noteUUID}", app.auth(app.DeleteNote, nil), false},
{"POST", "/v3/signin", cors(app.signin), true},
{"OPTIONS", "/v3/signout", cors(app.signoutOptions), true},
{"POST", "/v3/signout", cors(app.signout), true},
{"POST", "/v3/register", app.register, true},
{"GET", "/v3/sync/fragment", cors(a.auth(a.GetSyncFragment, nil)), false},
{"GET", "/v3/sync/state", cors(a.auth(a.GetSyncState, nil)), false},
{"OPTIONS", "/v3/books", cors(a.BooksOptions), true},
{"GET", "/v3/books", cors(a.auth(a.GetBooks, nil)), true},
{"GET", "/v3/books/{bookUUID}", cors(a.auth(a.GetBook, nil)), true},
{"POST", "/v3/books", cors(a.auth(a.CreateBook, nil)), false},
{"PATCH", "/v3/books/{bookUUID}", cors(a.auth(a.UpdateBook, nil)), false},
{"DELETE", "/v3/books/{bookUUID}", cors(a.auth(a.DeleteBook, nil)), false},
{"OPTIONS", "/v3/notes", cors(a.NotesOptions), true},
{"POST", "/v3/notes", cors(a.auth(a.CreateNote, nil)), false},
{"PATCH", "/v3/notes/{noteUUID}", a.auth(a.UpdateNote, nil), false},
{"DELETE", "/v3/notes/{noteUUID}", a.auth(a.DeleteNote, nil), false},
{"POST", "/v3/signin", cors(a.signin), true},
{"OPTIONS", "/v3/signout", cors(a.signoutOptions), true},
{"POST", "/v3/signout", cors(a.signout), true},
{"POST", "/v3/register", a.register, true},
}
router := mux.NewRouter().StrictSlash(true)
router.PathPrefix("/v1").Handler(applyMiddleware(app.notSupported, true))
router.PathPrefix("/v2").Handler(applyMiddleware(app.notSupported, true))
router.PathPrefix("/v1").Handler(applyMiddleware(a.notSupported, true))
router.PathPrefix("/v2").Handler(applyMiddleware(a.notSupported, true))
for _, route := range routes {
handler := route.HandlerFunc

View file

@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
@ -202,8 +203,8 @@ func TestAuthMiddleware(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
app := App{DB: testutils.DB}
server := httptest.NewServer(app.auth(handler, nil))
api := API{App: &app.App{DB: testutils.DB}}
server := httptest.NewServer(api.auth(handler, nil))
defer server.Close()
t.Run("with header", func(t *testing.T) {
@ -310,8 +311,8 @@ func TestAuthMiddleware_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
app := App{DB: testutils.DB}
server := httptest.NewServer(app.auth(handler, &AuthMiddlewareParams{
api := API{App: &app.App{DB: testutils.DB}}
server := httptest.NewServer(api.auth(handler, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
@ -403,8 +404,8 @@ func TestTokenAuthMiddleWare(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
app := App{DB: testutils.DB}
server := httptest.NewServer(app.tokenAuth(handler, database.TokenTypeEmailPreference, nil))
api := API{App: &app.App{DB: testutils.DB}}
server := httptest.NewServer(api.tokenAuth(handler, database.TokenTypeEmailPreference, nil))
defer server.Close()
t.Run("with token", func(t *testing.T) {
@ -533,8 +534,8 @@ func TestTokenAuthMiddleWare_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
app := App{DB: testutils.DB}
server := httptest.NewServer(app.tokenAuth(handler, database.TokenTypeEmailPreference, &AuthMiddlewareParams{
api := API{App: &app.App{DB: testutils.DB}}
server := httptest.NewServer(api.tokenAuth(handler, database.TokenTypeEmailPreference, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
@ -671,7 +672,7 @@ func TestNotSupportedVersions(t *testing.T) {
}
// setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
})
@ -691,11 +692,11 @@ func TestNotSupportedVersions(t *testing.T) {
func TestNewRouter_AppValidate(t *testing.T) {
testCases := []struct {
app App
app app.App
expectedErr error
}{
{
app: App{
app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@ -706,7 +707,7 @@ func TestNewRouter_AppValidate(t *testing.T) {
expectedErr: nil,
},
{
app: App{
app: app.App{
DB: nil,
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@ -714,10 +715,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyDB,
expectedErr: app.ErrEmptyDB,
},
{
app: App{
app: app.App{
DB: &gorm.DB{},
Clock: nil,
StripeAPIBackend: nil,
@ -725,10 +726,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyClock,
expectedErr: app.ErrEmptyClock,
},
{
app: App{
app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@ -736,10 +737,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyEmailTemplates,
expectedErr: app.ErrEmptyEmailTemplates,
},
{
app: App{
app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@ -747,10 +748,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: nil,
WebURL: "http://mock.url",
},
expectedErr: ErrEmptyEmailBackend,
expectedErr: app.ErrEmptyEmailBackend,
},
{
app: App{
app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@ -758,13 +759,14 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "",
},
expectedErr: ErrEmptyWebURL,
expectedErr: app.ErrEmptyWebURL,
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
_, err := NewRouter(&tc.app)
api := API{App: &tc.app}
_, err := api.NewRouter()
assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
})

View file

@ -26,9 +26,9 @@ import (
"os"
"strings"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
@ -125,7 +125,7 @@ type createSubPayload struct {
}
// createSub creates a subscription for a the current user
func (a *App) createSub(w http.ResponseWriter, r *http.Request) {
func (a *API) createSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -138,7 +138,7 @@ func (a *App) createSub(w http.ResponseWriter, r *http.Request) {
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
if err := tx.Model(&user).
Update(map[string]interface{}{
@ -214,7 +214,7 @@ func validateUpdateSubPayload(p updateSubPayload) error {
return nil
}
func (a *App) updateSub(w http.ResponseWriter, r *http.Request) {
func (a *API) updateSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -237,14 +237,14 @@ func (a *App) updateSub(w http.ResponseWriter, r *http.Request) {
var err error
if payload.Op == updateSubOpCancel {
err = operations.CancelSub(payload.StripeSubcriptionID, user)
err = a.App.CancelSub(payload.StripeSubcriptionID, user)
} else if payload.Op == updateSubOpReactivate {
err = operations.ReactivateSub(payload.StripeSubcriptionID, user)
err = a.App.ReactivateSub(payload.StripeSubcriptionID, user)
}
if err != nil {
var statusCode int
if err == operations.ErrSubscriptionActive {
if err == app.ErrSubscriptionActive {
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
@ -285,7 +285,7 @@ func respondWithEmptySub(w http.ResponseWriter) {
}
}
func (a *App) getSub(w http.ResponseWriter, r *http.Request) {
func (a *API) getSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -413,7 +413,7 @@ func validateUpdateStripeSourcePayload(p updateStripeSourcePayload) error {
return nil
}
func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
func (a *API) updateStripeSource(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -430,7 +430,7 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
if err := tx.Model(&user).
Update(map[string]interface{}{
@ -469,7 +469,7 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
func (a *API) getStripeSource(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -507,7 +507,7 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, resp)
}
func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
func (a *API) stripeWebhook(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
HandleError(w, "reading body", err, http.StatusServiceUnavailable)
@ -530,7 +530,7 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
return
}
operations.MarkUnsubscribed(a.DB, subscription.Customer.ID)
a.App.MarkUnsubscribed(subscription.Customer.ID)
}
default:
{

View file

@ -20,29 +20,18 @@ package handlers
import (
"net/http/httptest"
"os"
"testing"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/dnote/dnote/pkg/server/app"
"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")
app.DB = testutils.DB
func MustNewServer(t *testing.T, appParams *app.App) *httptest.Server {
api := NewTestAPI(appParams)
// If email backend was not provided, use the default mock backend
if app.EmailBackend == nil {
app.EmailBackend = &testutils.MockEmailbackendImplementation{}
}
emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR")
app.EmailTemplates = mailer.NewTemplates(&emailTmplDir)
r, err := NewRouter(app)
r, err := api.NewRouter()
if err != nil {
t.Fatal(errors.Wrap(err, "initializing server"))
}
@ -51,3 +40,10 @@ func MustNewServer(t *testing.T, app *App) *httptest.Server {
return server
}
// NewTestAPI returns a new API for test
func NewTestAPI(appParams *app.App) API {
a := app.NewTest(appParams)
return API{App: &a}
}

View file

@ -38,7 +38,7 @@ type updateProfilePayload struct {
}
// updateProfile updates user
func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
func (a *API) updateProfile(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -59,13 +59,13 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
err = a.DB.Where("user_id = ?", user.ID).First(&account).Error
err = a.App.DB.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
if err := tx.Save(&user).Error; err != nil {
tx.Rollback()
HandleError(w, "saving user", err, http.StatusInternalServerError)
@ -86,7 +86,7 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
tx.Commit()
respondWithSession(a.DB, w, user.ID, http.StatusOK)
a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
}
type updateEmailPayload struct {
@ -122,17 +122,17 @@ func respondWithCalendar(db *gorm.DB, w http.ResponseWriter, userID int) {
respondJSON(w, http.StatusOK, payload)
}
func (a *App) getCalendar(w http.ResponseWriter, r *http.Request) {
func (a *API) getCalendar(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
respondWithCalendar(a.DB, w, user.ID)
respondWithCalendar(a.App.DB, w, user.ID)
}
func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
func (a *API) createVerificationToken(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -140,7 +140,7 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
err := a.DB.Where("user_id = ?", user.ID).First(&account).Error
err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
@ -167,20 +167,19 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
Type: database.TokenTypeEmailVerification,
}
if err := a.DB.Save(&token).Error; err != nil {
if err := a.App.DB.Save(&token).Error; err != nil {
HandleError(w, "saving token", err, http.StatusInternalServerError)
return
}
body, err := a.EmailTemplates.Execute(mailer.EmailTypeEmailVerification, mailer.EmailKindText, mailer.EmailVerificationTmplData{
Token: tokenValue,
WebURL: a.WebURL,
})
if err != nil {
HandleError(w, errors.Wrap(err, "executing reset verification template").Error(), nil, http.StatusInternalServerError)
}
if err := a.EmailBackend.Queue("Verify your Dnote email address", "sung@getdnote.com", []string{account.Email.String}, "text/plain", body); err != nil {
HandleError(w, errors.Wrap(err, "queueing email").Error(), nil, http.StatusInternalServerError)
if err := a.App.SendVerificationEmail(account.Email.String, tokenValue); err != nil {
if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
respondInvalidSMTPConfig(w)
} else {
HandleError(w, errors.Wrap(err, "sending verification email").Error(), nil, http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
@ -190,7 +189,7 @@ type verifyEmailPayload struct {
Token string `json:"token"`
}
func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
func (a *API) verifyEmail(w http.ResponseWriter, r *http.Request) {
var params verifyEmailPayload
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
HandleError(w, "decoding payload", err, http.StatusInternalServerError)
@ -198,7 +197,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
}
var token database.Token
if err := a.DB.
if err := a.App.DB.
Where("value = ? AND type = ?", params.Token, database.TokenTypeEmailVerification).
First(&token).Error; err != nil {
http.Error(w, "invalid token", http.StatusBadRequest)
@ -217,7 +216,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@ -226,7 +225,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
account.EmailVerified = true
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
@ -241,7 +240,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
tx.Commit()
var user database.User
if err := a.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
if err := a.App.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
@ -254,7 +253,7 @@ type updateEmailPreferencePayload struct {
DigestWeekly bool `json:"digest_weekly"`
}
func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
func (a *API) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -268,12 +267,12 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
}
var frequency database.EmailPreference
if err := a.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&frequency).Error; err != nil {
if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&frequency).Error; err != nil {
HandleError(w, "finding frequency", err, http.StatusInternalServerError)
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
frequency.DigestWeekly = params.DigestWeekly
if err := tx.Save(&frequency).Error; err != nil {
@ -297,7 +296,7 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, frequency)
}
func (a *App) getEmailPreference(w http.ResponseWriter, r *http.Request) {
func (a *API) getEmailPreference(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -305,7 +304,7 @@ func (a *App) getEmailPreference(w http.ResponseWriter, r *http.Request) {
}
var pref database.EmailPreference
if err := a.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
HandleError(w, "finding pref", err, http.StatusInternalServerError)
return
}
@ -319,7 +318,7 @@ type updatePasswordPayload struct {
NewPassword string `json:"new_password"`
}
func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
func (a *API) updatePassword(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -337,7 +336,7 @@ func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
@ -362,7 +361,7 @@ func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
return
}
if err := a.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError)
return
}

View file

@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@ -40,8 +41,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -69,7 +69,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -96,7 +96,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -125,7 +125,7 @@ func TestCreateVerificationToken(t *testing.T) {
// Setup
emailBackend := testutils.MockEmailbackendImplementation{}
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
EmailBackend: &emailBackend,
})
@ -160,7 +160,7 @@ func TestCreateVerificationToken(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -194,8 +194,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -236,8 +235,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -281,8 +279,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -324,8 +321,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -370,8 +366,7 @@ func TestUpdateEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -405,8 +400,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -432,7 +426,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -473,7 +467,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -508,7 +502,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -545,7 +539,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -582,7 +576,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -609,7 +603,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -643,10 +637,9 @@ func TestUpdateEmailPreference(t *testing.T) {
}
func TestGetEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})

View file

@ -25,8 +25,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@ -80,7 +78,7 @@ type signinPayload struct {
Password string `json:"password"`
}
func (a *App) signin(w http.ResponseWriter, r *http.Request) {
func (a *API) signin(w http.ResponseWriter, r *http.Request) {
var params signinPayload
err := json.NewDecoder(r.Body).Decode(&params)
if err != nil {
@ -93,7 +91,7 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
conn := a.DB.Where("email = ?", params.Email).First(&account)
conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
@ -110,27 +108,27 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
}
var user database.User
err = a.DB.Where("id = ?", account.UserID).First(&user).Error
err = a.App.DB.Where("id = ?", account.UserID).First(&user).Error
if err != nil {
HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
err = operations.TouchLastLoginAt(user, a.DB)
err = a.App.TouchLastLoginAt(user, a.App.DB)
if err != nil {
http.Error(w, errors.Wrap(err, "touching login timestamp").Error(), http.StatusInternalServerError)
return
}
respondWithSession(a.DB, w, account.UserID, http.StatusOK)
a.respondWithSession(a.App.DB, w, account.UserID, http.StatusOK)
}
func (a *App) signoutOptions(w http.ResponseWriter, r *http.Request) {
func (a *API) signoutOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}
func (a *App) signout(w http.ResponseWriter, r *http.Request) {
func (a *API) signout(w http.ResponseWriter, r *http.Request) {
key, err := getCredential(r)
if err != nil {
HandleError(w, "getting credential", nil, http.StatusInternalServerError)
@ -142,7 +140,7 @@ func (a *App) signout(w http.ResponseWriter, r *http.Request) {
return
}
err = operations.DeleteSession(a.DB, key)
err = a.App.DeleteSession(key)
if err != nil {
HandleError(w, "deleting session", nil, http.StatusInternalServerError)
return
@ -177,7 +175,7 @@ func parseRegisterPaylaod(r *http.Request) (registerPayload, error) {
return ret, nil
}
func (a *App) register(w http.ResponseWriter, r *http.Request) {
func (a *API) register(w http.ResponseWriter, r *http.Request) {
params, err := parseRegisterPaylaod(r)
if err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@ -189,7 +187,7 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
}
var count int
if err := a.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
if err := a.App.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
HandleError(w, "checking duplicate user", err, http.StatusInternalServerError)
return
}
@ -198,36 +196,23 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
return
}
user, err := operations.CreateUser(a.DB, params.Email, params.Password)
user, err := a.App.CreateUser(params.Email, params.Password)
if err != nil {
HandleError(w, "creating user", err, http.StatusInternalServerError)
return
}
respondWithSession(a.DB, w, user.ID, http.StatusCreated)
a.respondWithSession(a.App.DB, w, user.ID, http.StatusCreated)
// send welcome email
body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
AccountEmail: params.Email,
WebURL: a.WebURL,
})
if err != nil {
log.WithFields(log.Fields{
"email": params.Email,
}).ErrorWrap(err, "executing welcome email template")
return
}
if err := a.EmailBackend.Queue("Welcome to Dnote!", "sung@getdnote.com", []string{params.Email}, "text/plain", body); err != nil {
log.WithFields(log.Fields{
"email": params.Email,
}).ErrorWrap(err, "queueing email")
if err := a.App.SendWelcomeEmail(params.Email); err != nil {
log.ErrorWrap(err, "sending welcome email")
}
}
// respondWithSession makes a HTTP response with the session from the user with the given userID.
// It sets the HTTP-Only cookie for browser clients and also sends a JSON response for non-browser clients.
func respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) {
session, err := operations.CreateSession(db, userID)
func (a *API) respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) {
session, err := a.App.CreateSession(userID)
if err != nil {
HandleError(w, "creating session", nil, http.StatusBadRequest)
return

View file

@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
@ -58,20 +59,35 @@ func assertSessionResp(t *testing.T, res *http.Response) {
func TestRegister(t *testing.T) {
testCases := []struct {
email string
password string
email string
password string
onPremise bool
expectedPro bool
}{
{
email: "alice@example.com",
password: "pass1234",
email: "alice@example.com",
password: "pass1234",
onPremise: false,
expectedPro: false,
},
{
email: "bob@example.com",
password: "Y9EwmjH@Jq6y5a64MSACUoM4w7SAhzvY",
email: "bob@example.com",
password: "Y9EwmjH@Jq6y5a64MSACUoM4w7SAhzvY",
onPremise: false,
expectedPro: false,
},
{
email: "chuck@example.com",
password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
email: "chuck@example.com",
password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
onPremise: false,
expectedPro: false,
},
// on premise
{
email: "dan@example.com",
password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
onPremise: true,
expectedPro: true,
},
}
@ -81,9 +97,10 @@ func TestRegister(t *testing.T) {
// Setup
emailBackend := testutils.MockEmailbackendImplementation{}
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
EmailBackend: &emailBackend,
OnPremise: tc.onPremise,
})
defer server.Close()
@ -105,7 +122,7 @@ func TestRegister(t *testing.T) {
var user database.User
testutils.MustExec(t, testutils.DB.Where("id = ?", account.UserID).First(&user), "finding user")
assert.Equal(t, user.Cloud, false, "Cloud mismatch")
assert.Equal(t, user.Cloud, tc.expectedPro, "Cloud mismatch")
assert.Equal(t, user.StripeCustomerID, "", "StripeCustomerID mismatch")
assert.Equal(t, user.MaxUSN, 0, "MaxUSN mismatch")
@ -125,11 +142,10 @@ func TestRegister(t *testing.T) {
func TestRegisterMissingParams(t *testing.T) {
t.Run("missing email", func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -157,7 +173,7 @@ func TestRegisterMissingParams(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -185,7 +201,7 @@ func TestRegisterDuplicateEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -219,11 +235,10 @@ func TestRegisterDuplicateEmail(t *testing.T) {
func TestSignIn(t *testing.T) {
t.Run("success", func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -250,11 +265,10 @@ func TestSignIn(t *testing.T) {
})
t.Run("wrong password", func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -286,7 +300,7 @@ func TestSignIn(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -318,7 +332,7 @@ func TestSignIn(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -362,7 +376,7 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@ -414,7 +428,7 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
// Setup
server := MustNewServer(t, &App{
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})

View file

@ -26,7 +26,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
@ -51,7 +50,7 @@ func validateCreateBookPayload(p createBookPayload) error {
}
// CreateBook creates a new book
func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
func (a *API) CreateBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@ -71,7 +70,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
}
var bookCount int
err = a.DB.Model(database.Book{}).
err = a.App.DB.Model(database.Book{}).
Where("user_id = ? AND label = ?", user.ID, params.Name).
Count(&bookCount).Error
if err != nil {
@ -83,7 +82,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
return
}
book, err := operations.CreateBook(a.DB, user, a.Clock, params.Name)
book, err := a.App.CreateBook(user, params.Name)
if err != nil {
HandleError(w, "inserting book", err, http.StatusInternalServerError)
}
@ -94,7 +93,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
}
// BooksOptions is a handler for OPTIONS endpoint for notes
func (a *App) BooksOptions(w http.ResponseWriter, r *http.Request) {
func (a *API) BooksOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}
@ -130,7 +129,7 @@ func respondWithBooks(db *gorm.DB, userID int, query url.Values, w http.Response
}
// GetBooks returns books for the user
func (a *App) GetBooks(w http.ResponseWriter, r *http.Request) {
func (a *API) GetBooks(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@ -138,11 +137,11 @@ func (a *App) GetBooks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
respondWithBooks(a.DB, user.ID, query, w)
respondWithBooks(a.App.DB, user.ID, query, w)
}
// GetBook returns a book for the user
func (a *App) GetBook(w http.ResponseWriter, r *http.Request) {
func (a *API) GetBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@ -152,7 +151,7 @@ func (a *App) GetBook(w http.ResponseWriter, r *http.Request) {
bookUUID := vars["bookUUID"]
var book database.Book
conn := a.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book)
conn := a.App.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book)
if conn.RecordNotFound() {
w.WriteHeader(http.StatusNotFound)
@ -177,7 +176,7 @@ type UpdateBookResp struct {
}
// UpdateBook updates a book
func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
func (a *API) UpdateBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@ -186,7 +185,7 @@ func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uuid := vars["bookUUID"]
tx := a.DB.Begin()
tx := a.App.DB.Begin()
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
@ -201,7 +200,7 @@ func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
return
}
book, err = operations.UpdateBook(tx, a.Clock, user, book, params.Name)
book, err = a.App.UpdateBook(tx, user, book, params.Name)
if err != nil {
tx.Rollback()
HandleError(w, "updating a book", err, http.StatusInternalServerError)
@ -222,7 +221,7 @@ type DeleteBookResp struct {
}
// DeleteBook removes a book
func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
func (a *API) DeleteBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@ -231,7 +230,7 @@ func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uuid := vars["bookUUID"]
tx := a.DB.Begin()
tx := a.App.DB.Begin()
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
@ -246,12 +245,12 @@ func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
}
for _, note := range notes {
if _, err := operations.DeleteNote(tx, user, note); err != nil {
if _, err := a.App.DeleteNote(tx, user, note); err != nil {
HandleError(w, "deleting a note", err, http.StatusInternalServerError)
return
}
}
b, err := operations.DeleteBook(tx, user, book)
b, err := a.App.DeleteBook(tx, user, book)
if err != nil {
HandleError(w, "deleting book", err, http.StatusInternalServerError)
return

View file

@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@ -33,13 +34,13 @@ import (
)
func TestGetBooks(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -113,13 +114,13 @@ Clock: clock.NewMock(),
}
func TestGetBooksByName(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -199,13 +200,13 @@ func TestDeleteBook(t *testing.T) {
for _, tc := range testCases {
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -350,13 +351,13 @@ Clock: clock.NewMock(),
}
func TestCreateBook(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -410,13 +411,13 @@ Clock: clock.NewMock(),
}
func TestCreateBookDuplicate(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -490,13 +491,13 @@ func TestUpdateBook(t *testing.T) {
for idx, tc := range testCases {
func() {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()

View file

@ -23,9 +23,9 @@ import (
"fmt"
"net/http"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@ -47,7 +47,7 @@ func validateUpdateNotePayload(p updateNotePayload) bool {
}
// UpdateNote updates note
func (a *App) UpdateNote(w http.ResponseWriter, r *http.Request) {
func (a *API) UpdateNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
@ -70,14 +70,14 @@ func (a *App) UpdateNote(w http.ResponseWriter, r *http.Request) {
}
var note database.Note
if err := a.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(&note).Error; err != nil {
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(&note).Error; err != nil {
HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
note, err = operations.UpdateNote(tx, user, a.Clock, note, &operations.UpdateNoteParams{
note, err = a.App.UpdateNote(tx, user, note, &app.UpdateNoteParams{
BookUUID: params.BookUUID,
Content: params.Content,
Public: params.Public,
@ -114,7 +114,7 @@ type deleteNoteResp struct {
}
// DeleteNote removes note
func (a *App) DeleteNote(w http.ResponseWriter, r *http.Request) {
func (a *API) DeleteNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
@ -125,14 +125,14 @@ func (a *App) DeleteNote(w http.ResponseWriter, r *http.Request) {
}
var note database.Note
if err := a.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(&note).Error; err != nil {
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(&note).Error; err != nil {
HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
tx := a.DB.Begin()
tx := a.App.DB.Begin()
n, err := operations.DeleteNote(tx, user, note)
n, err := a.App.DeleteNote(tx, user, note)
if err != nil {
tx.Rollback()
HandleError(w, "deleting note", err, http.StatusInternalServerError)
@ -169,7 +169,7 @@ type CreateNoteResp struct {
}
// CreateNote creates a note
func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
func (a *API) CreateNote(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -190,12 +190,12 @@ func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
}
var book database.Book
if err := a.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
if err := a.App.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
note, err := operations.CreateNote(a.DB, user, a.Clock, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false)
note, err := a.App.CreateNote(user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false)
if err != nil {
HandleError(w, "creating note", err, http.StatusInternalServerError)
return
@ -212,7 +212,7 @@ func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
}
// NotesOptions is a handler for OPTIONS endpoint for notes
func (a *App) NotesOptions(w http.ResponseWriter, r *http.Request) {
func (a *API) NotesOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}

View file

@ -25,18 +25,19 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestCreateNote(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -235,13 +236,13 @@ func TestUpdateNote(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@ -331,13 +332,13 @@ func TestDeleteNote(t *testing.T) {
for _, tc := range testCases {
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) {
defer testutils.ClearData()
// Setup
server := MustNewServer(t, &App{
Clock: clock.NewMock(),
server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()

View file

@ -120,13 +120,13 @@ func (e *queryParamError) Error() string {
return fmt.Sprintf("invalid query param %s=%s. %s", e.key, e.value, e.message)
}
func (a *App) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment, error) {
func (a *API) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment, error) {
var notes []database.Note
if err := a.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&notes).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&notes).Error; err != nil {
return SyncFragment{}, nil
}
var books []database.Book
if err := a.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&books).Error; err != nil {
if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&books).Error; err != nil {
return SyncFragment{}, nil
}
@ -191,7 +191,7 @@ func (a *App) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment
ret := SyncFragment{
FragMaxUSN: fragMaxUSN,
UserMaxUSN: userMaxUSN,
CurrentTime: a.Clock.Now().Unix(),
CurrentTime: a.App.Clock.Now().Unix(),
Notes: fragNotes,
Books: fragBooks,
ExpungedNotes: fragExpungedNotes,
@ -247,7 +247,7 @@ type GetSyncFragmentResp struct {
}
// GetSyncFragment responds with a sync fragment
func (a *App) GetSyncFragment(w http.ResponseWriter, r *http.Request) {
func (a *API) GetSyncFragment(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -280,7 +280,7 @@ type GetSyncStateResp struct {
}
// GetSyncState responds with a sync fragment
func (a *App) GetSyncState(w http.ResponseWriter, r *http.Request) {
func (a *API) GetSyncState(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@ -291,7 +291,7 @@ func (a *App) GetSyncState(w http.ResponseWriter, r *http.Request) {
FullSyncBefore: fullSyncBefore,
MaxUSN: user.MaxUSN,
// TODO: exposing server time means we probably shouldn't seed random generator with time?
CurrentTime: a.Clock.Now().Unix(),
CurrentTime: a.App.Clock.Now().Unix(),
}
log.WithFields(log.Fields{

View file

@ -1,3 +1,21 @@
/* 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 job
import (

View file

@ -1,3 +1,21 @@
/* 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 repetition
import (

View file

@ -1,3 +1,21 @@
/* 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 (
@ -10,6 +28,9 @@ import (
"gopkg.in/gomail.v2"
)
// ErrSMTPNotConfigured is an error indicating that SMTP is not configured
var ErrSMTPNotConfigured = errors.New("SMTP is not configured")
// Backend is an interface for sending emails.
type Backend interface {
Queue(subject, from string, to []string, contentType, body string) error
@ -27,18 +48,35 @@ type dialerParams struct {
Password string
}
func validateSMTPConfig() bool {
port := os.Getenv("SmtpPort")
host := os.Getenv("SmtpHost")
username := os.Getenv("SmtpUsername")
password := os.Getenv("SmtpPassword")
return port != "" && host != "" && username != "" && password != ""
}
func getSMTPParams() (*dialerParams, error) {
portStr := os.Getenv("SmtpPort")
port, err := strconv.Atoi(portStr)
portEnv := os.Getenv("SmtpPort")
hostEnv := os.Getenv("SmtpHost")
usernameEnv := os.Getenv("SmtpUsername")
passwordEnv := os.Getenv("SmtpPassword")
if portEnv != "" && hostEnv != "" && usernameEnv != "" && passwordEnv != "" {
return nil, ErrSMTPNotConfigured
}
port, err := strconv.Atoi(portEnv)
if err != nil {
return nil, errors.Wrap(err, "parsing SMTP port")
}
p := &dialerParams{
Host: os.Getenv("SmtpHost"),
Host: hostEnv,
Port: port,
Username: os.Getenv("SmtpUsername"),
Password: os.Getenv("SmtpPassword"),
Username: usernameEnv,
Password: passwordEnv,
}
return p, nil

View file

@ -26,6 +26,7 @@ import (
"os"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/dbconn"
"github.com/dnote/dnote/pkg/server/handlers"
@ -55,11 +56,11 @@ func mustFind(box *packr.Box, path string) []byte {
return b
}
func initContext(db *gorm.DB) web.Context {
func initContext(a *app.App) web.Context {
staticBox := packr.New("static", "../../web/public/static")
return web.Context{
DB: db,
App: a,
IndexHTML: mustFind(rootBox, "index.html"),
RobotsTxt: mustFind(rootBox, "robots.txt"),
ServiceWorkerJs: mustFind(rootBox, "service-worker.js"),
@ -67,13 +68,14 @@ func initContext(db *gorm.DB) web.Context {
}
}
func initServer(app handlers.App) (*http.ServeMux, error) {
apiRouter, err := handlers.NewRouter(&app)
func initServer(a app.App) (*http.ServeMux, error) {
api := handlers.API{App: &a}
apiRouter, err := api.NewRouter()
if err != nil {
return nil, errors.Wrap(err, "initializing router")
}
webCtx := initContext(app.DB)
webCtx := initContext(&a)
webHandlers, err := web.Init(webCtx)
if err != nil {
return nil, errors.Wrap(err, "initializing web handlers")
@ -110,10 +112,10 @@ func initDB() *gorm.DB {
return db
}
func initApp() handlers.App {
func initApp() app.App {
db := initDB()
return handlers.App{
return app.App{
DB: db,
Clock: clock.New(),
StripeAPIBackend: nil,
@ -123,7 +125,7 @@ func initApp() handlers.App {
}
}
func runJob(a handlers.App) error {
func runJob(a app.App) error {
jobRunner, err := job.NewRunner(a.DB, a.Clock, a.EmailTemplates, a.EmailBackend, a.WebURL)
if err != nil {
return errors.Wrap(err, "getting a job runner")

View file

@ -1,17 +0,0 @@
package operations
import (
"os"
"testing"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestMain(m *testing.M) {
testutils.InitTestDB()
code := m.Run()
testutils.ClearData()
os.Exit(code)
}

View file

@ -24,7 +24,7 @@ import (
"net/http"
"regexp"
"github.com/jinzhu/gorm"
"github.com/dnote/dnote/pkg/server/app"
"github.com/pkg/errors"
)
@ -37,14 +37,15 @@ var templateNoteMetaTags = "note_metatags"
// AppShell represents the application in HTML
type AppShell struct {
T *template.Template
App *app.App
T *template.Template
}
// ErrNotFound is an error indicating that a resource was not found
var ErrNotFound = errors.New("not found")
// NewAppShell parses the templates for the application
func NewAppShell(content []byte) (AppShell, error) {
func NewAppShell(a *app.App, content []byte) (AppShell, error) {
t, err := template.New(templateIndex).Parse(string(content))
if err != nil {
return AppShell{}, errors.Wrap(err, "parsing the index template")
@ -55,12 +56,12 @@ func NewAppShell(content []byte) (AppShell, error) {
return AppShell{}, errors.Wrap(err, "parsing the note meta tags template")
}
return AppShell{t}, nil
return AppShell{App: a, T: t}, nil
}
// Execute executes the index template
func (a AppShell) Execute(r *http.Request, db *gorm.DB) ([]byte, error) {
data, err := a.getData(db, r)
func (a AppShell) Execute(r *http.Request) ([]byte, error) {
data, err := a.getData(r)
if err != nil {
return nil, errors.Wrap(err, "getting data")
}
@ -73,11 +74,11 @@ func (a AppShell) Execute(r *http.Request, db *gorm.DB) ([]byte, error) {
return buf.Bytes(), nil
}
func (a AppShell) getData(db *gorm.DB, r *http.Request) (tmplData, error) {
func (a AppShell) getData(r *http.Request) (tmplData, error) {
path := r.URL.Path
if ok, params := matchPath(path, notesPathRegex); ok {
p, err := a.newNotePage(db, r, params[0])
p, err := a.newNotePage(r, params[0])
if err != nil {
return tmplData{}, errors.Wrap(err, "instantiating note page")
}

View file

@ -24,14 +24,17 @@ import (
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
)
func TestAppShellExecute(t *testing.T) {
testApp := app.NewTest(nil)
t.Run("home", func(t *testing.T) {
a, err := NewAppShell([]byte("<head><title>{{ .Title }}</title>{{ .MetaTags }}</head>"))
a, err := NewAppShell(&testApp, []byte("<head><title>{{ .Title }}</title>{{ .MetaTags }}</head>"))
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}
@ -41,7 +44,7 @@ func TestAppShellExecute(t *testing.T) {
t.Fatal(errors.Wrap(err, "preparing request"))
}
b, err := a.Execute(r, testutils.DB)
b, err := a.Execute(r)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
@ -66,7 +69,7 @@ func TestAppShellExecute(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing note")
a, err := NewAppShell([]byte("{{ .MetaTags }}"))
a, err := NewAppShell(&testApp, []byte("{{ .MetaTags }}"))
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}
@ -77,7 +80,7 @@ func TestAppShellExecute(t *testing.T) {
t.Fatal(errors.Wrap(err, "preparing request"))
}
b, err := a.Execute(r, testutils.DB)
b, err := a.Execute(r)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}

View file

@ -29,8 +29,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/handlers"
"github.com/dnote/dnote/pkg/server/operations"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
@ -52,13 +50,13 @@ type notePage struct {
T *template.Template
}
func (a AppShell) newNotePage(db *gorm.DB, r *http.Request, noteUUID string) (notePage, error) {
user, _, err := handlers.AuthWithSession(db, r, nil)
func (a AppShell) newNotePage(r *http.Request, noteUUID string) (notePage, error) {
user, _, err := handlers.AuthWithSession(a.App.DB, r, nil)
if err != nil {
return notePage{}, errors.Wrap(err, "authenticating with session")
}
note, ok, err := operations.GetNote(db, noteUUID, user)
note, ok, err := a.App.GetNote(noteUUID, user)
if !ok {
return notePage{}, ErrNotFound

View file

@ -24,6 +24,7 @@ import (
"time"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/pkg/errors"
)
@ -38,7 +39,8 @@ func TestDefaultPageGetData(t *testing.T) {
}
func TestNotePageGetData(t *testing.T) {
a, err := NewAppShell(nil)
testApp := app.NewTest(nil)
a, err := NewAppShell(&testApp, nil)
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}

View file

@ -1,3 +1,21 @@
/* 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 tmpl
import (

View file

@ -22,15 +22,13 @@ package web
import (
"net/http"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/handlers"
"github.com/dnote/dnote/pkg/server/tmpl"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
var (
// ErrEmptyDB is an error for missing database connection in the context
ErrEmptyDB = errors.New("No database connection was provided")
// ErrEmptyIndexHTML is an error for missing index.html content in the context
ErrEmptyIndexHTML = errors.New("No index.html content was provided")
// ErrEmptyRobotsTxt is an error for missing robots.txt content in the context
@ -43,7 +41,7 @@ var (
// Context contains contents of web assets
type Context struct {
DB *gorm.DB
App *app.App
IndexHTML []byte
RobotsTxt []byte
ServiceWorkerJs []byte
@ -59,8 +57,8 @@ type Handlers struct {
}
func validateContext(c Context) error {
if c.DB == nil {
return ErrEmptyDB
if err := c.App.Validate(); err != nil {
return errors.Wrap(err, "validating app")
}
if c.IndexHTML == nil {
return ErrEmptyIndexHTML
@ -94,7 +92,7 @@ func Init(c Context) (Handlers, error) {
// getRootHandler returns an HTTP handler that serves the app shell
func getRootHandler(c Context) http.HandlerFunc {
appShell, err := tmpl.NewAppShell(c.IndexHTML)
appShell, err := tmpl.NewAppShell(c.App, c.IndexHTML)
if err != nil {
panic(errors.Wrap(err, "initializing app shell"))
}
@ -103,7 +101,7 @@ func getRootHandler(c Context) http.HandlerFunc {
// index.html must not be cached
w.Header().Set("Cache-Control", "no-cache")
buf, err := appShell.Execute(r, c.DB)
buf, err := appShell.Execute(r)
if err != nil {
if errors.Cause(err) == tmpl.ErrNotFound {
handlers.RespondNotFound(w)

View file

@ -1,3 +1,21 @@
/* 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 web
import (
@ -6,7 +24,7 @@ import (
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/jinzhu/gorm"
"github.com/dnote/dnote/pkg/server/app"
"github.com/pkg/errors"
)
@ -16,13 +34,17 @@ func TestInit(t *testing.T) {
mockServiceWorkerJs := []byte("function() {}")
mockStaticFileSystem := http.Dir(".")
testApp := app.NewTest(nil)
testAppNoDB := app.NewTest(nil)
testAppNoDB.DB = nil
testCases := []struct {
ctx Context
expectedErr error
}{
{
ctx: Context{
DB: &gorm.DB{},
App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
@ -32,17 +54,17 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
DB: nil,
App: &testAppNoDB,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
StaticFileSystem: mockStaticFileSystem,
},
expectedErr: ErrEmptyDB,
expectedErr: app.ErrEmptyDB,
},
{
ctx: Context{
DB: &gorm.DB{},
App: &testApp,
IndexHTML: nil,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
@ -52,7 +74,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
DB: &gorm.DB{},
App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: nil,
ServiceWorkerJs: mockServiceWorkerJs,
@ -62,7 +84,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
DB: &gorm.DB{},
App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: nil,
@ -72,7 +94,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
DB: &gorm.DB{},
App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,

View file

@ -0,0 +1,35 @@
/* 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 web
import (
"os"
"testing"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestMain(m *testing.M) {
testutils.InitTestDB()
code := m.Run()
testutils.ClearData()
os.Exit(code)
}

View file

@ -3,16 +3,16 @@
* 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
* it under the terms of the GNU 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.
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/

View file

@ -56,9 +56,10 @@ agpl="/* Copyright (C) 2019 Monomax Software Pty Ltd
*/"
dir=$(dirname "${BASH_SOURCE[0]}")
pkgPath="$dir/pkg"
serverPath="$dir/pkg/server"
browserPath="$dir/browser"
basedir="$dir/.."
pkgPath="$basedir/pkg"
serverPath="$basedir/pkg/server"
browserPath="$basedir/browser"
gplFiles=$(find "$pkgPath" "$browserPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "$serverPath/*")
@ -67,8 +68,8 @@ for file in $gplFiles; do
add_notice "$file" "$gpl"
done
webPath="$dir/web"
jslibPath="$dir/jslib/src"
webPath="$basedir/web"
jslibPath="$basedir/jslib/src"
agplFiles=$(find "$serverPath" "$webPath" "$jslibPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "**/dist/*")
for file in $agplFiles; do

View file

@ -1,3 +1,21 @@
/* 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/>.
*/
module.exports = {
preset: 'ts-jest',
collectCoverageFrom: ['./src/**/*.ts']

View file

@ -1,3 +1,21 @@
/* 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/>.
*/
import React, { Fragment, useState, useEffect } from 'react';
import classnames from 'classnames';

View file

@ -1,3 +1,21 @@
/* 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/>.
*/
import React from 'react';
import styles from './Subscription.scss';

View file

@ -1,3 +1,21 @@
/* 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/>.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import classnames from 'classnames';