mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
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:
parent
295dcefff1
commit
891be61031
61 changed files with 1408 additions and 567 deletions
72
pkg/server/app/app.go
Normal file
72
pkg/server/app/app.go
Normal 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
113
pkg/server/app/app_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
137
pkg/server/app/email.go
Normal 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
|
||||
}
|
||||
132
pkg/server/app/email_test.go
Normal file
132
pkg/server/app/email_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package operations
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
35
pkg/server/app/main_test.go
Normal file
35
pkg/server/app/main_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(¬e), 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"))
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
64
pkg/server/app/testutils.go
Normal file
64
pkg/server/app/testutils.go
Normal 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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
66
pkg/server/app/users_test.go
Normal file
66
pkg/server/app/users_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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(¶ms); 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(¶ms); 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(¶ms); 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(¬es).Error; err != nil {
|
||||
if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
|
||||
HandleError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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(¬es).Error; err != nil {
|
||||
if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
|
||||
HandleError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¶ms); 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(¶ms)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(¬e).Error; err != nil {
|
||||
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).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(¬e).Error; err != nil {
|
||||
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(¬es).Error; err != nil {
|
||||
if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(¬es).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{
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
35
pkg/server/web/main_test.go
Normal file
35
pkg/server/web/main_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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/>.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue