diff --git a/CHANGELOG.md b/CHANGELOG.md
index 851a4f8c..84be0649 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,9 @@ The following log documentes the history of the server project.
### [Unreleased]
-N/A
+#### Added
+
+- Share notes (#300)
### 0.2.1 - 2019-11-04
diff --git a/Vagrantfile b/Vagrantfile
index 67ce9536..08f6a390 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -2,7 +2,9 @@
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
- config.vm.synced_folder '.', '/go/src/github.com/dnote/dnote', type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"]
+ config.vm.synced_folder '.', '/go/src/github.com/dnote/dnote', type: "rsync", create: true,
+ rsync__args: ["--verbose", "--archive", "--delete", "-z"],
+ rsync__exclude: [".git/", ".vagrant/", "web/public"]
config.vm.network "forwarded_port", guest: 3000, host: 3000
config.vm.network "forwarded_port", guest: 8080, host: 8080
config.vm.network "forwarded_port", guest: 5432, host: 5433
diff --git a/pkg/server/.gitignore b/pkg/server/.gitignore
index fa49c96a..0494e979 100644
--- a/pkg/server/.gitignore
+++ b/pkg/server/.gitignore
@@ -6,3 +6,11 @@ test-dnote
/dist
/build
server
+
+# Elastic Beanstalk Files
+/tmp
+application.zip
+test-api
+/dump
+api
+/build
diff --git a/pkg/server/api/.gitignore b/pkg/server/api/.gitignore
deleted file mode 100644
index 78d0d862..00000000
--- a/pkg/server/api/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-# Elastic Beanstalk Files
-/tmp
-application.zip
-test-api
-/dump
-api
-/build
diff --git a/pkg/server/api/README.md b/pkg/server/api/README.md
deleted file mode 100644
index 44ae40ac..00000000
--- a/pkg/server/api/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# API
-
-Dnote API.
-
-## Development
-
-* Ensure `timezone = 'UTC'` in postgres setting (`postgresql.conf`)
-
diff --git a/pkg/server/api/handlers/notes_test.go b/pkg/server/api/handlers/notes_test.go
deleted file mode 100644
index 0f18a6e8..00000000
--- a/pkg/server/api/handlers/notes_test.go
+++ /dev/null
@@ -1,336 +0,0 @@
-/* Copyright (C) 2019 Monomax Software Pty Ltd
- *
- * This file is part of Dnote.
- *
- * Dnote is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Dnote is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Dnote. If not, see .
- */
-
-package handlers
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "testing"
- "time"
-
- "github.com/dnote/dnote/pkg/assert"
- "github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/presenters"
- "github.com/dnote/dnote/pkg/server/database"
- "github.com/dnote/dnote/pkg/server/testutils"
- "github.com/pkg/errors"
-)
-
-func init() {
- testutils.InitTestDB()
-}
-
-func TestGetNotes(t *testing.T) {
- defer testutils.ClearData()
- db := database.DBConn
-
- // Setup
- server := MustNewServer(t, &App{
- Clock: clock.NewMock(),
- })
- defer server.Close()
-
- user := testutils.SetupUserData()
- anotherUser := testutils.SetupUserData()
-
- b1 := database.Book{
- UserID: user.ID,
- Label: "js",
- }
- testutils.MustExec(t, db.Save(&b1), "preparing b1")
- b2 := database.Book{
- UserID: user.ID,
- Label: "css",
- }
- testutils.MustExec(t, db.Save(&b2), "preparing b2")
- b3 := database.Book{
- UserID: anotherUser.ID,
- Label: "css",
- }
- testutils.MustExec(t, db.Save(&b3), "preparing b3")
-
- n1 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "n1 content",
- USN: 11,
- Deleted: false,
- AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n1), "preparing n1")
- n2 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "n2 content",
- USN: 14,
- Deleted: false,
- AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n2), "preparing n2")
- n3 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "n3 content",
- USN: 17,
- Deleted: false,
- AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n3), "preparing n3")
- n4 := database.Note{
- UserID: user.ID,
- BookUUID: b2.UUID,
- Body: "n4 content",
- USN: 18,
- Deleted: false,
- AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n4), "preparing n4")
- n5 := database.Note{
- UserID: anotherUser.ID,
- BookUUID: b3.UUID,
- Body: "n5 content",
- USN: 19,
- Deleted: false,
- AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n5), "preparing n5")
- n6 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "",
- USN: 11,
- Deleted: true,
- AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n6), "preparing n6")
-
- // Execute
- req := testutils.MakeReq(server, "GET", "/notes?year=2018&month=8", "")
- res := testutils.HTTPAuthDo(t, req, user)
-
- // Test
- assert.StatusCodeEquals(t, res, http.StatusOK, "")
-
- var payload GetNotesResponse
- if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
- t.Fatal(errors.Wrap(err, "decoding payload"))
- }
-
- var n2Record, n1Record database.Note
- testutils.MustExec(t, db.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record")
- testutils.MustExec(t, db.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
-
- expected := GetNotesResponse{
- Notes: []presenters.Note{
- {
- CreatedAt: n2Record.CreatedAt,
- UpdatedAt: n2Record.UpdatedAt,
- UUID: n2Record.UUID,
- Body: n2Record.Body,
- AddedOn: n2Record.AddedOn,
- USN: n2Record.USN,
- Book: presenters.NoteBook{
- UUID: b1.UUID,
- Label: b1.Label,
- },
- User: presenters.NoteUser{
- Name: user.Name,
- UUID: user.UUID,
- },
- },
- {
- CreatedAt: n1Record.CreatedAt,
- UpdatedAt: n1Record.UpdatedAt,
- UUID: n1Record.UUID,
- Body: n1Record.Body,
- AddedOn: n1Record.AddedOn,
- USN: n1Record.USN,
- Book: presenters.NoteBook{
- UUID: b1.UUID,
- Label: b1.Label,
- },
- User: presenters.NoteUser{
- Name: user.Name,
- UUID: user.UUID,
- },
- },
- },
- Total: 2,
- }
-
- assert.DeepEqual(t,payload, expected, "payload mismatch")
-}
-
-func TestGetNote(t *testing.T) {
- defer testutils.ClearData()
- db := database.DBConn
-
- // Setup
- server := MustNewServer(t, &App{
- Clock: clock.NewMock(),
- })
- defer server.Close()
-
- user := testutils.SetupUserData()
-
- b1 := database.Book{
- UserID: user.ID,
- Label: "js",
- }
- testutils.MustExec(t, db.Save(&b1), "preparing b1")
- b2 := database.Book{
- UserID: user.ID,
- Label: "css",
- }
- testutils.MustExec(t, db.Save(&b2), "preparing b2")
-
- n1 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "n1 content",
- USN: 1123,
- AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
- Public: true,
- }
- testutils.MustExec(t, db.Save(&n1), "preparing n1")
- n2 := database.Note{
- UserID: user.ID,
- BookUUID: b1.UUID,
- Body: "n2 content",
- USN: 1888,
- AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
- }
- testutils.MustExec(t, db.Save(&n2), "preparing n2")
-
- // Execute
- url := fmt.Sprintf("/notes/%s", n1.UUID)
- req := testutils.MakeReq(server, "GET", url, "")
- res := testutils.HTTPAuthDo(t, req, user)
-
- // Test
- assert.StatusCodeEquals(t, res, http.StatusOK, "")
-
- var payload presenters.Note
- if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
- t.Fatal(errors.Wrap(err, "decoding payload"))
- }
-
- var n1Record database.Note
- testutils.MustExec(t, db.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
-
- expected := presenters.Note{
- UUID: n1Record.UUID,
- CreatedAt: n1Record.CreatedAt,
- UpdatedAt: n1Record.UpdatedAt,
- Body: n1Record.Body,
- AddedOn: n1Record.AddedOn,
- Public: n1Record.Public,
- USN: n1Record.USN,
- Book: presenters.NoteBook{
- UUID: b1.UUID,
- Label: b1.Label,
- },
- User: presenters.NoteUser{
- Name: user.Name,
- UUID: user.UUID,
- },
- }
-
- assert.DeepEqual(t,payload, expected, "payload mismatch")
-}
-
-// TODO: finish the test after implementing note sharing
-// func TestGetNote_guestAccessPrivate(t *testing.T) {
-// defer testutils.ClearData()
-// db := database.DBConn
-//
-// // Setup
-// server := httptest.NewServer(NewRouter(&App{
-// Clock: clock.NewMock(),
-// }))
-// defer server.Close()
-//
-// user := testutils.SetupUserData()
-//
-// b1 := database.Book{
-// UUID: "b1-uuid",
-// UserID: user.ID,
-// Label: "js",
-// }
-// testutils.MustExec(t, db.Save(&b1), "preparing b1")
-//
-// n1 := database.Note{
-// UserID: user.ID,
-// BookUUID: b1.UUID,
-// Body: "n1 content",
-// AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
-// Public: false,
-// }
-// testutils.MustExec(t, db.Save(&n1), "preparing n1")
-//
-// // Execute
-// url := fmt.Sprintf("/notes/%s", n1.UUID)
-// req := testutils.MakeReq(server, "GET", url, "")
-//
-// res := testutils.HTTPDo(t, req)
-//
-// // Test
-// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
-// }
-
-// func TestGetNote_nonOwnerAccessPrivate(t *testing.T) {
-// defer testutils.ClearData()
-// db := database.DBConn
-//
-// // Setup
-// server := httptest.NewServer(NewRouter(&App{
-// Clock: clock.NewMock(),
-// }))
-// defer server.Close()
-//
-// owner := testutils.SetupUserData()
-//
-// nonOwner := testutils.SetupUserData()
-// testutils.MustExec(t, db.Model(&nonOwner).Update("api_key", "non-owner-api-key"), "preparing user max_usn")
-//
-// b1 := database.Book{
-// UUID: "b1-uuid",
-// UserID: owner.ID,
-// Label: "js",
-// }
-// testutils.MustExec(t, db.Save(&b1), "preparing b1")
-//
-// n1 := database.Note{
-// UserID: owner.ID,
-// BookUUID: b1.UUID,
-// Body: "n1 content",
-// AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
-// Public: false,
-// }
-// testutils.MustExec(t, db.Save(&n1), "preparing n1")
-//
-// // Execute
-// url := fmt.Sprintf("/notes/%s", n1.UUID)
-// req := testutils.MakeReq(server, "GET", url, "")
-// res := testutils.HTTPAuthDo(t, req, nonOwner)
-//
-// // Test
-// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
-// }
diff --git a/pkg/server/api/scripts/build.sh b/pkg/server/api/scripts/build.sh
deleted file mode 100755
index 709e44ac..00000000
--- a/pkg/server/api/scripts/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-set -eux
-
-basePath="$GOPATH/src/github.com/dnote/dnote/pkg/server/api"
-
-cd "$basePath"
-GOOS=linux GOARCH=amd64 go build -o "$basePath/build/api" main.go
diff --git a/pkg/server/api/scripts/setup.sh b/pkg/server/api/scripts/setup.sh
deleted file mode 100755
index a1230d71..00000000
--- a/pkg/server/api/scripts/setup.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-# setup.sh installs programs and depedencies necessary to run the project locally
-# usage: ./setup.sh
-set -eux
-
-curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
-go get github.com/githubnemo/CompileDaemon
-dep ensure
-
diff --git a/pkg/server/api/crypt/crypt.go b/pkg/server/crypt/crypt.go
similarity index 100%
rename from pkg/server/api/crypt/crypt.go
rename to pkg/server/crypt/crypt.go
diff --git a/pkg/server/database/notes.go b/pkg/server/database/notes.go
new file mode 100644
index 00000000..fa3cb6a7
--- /dev/null
+++ b/pkg/server/database/notes.go
@@ -0,0 +1,28 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package database
+
+import (
+ "github.com/jinzhu/gorm"
+)
+
+// PreloadNote preloads the associations for a notes for the given query
+func PreloadNote(conn *gorm.DB) *gorm.DB {
+ return conn.Preload("Book").Preload("User")
+}
diff --git a/pkg/server/api/handlers/auth.go b/pkg/server/handlers/auth.go
similarity index 85%
rename from pkg/server/api/handlers/auth.go
rename to pkg/server/handlers/auth.go
index f2397fb2..32ed6da4 100644
--- a/pkg/server/api/handlers/auth.go
+++ b/pkg/server/handlers/auth.go
@@ -24,8 +24,8 @@ import (
"net/http"
"time"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/operations"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/pkg/errors"
@@ -56,7 +56,7 @@ func makeSession(user database.User, account database.Account) Session {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -64,7 +64,7 @@ func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
var account database.Account
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -106,7 +106,7 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
return
}
if err := conn.Error; err != nil {
- handleError(w, errors.Wrap(err, "finding account").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "finding account").Error(), nil, http.StatusInternalServerError)
return
}
@@ -117,7 +117,7 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
resetToken, err := generateResetToken()
if err != nil {
- handleError(w, errors.Wrap(err, "generating token").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "generating token").Error(), nil, http.StatusInternalServerError)
return
}
@@ -128,7 +128,7 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
}
if err := db.Save(&token).Error; err != nil {
- handleError(w, errors.Wrap(err, "saving token").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "saving token").Error(), nil, http.StatusInternalServerError)
return
}
@@ -140,12 +140,12 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
}
email := mailer.NewEmail("noreply@getdnote.com", []string{params.Email}, subject)
if err := email.ParseTemplate(mailer.EmailTypeResetPassword, data); err != nil {
- handleError(w, errors.Wrap(err, "parsing template").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "parsing template").Error(), nil, http.StatusInternalServerError)
return
}
if err := email.Send(); err != nil {
- handleError(w, errors.Wrap(err, "sending email").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "sending email").Error(), nil, http.StatusInternalServerError)
return
}
}
@@ -171,7 +171,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
if err := conn.Error; err != nil {
- handleError(w, errors.Wrap(err, "finding token").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "finding token").Error(), nil, http.StatusInternalServerError)
return
}
@@ -191,25 +191,25 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost)
if err != nil {
tx.Rollback()
- handleError(w, errors.Wrap(err, "hashing password").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "hashing password").Error(), nil, http.StatusInternalServerError)
return
}
var account database.Account
if err := db.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
tx.Rollback()
- handleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
}
if err := tx.Model(&account).Update("password", string(hashedPassword)).Error; err != nil {
tx.Rollback()
- handleError(w, errors.Wrap(err, "updating password").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "updating password").Error(), nil, http.StatusInternalServerError)
return
}
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
tx.Rollback()
- handleError(w, errors.Wrap(err, "updating password reset token").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "updating password reset token").Error(), nil, http.StatusInternalServerError)
return
}
@@ -217,7 +217,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
var user database.User
if err := db.Where("id = ?", account.UserID).First(&user).Error; err != nil {
- handleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
+ HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/auth_test.go b/pkg/server/handlers/auth_test.go
similarity index 100%
rename from pkg/server/api/handlers/auth_test.go
rename to pkg/server/handlers/auth_test.go
diff --git a/pkg/server/api/handlers/classic.go b/pkg/server/handlers/classic.go
similarity index 84%
rename from pkg/server/api/handlers/classic.go
rename to pkg/server/handlers/classic.go
index ed85a766..25aaaaa7 100644
--- a/pkg/server/api/handlers/classic.go
+++ b/pkg/server/handlers/classic.go
@@ -22,10 +22,10 @@ import (
"encoding/json"
"net/http"
- "github.com/dnote/dnote/pkg/server/api/crypt"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/operations"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/crypt"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/operations"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/pkg/errors"
@@ -35,7 +35,7 @@ import (
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -43,7 +43,7 @@ func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
var account database.Account
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -55,7 +55,7 @@ func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
"client_kdf_iteration": 0,
"server_kdf_iteration": 0,
}).Error; err != nil {
- handleError(w, "updating account", err, http.StatusInternalServerError)
+ HandleError(w, "updating account", err, http.StatusInternalServerError)
return
}
}
@@ -78,7 +78,7 @@ func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
var account database.Account
conn := db.Where("email = ?", email).First(&account)
if !conn.RecordNotFound() && conn.Error != nil {
- handleError(w, "getting user", conn.Error, http.StatusInternalServerError)
+ HandleError(w, "getting user", conn.Error, http.StatusInternalServerError)
return
}
@@ -95,7 +95,7 @@ func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
- handleError(w, "encoding response", nil, http.StatusInternalServerError)
+ HandleError(w, "encoding response", nil, http.StatusInternalServerError)
return
}
}
@@ -110,7 +110,7 @@ func (a *App) 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)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
@@ -125,7 +125,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
} else if err := conn.Error; err != nil {
- handleError(w, "getting user", err, http.StatusInternalServerError)
+ HandleError(w, "getting user", err, http.StatusInternalServerError)
return
}
@@ -140,7 +140,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
session, err := operations.CreateSession(db, account.UserID)
if err != nil {
- handleError(w, "creating session", nil, http.StatusBadRequest)
+ HandleError(w, "creating session", nil, http.StatusBadRequest)
return
}
@@ -157,7 +157,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
- handleError(w, "encoding response", err, http.StatusInternalServerError)
+ HandleError(w, "encoding response", err, http.StatusInternalServerError)
return
}
}
@@ -165,7 +165,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -173,7 +173,7 @@ func (a *App) classicGetMe(w http.ResponseWriter, r *http.Request) {
var account database.Account
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -225,7 +225,7 @@ type classicSetPasswordPayload struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -233,13 +233,13 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
var params classicSetPasswordPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
var account database.Account
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
- handleError(w, "getting user", nil, http.StatusInternalServerError)
+ HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
@@ -260,14 +260,14 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var notes []database.Note
db := database.DBConn
if err := db.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
- handleError(w, "finding notes", err, http.StatusInternalServerError)
+ HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/classic_test.go b/pkg/server/handlers/classic_test.go
similarity index 100%
rename from pkg/server/api/handlers/classic_test.go
rename to pkg/server/handlers/classic_test.go
diff --git a/pkg/server/api/handlers/health.go b/pkg/server/handlers/health.go
similarity index 100%
rename from pkg/server/api/handlers/health.go
rename to pkg/server/handlers/health.go
diff --git a/pkg/server/api/handlers/health_test.go b/pkg/server/handlers/health_test.go
similarity index 100%
rename from pkg/server/api/handlers/health_test.go
rename to pkg/server/handlers/health_test.go
diff --git a/pkg/server/api/handlers/helpers.go b/pkg/server/handlers/helpers.go
similarity index 83%
rename from pkg/server/api/handlers/helpers.go
rename to pkg/server/handlers/helpers.go
index b097f9f6..1fd4dcb7 100644
--- a/pkg/server/api/handlers/helpers.go
+++ b/pkg/server/handlers/helpers.go
@@ -108,8 +108,8 @@ func getClientType(origin string) string {
return "web"
}
-// handleError logs the error and responds with the given status code with a generic status text
-func handleError(w http.ResponseWriter, msg string, err error, statusCode int) {
+// HandleError logs the error and responds with the given status code with a generic status text
+func HandleError(w http.ResponseWriter, msg string, err error, statusCode int) {
var message string
if err == nil {
message = msg
@@ -131,7 +131,7 @@ func respondJSON(w http.ResponseWriter, statusCode int, payload interface{}) {
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(payload); err != nil {
- handleError(w, "encoding response", err, http.StatusInternalServerError)
+ HandleError(w, "encoding response", err, http.StatusInternalServerError)
}
}
@@ -140,3 +140,18 @@ func (a *App) notSupported(w http.ResponseWriter, r *http.Request) {
http.Error(w, "API version is not supported. Please upgrade your client.", http.StatusGone)
return
}
+
+func respondForbidden(w http.ResponseWriter) {
+ http.Error(w, "forbidden", http.StatusForbidden)
+}
+
+func respondUnauthorized(w http.ResponseWriter) {
+ unsetSessionCookie(w)
+ w.Header().Add("WWW-Authenticate", `Bearer realm="Dnote Pro", charset="UTF-8"`)
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+}
+
+// RespondNotFound responds with not found
+func RespondNotFound(w http.ResponseWriter) {
+ http.Error(w, "not found", http.StatusNotFound)
+}
diff --git a/pkg/server/api/handlers/limit.go b/pkg/server/handlers/limit.go
similarity index 100%
rename from pkg/server/api/handlers/limit.go
rename to pkg/server/handlers/limit.go
diff --git a/pkg/server/api/handlers/notes.go b/pkg/server/handlers/notes.go
similarity index 84%
rename from pkg/server/api/handlers/notes.go
rename to pkg/server/handlers/notes.go
index 9f946829..c8ac1335 100644
--- a/pkg/server/api/handlers/notes.go
+++ b/pkg/server/handlers/notes.go
@@ -26,9 +26,10 @@ import (
"strings"
"time"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/presenters"
"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"
"github.com/pkg/errors"
@@ -85,7 +86,7 @@ func parseSearchQuery(q url.Values) string {
return escapeSearchQuery(searchStr)
}
-func getNoteBaseQuery(noteUUID string, userID int, search string) *gorm.DB {
+func getNoteBaseQuery(noteUUID string, search string) *gorm.DB {
db := database.DBConn
var conn *gorm.DB
@@ -95,32 +96,28 @@ func getNoteBaseQuery(noteUUID string, userID int, search string) *gorm.DB {
conn = db
}
- conn = conn.Where("notes.uuid = ? AND notes.user_id = ?", noteUUID, userID)
+ conn = conn.Where("notes.uuid = ? AND deleted = ?", noteUUID, false)
return conn
}
func (a *App) getNote(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)
+ user, _, err := AuthWithSession(r, nil)
+ if err != nil {
+ HandleError(w, "authenticating", err, http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
- search := parseSearchQuery(r.URL.Query())
- var note database.Note
- conn := getNoteBaseQuery(noteUUID, user.ID, search)
- conn = preloadNote(conn)
- conn.Find(¬e)
-
- if conn.RecordNotFound() {
- http.Error(w, "not found", http.StatusNotFound)
+ note, ok, err := operations.GetNote(noteUUID, user)
+ if !ok {
+ RespondNotFound(w)
return
- } else if err := conn.Error; err != nil {
- handleError(w, "finding note", err, http.StatusInternalServerError)
+ }
+ if err != nil {
+ HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
@@ -143,7 +140,7 @@ type dateRange struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
query := r.URL.Query()
@@ -162,18 +159,18 @@ func respondGetNotes(userID int, query url.Values, w http.ResponseWriter) {
var total int
if err := conn.Model(database.Note{}).Count(&total).Error; err != nil {
- handleError(w, "counting total", err, http.StatusInternalServerError)
+ HandleError(w, "counting total", err, http.StatusInternalServerError)
return
}
notes := []database.Note{}
if total != 0 {
conn = orderGetNotes(conn)
- conn = preloadNote(conn)
+ conn = database.PreloadNote(conn)
conn = paginate(conn, q.Page)
if err := conn.Find(¬es).Error; err != nil {
- handleError(w, "finding notes", err, http.StatusInternalServerError)
+ HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
}
@@ -280,7 +277,7 @@ func getDateBounds(year, month int) (int64, int64) {
func getNotesBaseQuery(userID int, q getNotesQuery) *gorm.DB {
db := database.DBConn
- conn := db.Debug().Where(
+ conn := db.Where(
"notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?",
userID, false, q.Encrypted,
)
@@ -307,10 +304,6 @@ func orderGetNotes(conn *gorm.DB) *gorm.DB {
return conn.Order("notes.added_on DESC, notes.id DESC")
}
-func preloadNote(conn *gorm.DB) *gorm.DB {
- return conn.Preload("Book").Preload("User")
-}
-
// escapeSearchQuery escapes the query for full text search
func escapeSearchQuery(searchQuery string) string {
return strings.Join(strings.Fields(searchQuery), "&")
@@ -319,14 +312,14 @@ func escapeSearchQuery(searchQuery string) string {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var notes []database.Note
db := database.DBConn
if err := db.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
- handleError(w, "finding notes", err, http.StatusInternalServerError)
+ HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/handlers/notes_test.go b/pkg/server/handlers/notes_test.go
new file mode 100644
index 00000000..ee9cc6ea
--- /dev/null
+++ b/pkg/server/handlers/notes_test.go
@@ -0,0 +1,363 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/presenters"
+ "github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/testutils"
+ "github.com/pkg/errors"
+)
+
+func init() {
+ testutils.InitTestDB()
+}
+
+func getExpectedNotePayload(n database.Note, b database.Book, u database.User) presenters.Note {
+ return presenters.Note{
+ UUID: n.UUID,
+ CreatedAt: n.CreatedAt,
+ UpdatedAt: n.UpdatedAt,
+ Body: n.Body,
+ AddedOn: n.AddedOn,
+ Public: n.Public,
+ USN: n.USN,
+ Book: presenters.NoteBook{
+ UUID: b.UUID,
+ Label: b.Label,
+ },
+ User: presenters.NoteUser{
+ Name: u.Name,
+ UUID: u.UUID,
+ },
+ }
+}
+
+func TestGetNotes(t *testing.T) {
+ defer testutils.ClearData()
+ db := database.DBConn
+
+ // Setup
+ server := MustNewServer(t, &App{
+ Clock: clock.NewMock(),
+ })
+ defer server.Close()
+
+ user := testutils.SetupUserData()
+ anotherUser := testutils.SetupUserData()
+
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+ b2 := database.Book{
+ UserID: user.ID,
+ Label: "css",
+ }
+ testutils.MustExec(t, db.Save(&b2), "preparing b2")
+ b3 := database.Book{
+ UserID: anotherUser.ID,
+ Label: "css",
+ }
+ testutils.MustExec(t, db.Save(&b3), "preparing b3")
+
+ n1 := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "n1 content",
+ USN: 11,
+ Deleted: false,
+ AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n1), "preparing n1")
+ n2 := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "n2 content",
+ USN: 14,
+ Deleted: false,
+ AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n2), "preparing n2")
+ n3 := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "n3 content",
+ USN: 17,
+ Deleted: false,
+ AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n3), "preparing n3")
+ n4 := database.Note{
+ UserID: user.ID,
+ BookUUID: b2.UUID,
+ Body: "n4 content",
+ USN: 18,
+ Deleted: false,
+ AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n4), "preparing n4")
+ n5 := database.Note{
+ UserID: anotherUser.ID,
+ BookUUID: b3.UUID,
+ Body: "n5 content",
+ USN: 19,
+ Deleted: false,
+ AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n5), "preparing n5")
+ n6 := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "",
+ USN: 11,
+ Deleted: true,
+ AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
+ }
+ testutils.MustExec(t, db.Save(&n6), "preparing n6")
+
+ // Execute
+ req := testutils.MakeReq(server, "GET", "/notes?year=2018&month=8", "")
+ res := testutils.HTTPAuthDo(t, req, user)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusOK, "")
+
+ var payload GetNotesResponse
+ if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
+ t.Fatal(errors.Wrap(err, "decoding payload"))
+ }
+
+ var n2Record, n1Record database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record")
+ testutils.MustExec(t, db.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
+
+ expected := GetNotesResponse{
+ Notes: []presenters.Note{
+ getExpectedNotePayload(n2Record, b1, user),
+ getExpectedNotePayload(n1Record, b1, user),
+ },
+ Total: 2,
+ }
+
+ assert.DeepEqual(t, payload, expected, "payload mismatch")
+}
+
+func TestGetNote(t *testing.T) {
+ defer testutils.ClearData()
+ db := database.DBConn
+
+ // Setup
+ server := MustNewServer(t, &App{
+ Clock: clock.NewMock(),
+ })
+ defer server.Close()
+
+ user := testutils.SetupUserData()
+ anotherUser := testutils.SetupUserData()
+
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+
+ privateNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "privateNote content",
+ Public: false,
+ }
+ testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
+ publicNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "publicNote content",
+ Public: true,
+ }
+ testutils.MustExec(t, db.Save(&publicNote), "preparing publicNote")
+ deletedNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Deleted: true,
+ }
+ testutils.MustExec(t, db.Save(&deletedNote), "preparing publicNote")
+
+ t.Run("owner accessing private note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", privateNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, user)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusOK, "")
+
+ var payload presenters.Note
+ if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
+ t.Fatal(errors.Wrap(err, "decoding payload"))
+ }
+
+ var n1Record database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", privateNote.UUID).First(&n1Record), "finding n1Record")
+
+ expected := getExpectedNotePayload(n1Record, b1, user)
+ assert.DeepEqual(t, payload, expected, "payload mismatch")
+ })
+
+ t.Run("owner accessing public note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", publicNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, user)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusOK, "")
+
+ var payload presenters.Note
+ if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
+ t.Fatal(errors.Wrap(err, "decoding payload"))
+ }
+
+ var n2Record database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
+
+ expected := getExpectedNotePayload(n2Record, b1, user)
+ assert.DeepEqual(t, payload, expected, "payload mismatch")
+ })
+
+ t.Run("non-owner accessing public note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", publicNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, anotherUser)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusOK, "")
+
+ var payload presenters.Note
+ if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
+ t.Fatal(errors.Wrap(err, "decoding payload"))
+ }
+
+ var n2Record database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
+
+ expected := getExpectedNotePayload(n2Record, b1, user)
+ assert.DeepEqual(t, payload, expected, "payload mismatch")
+ })
+
+ t.Run("non-owner accessing private note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", privateNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, anotherUser)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "reading body"))
+ }
+
+ assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
+ })
+
+ t.Run("guest accessing public note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", publicNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPDo(t, req)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusOK, "")
+
+ var payload presenters.Note
+ if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
+ t.Fatal(errors.Wrap(err, "decoding payload"))
+ }
+
+ var n2Record database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
+
+ expected := getExpectedNotePayload(n2Record, b1, user)
+ assert.DeepEqual(t, payload, expected, "payload mismatch")
+ })
+
+ t.Run("guest accessing private note", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", privateNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPDo(t, req)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "reading body"))
+ }
+
+ assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
+ })
+
+ t.Run("nonexistent", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", "someRandomString")
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, user)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "reading body"))
+ }
+
+ assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
+ })
+
+ t.Run("deleted", func(t *testing.T) {
+ // Execute
+ url := fmt.Sprintf("/notes/%s", deletedNote.UUID)
+ req := testutils.MakeReq(server, "GET", url, "")
+ res := testutils.HTTPAuthDo(t, req, user)
+
+ // Test
+ assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "reading body"))
+ }
+
+ assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
+ })
+}
diff --git a/pkg/server/api/handlers/repetition_rules.go b/pkg/server/handlers/repetition_rules.go
similarity index 90%
rename from pkg/server/api/handlers/repetition_rules.go
rename to pkg/server/handlers/repetition_rules.go
index c66d8b5b..cc305cb1 100644
--- a/pkg/server/api/handlers/repetition_rules.go
+++ b/pkg/server/handlers/repetition_rules.go
@@ -23,8 +23,8 @@ import (
"net/http"
"time"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -33,7 +33,7 @@ import (
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -48,7 +48,7 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
db := database.DBConn
var repetitionRule database.RepetitionRule
if err := db.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil {
- handleError(w, "getting repetition rules", err, http.StatusInternalServerError)
+ HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@@ -59,14 +59,14 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
db := database.DBConn
var repetitionRules []database.RepetitionRule
if err := 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)
+ HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@@ -278,7 +278,7 @@ func calcNextActive(now time.Time, p calcNextActiveParams) int64 {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -291,7 +291,7 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
db := database.DBConn
var books []database.Book
if err := db.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil {
- handleError(w, "finding books", nil, http.StatusInternalServerError)
+ HandleError(w, "finding books", nil, http.StatusInternalServerError)
return
}
@@ -314,7 +314,7 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
Enabled: params.GetEnabled(),
}
if err := db.Create(&record).Error; err != nil {
- handleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
+ HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
return
}
@@ -339,7 +339,7 @@ func parseUpdateDigestParams(r *http.Request) (repetitionRuleParams, error) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -355,12 +355,12 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not found", http.StatusNotFound)
return
} else if err := conn.Error; err != nil {
- handleError(w, "finding the repetition rule", err, http.StatusInternalServerError)
+ HandleError(w, "finding the repetition rule", err, http.StatusInternalServerError)
return
}
if err := db.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil {
- handleError(w, "deleting the repetition rule", err, http.StatusInternalServerError)
+ HandleError(w, "deleting the repetition rule", err, http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
@@ -369,7 +369,7 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -387,7 +387,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
var repetitionRule database.RepetitionRule
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").First(&repetitionRule).Error; err != nil {
- handleError(w, "finding record", nil, http.StatusInternalServerError)
+ HandleError(w, "finding record", nil, http.StatusInternalServerError)
return
}
@@ -433,26 +433,26 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
if params.BookUUIDs != nil {
var books []database.Book
if err := tx.Where("user_id = ? AND uuid IN (?)", user.ID, *params.BookUUIDs).Find(&books).Error; err != nil {
- handleError(w, "finding books", err, http.StatusInternalServerError)
+ HandleError(w, "finding books", err, http.StatusInternalServerError)
return
}
if err := tx.Model(&repetitionRule).Association("Books").Replace(books).Error; err != nil {
tx.Rollback()
- handleError(w, "updating books association for a repetitionRule", err, http.StatusInternalServerError)
+ HandleError(w, "updating books association for a repetitionRule", err, http.StatusInternalServerError)
return
}
}
if err := tx.Save(&repetitionRule).Error; err != nil {
tx.Rollback()
- handleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
+ HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
return
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
- handleError(w, "committing a transaction", err, http.StatusInternalServerError)
+ HandleError(w, "committing a transaction", err, http.StatusInternalServerError)
}
resp := presenters.PresentRepetitionRule(repetitionRule)
diff --git a/pkg/server/api/handlers/repetition_rules_test.go b/pkg/server/handlers/repetition_rules_test.go
similarity index 99%
rename from pkg/server/api/handlers/repetition_rules_test.go
rename to pkg/server/handlers/repetition_rules_test.go
index 993dcf3d..3015d41c 100644
--- a/pkg/server/api/handlers/repetition_rules_test.go
+++ b/pkg/server/handlers/repetition_rules_test.go
@@ -27,7 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
diff --git a/pkg/server/api/handlers/routes.go b/pkg/server/handlers/routes.go
similarity index 87%
rename from pkg/server/api/handlers/routes.go
rename to pkg/server/handlers/routes.go
index d92a9a64..42a8b849 100644
--- a/pkg/server/api/handlers/routes.go
+++ b/pkg/server/handlers/routes.go
@@ -27,20 +27,14 @@ import (
"time"
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/helpers"
"github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
)
-// ErrInvalidAuthHeader is an error for invalid format of Authorization HTTP header
-var ErrInvalidAuthHeader = errors.New("Invalid authorization header")
-
-// ErrForbidden is an error for forbidden requests
-var ErrForbidden = errors.New("forbidden")
-
// Route represents a single route
type Route struct {
Method string
@@ -58,7 +52,7 @@ func parseAuthHeader(h string) (authHeader, error) {
parts := strings.Split(h, " ")
if len(parts) != 2 {
- return authHeader{}, ErrInvalidAuthHeader
+ return authHeader{}, errors.New("Invalid authorization header")
}
parsed := authHeader{
@@ -69,16 +63,6 @@ func parseAuthHeader(h string) (authHeader, error) {
return parsed, nil
}
-func respondForbidden(w http.ResponseWriter) {
- http.Error(w, "forbidden", http.StatusForbidden)
-}
-
-func respondUnauthorized(w http.ResponseWriter) {
- unsetSessionCookie(w)
- w.Header().Add("WWW-Authenticate", `Bearer realm="Dnote Pro", charset="UTF-8"`)
- http.Error(w, "unauthorized", http.StatusUnauthorized)
-}
-
func legacyAuth(next http.HandlerFunc) http.HandlerFunc {
db := database.DBConn
@@ -153,7 +137,8 @@ func getCredential(r *http.Request) (string, error) {
return ret, nil
}
-func authWithSession(r *http.Request, p *authMiddlewareParams) (database.User, bool, error) {
+// AuthWithSession performs user authentication with session
+func AuthWithSession(r *http.Request, p *AuthMiddlewareParams) (database.User, bool, error) {
db := database.DBConn
var user database.User
@@ -161,7 +146,6 @@ func authWithSession(r *http.Request, p *authMiddlewareParams) (database.User, b
if err != nil {
return user, false, errors.Wrap(err, "getting credential")
}
-
if sessionKey == "" {
return user, false, nil
}
@@ -187,16 +171,10 @@ func authWithSession(r *http.Request, p *authMiddlewareParams) (database.User, b
return user, false, errors.Wrap(err, "finding user from token")
}
- if p != nil && p.ProOnly {
- if !user.Cloud {
- return user, false, ErrForbidden
- }
- }
-
return user, true, nil
}
-func authWithToken(r *http.Request, tokenType string, p *authMiddlewareParams) (database.User, database.Token, bool, error) {
+func authWithToken(r *http.Request, tokenType string, p *AuthMiddlewareParams) (database.User, database.Token, bool, error) {
db := database.DBConn
var user database.User
var token database.Token
@@ -222,45 +200,41 @@ func authWithToken(r *http.Request, tokenType string, p *authMiddlewareParams) (
return user, token, false, errors.Wrap(err, "finding user")
}
- if p != nil && p.ProOnly {
- if !user.Cloud {
- return user, token, false, ErrForbidden
- }
- }
-
return user, token, true, nil
}
-type authMiddlewareParams struct {
+// AuthMiddlewareParams is the params for the authentication middleware
+type AuthMiddlewareParams struct {
ProOnly bool
}
-func auth(next http.HandlerFunc, p *authMiddlewareParams) http.HandlerFunc {
+func auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- user, ok, err := authWithSession(r, p)
- if !ok || err != nil {
- if err == ErrForbidden {
- http.Error(w, "forbidden", http.StatusForbidden)
- } else {
- respondUnauthorized(w)
- }
+ user, ok, err := AuthWithSession(r, p)
+ if !ok {
+ respondUnauthorized(w)
return
}
+ if err != nil {
+ HandleError(w, "authenticating with session", err, http.StatusInternalServerError)
+ return
+ }
+
+ if p != nil && p.ProOnly {
+ if !user.Cloud {
+ respondForbidden(w)
+ }
+ }
ctx := context.WithValue(r.Context(), helpers.KeyUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
-func tokenAuth(next http.HandlerFunc, tokenType string, p *authMiddlewareParams) http.HandlerFunc {
+func 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(r, tokenType, p)
if err != nil {
- if err == ErrForbidden {
- respondForbidden(w)
- return
- }
-
// log the error and continue
log.ErrorWrap(err, "authenticating with token")
}
@@ -271,22 +245,24 @@ func tokenAuth(next http.HandlerFunc, tokenType string, p *authMiddlewareParams)
ctx = context.WithValue(ctx, helpers.KeyToken, token)
} else {
// If token-based auth fails, fall back to session-based auth
- user, ok, err = authWithSession(r, p)
+ user, ok, err = AuthWithSession(r, p)
if err != nil {
- // log the error and continue
- log.ErrorWrap(err, "authenticating with session")
+ HandleError(w, "authenticating with session", err, http.StatusInternalServerError)
+ return
}
if !ok {
- if err == ErrForbidden {
- respondForbidden(w)
- } else {
- respondUnauthorized(w)
- }
+ respondUnauthorized(w)
return
}
}
+ if p != nil && p.ProOnly {
+ if !user.Cloud {
+ respondForbidden(w)
+ }
+ }
+
ctx = context.WithValue(ctx, helpers.KeyUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
@@ -383,7 +359,7 @@ func NewRouter(app *App) (*mux.Router, error) {
return nil, errors.Wrap(err, "initializing app")
}
- proOnly := authMiddlewareParams{ProOnly: true}
+ proOnly := AuthMiddlewareParams{ProOnly: true}
var routes = []Route{
// internal
@@ -404,7 +380,7 @@ func NewRouter(app *App) (*mux.Router, error) {
{"GET", "/stripe_source", auth(app.getStripeSource, nil), true},
{"PATCH", "/stripe_source", auth(app.updateStripeSource, nil), true},
{"GET", "/notes", auth(app.getNotes, &proOnly), false},
- {"GET", "/notes/{noteUUID}", auth(app.getNote, &proOnly), true},
+ {"GET", "/notes/{noteUUID}", app.getNote, true},
{"GET", "/calendar", auth(app.getCalendar, &proOnly), true},
{"GET", "/repetition_rules", auth(app.getRepetitionRules, &proOnly), true},
{"GET", "/repetition_rules/{repetitionRuleUUID}", tokenAuth(app.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
diff --git a/pkg/server/api/handlers/routes_test.go b/pkg/server/handlers/routes_test.go
similarity index 99%
rename from pkg/server/api/handlers/routes_test.go
rename to pkg/server/handlers/routes_test.go
index 01683d90..53227e7c 100644
--- a/pkg/server/api/handlers/routes_test.go
+++ b/pkg/server/handlers/routes_test.go
@@ -317,7 +317,7 @@ func TestAuthMiddleware_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- server := httptest.NewServer(auth(handler, &authMiddlewareParams{
+ server := httptest.NewServer(auth(handler, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
@@ -544,7 +544,7 @@ func TestTokenAuthMiddleWare_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- server := httptest.NewServer(tokenAuth(handler, database.TokenTypeEmailPreference, &authMiddlewareParams{
+ server := httptest.NewServer(tokenAuth(handler, database.TokenTypeEmailPreference, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
diff --git a/pkg/server/api/handlers/semver.go b/pkg/server/handlers/semver.go
similarity index 100%
rename from pkg/server/api/handlers/semver.go
rename to pkg/server/handlers/semver.go
diff --git a/pkg/server/api/handlers/subscription.go b/pkg/server/handlers/subscription.go
similarity index 86%
rename from pkg/server/api/handlers/subscription.go
rename to pkg/server/handlers/subscription.go
index 79476652..1fe700b6 100644
--- a/pkg/server/api/handlers/subscription.go
+++ b/pkg/server/handlers/subscription.go
@@ -26,8 +26,8 @@ import (
"os"
"strings"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/operations"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/database"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
@@ -128,13 +128,13 @@ type createSubPayload struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var payload createSubPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
- handleError(w, "decoding params", err, http.StatusBadRequest)
+ HandleError(w, "decoding params", err, http.StatusBadRequest)
return
}
@@ -147,31 +147,31 @@ func (a *App) createSub(w http.ResponseWriter, r *http.Request) {
"billing_country": payload.Country,
}).Error; err != nil {
tx.Rollback()
- handleError(w, "updating user", err, http.StatusInternalServerError)
+ HandleError(w, "updating user", err, http.StatusInternalServerError)
return
}
customer, err := getOrCreateStripeCustomer(tx, user)
if err != nil {
tx.Rollback()
- handleError(w, "getting customer", err, http.StatusInternalServerError)
+ HandleError(w, "getting customer", err, http.StatusInternalServerError)
return
}
if _, err = addCustomerSource(customer.ID, payload.Source.ID); err != nil {
tx.Rollback()
- handleError(w, "attaching source", err, http.StatusInternalServerError)
+ HandleError(w, "attaching source", err, http.StatusInternalServerError)
return
}
if _, err := createCustomerSubscription(customer.ID, proPlanID); err != nil {
tx.Rollback()
- handleError(w, "creating subscription", err, http.StatusInternalServerError)
+ HandleError(w, "creating subscription", err, http.StatusInternalServerError)
return
}
if err := tx.Commit().Error; err != nil {
- handleError(w, "committing a subscription transaction", err, http.StatusInternalServerError)
+ HandleError(w, "committing a subscription transaction", err, http.StatusInternalServerError)
return
}
@@ -218,21 +218,21 @@ func validateUpdateSubPayload(p updateSubPayload) error {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
if user.StripeCustomerID == "" {
- handleError(w, "Customer does not exist", nil, http.StatusForbidden)
+ HandleError(w, "Customer does not exist", nil, http.StatusForbidden)
return
}
var payload updateSubPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
- handleError(w, "decoding params", err, http.StatusBadRequest)
+ HandleError(w, "decoding params", err, http.StatusBadRequest)
return
}
if err := validateUpdateSubPayload(payload); err != nil {
- handleError(w, "invalid payload", err, http.StatusBadRequest)
+ HandleError(w, "invalid payload", err, http.StatusBadRequest)
return
}
@@ -251,7 +251,7 @@ func (a *App) updateSub(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError
}
- handleError(w, fmt.Sprintf("during operation %s", payload.Op), err, statusCode)
+ HandleError(w, fmt.Sprintf("during operation %s", payload.Op), err, statusCode)
return
}
@@ -281,7 +281,7 @@ func respondWithEmptySub(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(emptyGetSubResponse); err != nil {
- handleError(w, "encoding response", err, http.StatusInternalServerError)
+ HandleError(w, "encoding response", err, http.StatusInternalServerError)
return
}
}
@@ -289,7 +289,7 @@ func respondWithEmptySub(w http.ResponseWriter) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
if user.StripeCustomerID == "" {
@@ -304,7 +304,7 @@ func (a *App) getSub(w http.ResponseWriter, r *http.Request) {
if !i.Next() {
if err := i.Err(); err != nil {
- handleError(w, "fetching subscription", err, http.StatusInternalServerError)
+ HandleError(w, "fetching subscription", err, http.StatusInternalServerError)
return
}
@@ -417,13 +417,13 @@ func validateUpdateStripeSourcePayload(p updateStripeSourcePayload) error {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var payload updateStripeSourcePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
- handleError(w, "decoding params", err, http.StatusBadRequest)
+ HandleError(w, "decoding params", err, http.StatusBadRequest)
return
}
if err := validateUpdateStripeSourcePayload(payload); err != nil {
@@ -439,32 +439,32 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
"billing_country": payload.Country,
}).Error; err != nil {
tx.Rollback()
- handleError(w, "updating user", err, http.StatusInternalServerError)
+ HandleError(w, "updating user", err, http.StatusInternalServerError)
return
}
c, err := customer.Get(user.StripeCustomerID, nil)
if err != nil {
tx.Rollback()
- handleError(w, "retriving customer", err, http.StatusInternalServerError)
+ HandleError(w, "retriving customer", err, http.StatusInternalServerError)
return
}
if _, err := removeCustomerSource(user.StripeCustomerID, c.DefaultSource.ID); err != nil {
tx.Rollback()
- handleError(w, "removing source", err, http.StatusInternalServerError)
+ HandleError(w, "removing source", err, http.StatusInternalServerError)
return
}
if _, err := addCustomerSource(user.StripeCustomerID, payload.Source.ID); err != nil {
tx.Rollback()
- handleError(w, "attaching source", err, http.StatusInternalServerError)
+ HandleError(w, "attaching source", err, http.StatusInternalServerError)
return
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
- handleError(w, "committing transaction", err, http.StatusInternalServerError)
+ HandleError(w, "committing transaction", err, http.StatusInternalServerError)
return
}
@@ -474,7 +474,7 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
if user.StripeCustomerID == "" {
@@ -484,7 +484,7 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
c, err := customer.Get(user.StripeCustomerID, nil)
if err != nil {
- handleError(w, "fetching stripe customer", err, http.StatusInternalServerError)
+ HandleError(w, "fetching stripe customer", err, http.StatusInternalServerError)
return
}
@@ -495,7 +495,7 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
cd, err := getStripeCard(user.StripeCustomerID, c.DefaultSource.ID)
if err != nil {
- handleError(w, "fetching stripe source", err, http.StatusInternalServerError)
+ HandleError(w, "fetching stripe source", err, http.StatusInternalServerError)
return
}
@@ -512,14 +512,14 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
- handleError(w, "reading body", err, http.StatusServiceUnavailable)
+ HandleError(w, "reading body", err, http.StatusServiceUnavailable)
return
}
webhookSecret := os.Getenv("StripeWebhookSecret")
event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), webhookSecret)
if err != nil {
- handleError(w, "verifying stripe webhook signature", err, http.StatusBadRequest)
+ HandleError(w, "verifying stripe webhook signature", err, http.StatusBadRequest)
return
}
@@ -528,7 +528,7 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
{
var subscription stripe.Subscription
if json.Unmarshal(event.Data.Raw, &subscription); err != nil {
- handleError(w, "unmarshaling payload", err, http.StatusBadRequest)
+ HandleError(w, "unmarshaling payload", err, http.StatusBadRequest)
return
}
@@ -537,7 +537,7 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
default:
{
msg := fmt.Sprintf("Unsupported webhook event type %s", event.Type)
- handleError(w, msg, err, http.StatusBadRequest)
+ HandleError(w, msg, err, http.StatusBadRequest)
return
}
}
diff --git a/pkg/server/api/handlers/testutils.go b/pkg/server/handlers/testutils.go
similarity index 100%
rename from pkg/server/api/handlers/testutils.go
rename to pkg/server/handlers/testutils.go
diff --git a/pkg/server/api/handlers/user.go b/pkg/server/handlers/user.go
similarity index 83%
rename from pkg/server/api/handlers/user.go
rename to pkg/server/handlers/user.go
index 9160e1ca..e7acc47f 100644
--- a/pkg/server/api/handlers/user.go
+++ b/pkg/server/handlers/user.go
@@ -23,8 +23,8 @@ import (
"net/http"
"time"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/mailer"
@@ -42,7 +42,7 @@ func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -62,14 +62,14 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
var account database.Account
err = db.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
tx := db.Begin()
if err := tx.Save(&user).Error; err != nil {
tx.Rollback()
- handleError(w, "saving user", err, http.StatusInternalServerError)
+ HandleError(w, "saving user", err, http.StatusInternalServerError)
return
}
@@ -81,7 +81,7 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
- handleError(w, "saving account", err, http.StatusInternalServerError)
+ HandleError(w, "saving account", err, http.StatusInternalServerError)
return
}
@@ -106,7 +106,7 @@ func respondWithCalendar(w http.ResponseWriter, userID int) {
Order("added_date DESC").Rows()
if err != nil {
- handleError(w, "Failed to count lessons", err, http.StatusInternalServerError)
+ HandleError(w, "Failed to count lessons", err, http.StatusInternalServerError)
return
}
@@ -117,7 +117,7 @@ func respondWithCalendar(w http.ResponseWriter, userID int) {
var d time.Time
if err := rows.Scan(&count, &d); err != nil {
- handleError(w, "counting notes", err, http.StatusInternalServerError)
+ HandleError(w, "counting notes", err, http.StatusInternalServerError)
}
payload[d.Format("2006-1-2")] = count
}
@@ -128,7 +128,7 @@ func respondWithCalendar(w http.ResponseWriter, userID int) {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -138,7 +138,7 @@ func (a *App) getCalendar(w http.ResponseWriter, r *http.Request) {
func (a *App) getDemoCalendar(w http.ResponseWriter, r *http.Request) {
userID, err := helpers.GetDemoUserID()
if err != nil {
- handleError(w, "finding demo user", err, http.StatusInternalServerError)
+ HandleError(w, "finding demo user", err, http.StatusInternalServerError)
return
}
@@ -150,14 +150,14 @@ func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var account database.Account
err := db.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -172,7 +172,7 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
tokenValue, err := generateVerificationCode()
if err != nil {
- handleError(w, "generating verification code", err, http.StatusInternalServerError)
+ HandleError(w, "generating verification code", err, http.StatusInternalServerError)
return
}
@@ -183,7 +183,7 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
}
if err := db.Save(&token).Error; err != nil {
- handleError(w, "saving token", err, http.StatusInternalServerError)
+ HandleError(w, "saving token", err, http.StatusInternalServerError)
return
}
@@ -195,12 +195,12 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
}
email := mailer.NewEmail("noreply@getdnote.com", []string{account.Email.String}, subject)
if err := email.ParseTemplate(mailer.EmailTypeEmailVerification, data); err != nil {
- handleError(w, "parsing template", err, http.StatusInternalServerError)
+ HandleError(w, "parsing template", err, http.StatusInternalServerError)
return
}
if err := email.Send(); err != nil {
- handleError(w, "sending email", err, http.StatusInternalServerError)
+ HandleError(w, "sending email", err, http.StatusInternalServerError)
return
}
@@ -216,7 +216,7 @@ func (a *App) 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)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
@@ -241,7 +241,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
var account database.Account
if err := db.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
- handleError(w, "finding account", err, http.StatusInternalServerError)
+ HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
if account.EmailVerified {
@@ -253,19 +253,19 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
account.EmailVerified = true
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
- handleError(w, "updating email_verified", err, http.StatusInternalServerError)
+ HandleError(w, "updating email_verified", err, http.StatusInternalServerError)
return
}
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
tx.Rollback()
- handleError(w, "updating reset token", err, http.StatusInternalServerError)
+ HandleError(w, "updating reset token", err, http.StatusInternalServerError)
return
}
tx.Commit()
var user database.User
if err := db.Where("id = ?", token.UserID).First(&user).Error; err != nil {
- handleError(w, "finding user", err, http.StatusInternalServerError)
+ HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
@@ -282,19 +282,19 @@ func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var params updateEmailPreferencePayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
var frequency database.EmailPreference
if err := db.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&frequency).Error; err != nil {
- handleError(w, "finding frequency", err, http.StatusInternalServerError)
+ HandleError(w, "finding frequency", err, http.StatusInternalServerError)
return
}
@@ -303,7 +303,7 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
frequency.DigestWeekly = params.DigestWeekly
if err := tx.Save(&frequency).Error; err != nil {
tx.Rollback()
- handleError(w, "saving frequency", err, http.StatusInternalServerError)
+ HandleError(w, "saving frequency", err, http.StatusInternalServerError)
return
}
@@ -312,7 +312,7 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
// Use token if the user was authenticated by token
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
tx.Rollback()
- handleError(w, "updating reset token", err, http.StatusInternalServerError)
+ HandleError(w, "updating reset token", err, http.StatusInternalServerError)
return
}
}
@@ -327,13 +327,13 @@ func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var pref database.EmailPreference
if err := db.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
- handleError(w, "finding pref", err, http.StatusInternalServerError)
+ HandleError(w, "finding pref", err, http.StatusInternalServerError)
return
}
@@ -351,7 +351,7 @@ func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
@@ -367,7 +367,7 @@ func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
var account database.Account
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
- handleError(w, "getting user", nil, http.StatusInternalServerError)
+ HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/user_test.go b/pkg/server/handlers/user_test.go
similarity index 99%
rename from pkg/server/api/handlers/user_test.go
rename to pkg/server/handlers/user_test.go
index efe3dd39..e9e1ac5b 100644
--- a/pkg/server/api/handlers/user_test.go
+++ b/pkg/server/handlers/user_test.go
@@ -27,7 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
diff --git a/pkg/server/api/handlers/v3_auth.go b/pkg/server/handlers/v3_auth.go
similarity index 90%
rename from pkg/server/api/handlers/v3_auth.go
rename to pkg/server/handlers/v3_auth.go
index 45af3fdd..18cf194b 100644
--- a/pkg/server/api/handlers/v3_auth.go
+++ b/pkg/server/handlers/v3_auth.go
@@ -23,7 +23,7 @@ import (
"net/http"
"time"
- "github.com/dnote/dnote/pkg/server/api/operations"
+ "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/database"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@@ -85,7 +85,7 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
var params signinPayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
if params.Email == "" || params.Password == "" {
@@ -99,7 +99,7 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
} else if conn.Error != nil {
- handleError(w, "getting user", err, http.StatusInternalServerError)
+ HandleError(w, "getting user", err, http.StatusInternalServerError)
return
}
@@ -113,7 +113,7 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
var user database.User
err = db.Where("id = ?", account.UserID).First(&user).Error
if err != nil {
- handleError(w, "finding user", err, http.StatusInternalServerError)
+ HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
@@ -134,7 +134,7 @@ func (a *App) signoutOptions(w http.ResponseWriter, r *http.Request) {
func (a *App) signout(w http.ResponseWriter, r *http.Request) {
key, err := getCredential(r)
if err != nil {
- handleError(w, "getting credential", nil, http.StatusInternalServerError)
+ HandleError(w, "getting credential", nil, http.StatusInternalServerError)
return
}
@@ -145,7 +145,7 @@ func (a *App) signout(w http.ResponseWriter, r *http.Request) {
err = operations.DeleteSession(database.DBConn, key)
if err != nil {
- handleError(w, "deleting session", nil, http.StatusInternalServerError)
+ HandleError(w, "deleting session", nil, http.StatusInternalServerError)
return
}
@@ -192,7 +192,7 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
var count int
if err := db.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
- handleError(w, "checking duplicate user", err, http.StatusInternalServerError)
+ HandleError(w, "checking duplicate user", err, http.StatusInternalServerError)
return
}
if count > 0 {
@@ -202,7 +202,7 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
user, err := operations.CreateUser(params.Email, params.Password)
if err != nil {
- handleError(w, "creating user", err, http.StatusInternalServerError)
+ HandleError(w, "creating user", err, http.StatusInternalServerError)
return
}
@@ -216,7 +216,7 @@ func respondWithSession(w http.ResponseWriter, userID int, statusCode int) {
session, err := operations.CreateSession(db, userID)
if err != nil {
- handleError(w, "creating session", nil, http.StatusBadRequest)
+ HandleError(w, "creating session", nil, http.StatusBadRequest)
return
}
@@ -230,7 +230,7 @@ func respondWithSession(w http.ResponseWriter, userID int, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(response); err != nil {
- handleError(w, "encoding response", err, http.StatusInternalServerError)
+ HandleError(w, "encoding response", err, http.StatusInternalServerError)
return
}
}
diff --git a/pkg/server/api/handlers/v3_auth_test.go b/pkg/server/handlers/v3_auth_test.go
similarity index 100%
rename from pkg/server/api/handlers/v3_auth_test.go
rename to pkg/server/handlers/v3_auth_test.go
diff --git a/pkg/server/api/handlers/v3_books.go b/pkg/server/handlers/v3_books.go
similarity index 85%
rename from pkg/server/api/handlers/v3_books.go
rename to pkg/server/handlers/v3_books.go
index d18e75cb..7be082d5 100644
--- a/pkg/server/api/handlers/v3_books.go
+++ b/pkg/server/handlers/v3_books.go
@@ -24,9 +24,9 @@ import (
"net/http"
"net/url"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/operations"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/operations"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -59,13 +59,13 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
var params createBookPayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
err = validateCreateBookPayload(params)
if err != nil {
- handleError(w, "validating payload", err, http.StatusBadRequest)
+ HandleError(w, "validating payload", err, http.StatusBadRequest)
return
}
@@ -76,7 +76,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
Where("user_id = ? AND label = ?", user.ID, params.Name).
Count(&bookCount).Error
if err != nil {
- handleError(w, "checking duplicate", err, http.StatusInternalServerError)
+ HandleError(w, "checking duplicate", err, http.StatusInternalServerError)
return
}
if bookCount > 0 {
@@ -86,7 +86,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
book, err := operations.CreateBook(user, a.Clock, params.Name)
if err != nil {
- handleError(w, "inserting book", err, http.StatusInternalServerError)
+ HandleError(w, "inserting book", err, http.StatusInternalServerError)
}
resp := CreateBookResp{
Book: presenters.PresentBook(book),
@@ -124,7 +124,7 @@ func respondWithBooks(userID int, query url.Values, w http.ResponseWriter) {
}
if err := conn.Find(&books).Error; err != nil {
- handleError(w, "finding books", err, http.StatusInternalServerError)
+ HandleError(w, "finding books", err, http.StatusInternalServerError)
return
}
@@ -136,7 +136,7 @@ func respondWithBooks(userID int, query url.Values, w http.ResponseWriter) {
func (a *App) GetDemoBooks(w http.ResponseWriter, r *http.Request) {
demoUserID, err := helpers.GetDemoUserID()
if err != nil {
- handleError(w, "finding demo user", err, http.StatusInternalServerError)
+ HandleError(w, "finding demo user", err, http.StatusInternalServerError)
return
}
@@ -177,7 +177,7 @@ func (a *App) GetBook(w http.ResponseWriter, r *http.Request) {
return
}
if err := conn.Error; err != nil {
- handleError(w, "finding book", err, http.StatusInternalServerError)
+ HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
@@ -209,21 +209,21 @@ func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
- handleError(w, "finding book", err, http.StatusInternalServerError)
+ HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
var params updateBookPayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
book, err = operations.UpdateBook(tx, a.Clock, user, book, params.Name)
if err != nil {
tx.Rollback()
- handleError(w, "updating a book", err, http.StatusInternalServerError)
+ HandleError(w, "updating a book", err, http.StatusInternalServerError)
}
tx.Commit()
@@ -255,25 +255,25 @@ func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
- handleError(w, "finding book", err, http.StatusInternalServerError)
+ HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
var notes []database.Note
if err := tx.Where("book_uuid = ? AND NOT deleted", uuid).Order("usn ASC").Find(¬es).Error; err != nil {
- handleError(w, "finding notes", err, http.StatusInternalServerError)
+ HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
for _, note := range notes {
if _, err := operations.DeleteNote(tx, user, note); err != nil {
- handleError(w, "deleting a note", err, http.StatusInternalServerError)
+ HandleError(w, "deleting a note", err, http.StatusInternalServerError)
return
}
}
b, err := operations.DeleteBook(tx, user, book)
if err != nil {
- handleError(w, "deleting book", err, http.StatusInternalServerError)
+ HandleError(w, "deleting book", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/v3_books_test.go b/pkg/server/handlers/v3_books_test.go
similarity index 99%
rename from pkg/server/api/handlers/v3_books_test.go
rename to pkg/server/handlers/v3_books_test.go
index d80d4d11..ab07c7a6 100644
--- a/pkg/server/api/handlers/v3_books_test.go
+++ b/pkg/server/handlers/v3_books_test.go
@@ -26,7 +26,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
diff --git a/pkg/server/api/handlers/v3_notes.go b/pkg/server/handlers/v3_notes.go
similarity index 78%
rename from pkg/server/api/handlers/v3_notes.go
rename to pkg/server/handlers/v3_notes.go
index cb3e7612..e6e62c78 100644
--- a/pkg/server/api/handlers/v3_notes.go
+++ b/pkg/server/handlers/v3_notes.go
@@ -23,9 +23,9 @@ import (
"fmt"
"net/http"
- "github.com/dnote/dnote/pkg/server/api/helpers"
- "github.com/dnote/dnote/pkg/server/api/operations"
- "github.com/dnote/dnote/pkg/server/api/presenters"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/operations"
+ "github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/database"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -34,6 +34,7 @@ import (
type updateNotePayload struct {
BookUUID *string `json:"book_uuid"`
Content *string `json:"content"`
+ Public *bool `json:"public"`
}
type updateNoteResp struct {
@@ -42,7 +43,7 @@ type updateNoteResp struct {
}
func validateUpdateNotePayload(p updateNotePayload) bool {
- return p.BookUUID != nil || p.Content != nil
+ return p.BookUUID != nil || p.Content != nil || p.Public != nil
}
// UpdateNote updates note
@@ -53,41 +54,45 @@ func (a *App) UpdateNote(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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var params updateNotePayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
- handleError(w, "decoding params", err, http.StatusInternalServerError)
+ HandleError(w, "decoding params", err, http.StatusInternalServerError)
return
}
if ok := validateUpdateNotePayload(params); !ok {
- handleError(w, "Invalid payload", nil, http.StatusBadRequest)
+ HandleError(w, "Invalid payload", nil, http.StatusBadRequest)
return
}
var note database.Note
if err := db.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).Error; err != nil {
- handleError(w, "finding note", err, http.StatusInternalServerError)
+ HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
tx := db.Begin()
- note, err = operations.UpdateNote(tx, user, a.Clock, note, params.BookUUID, params.Content)
+ note, err = operations.UpdateNote(tx, user, a.Clock, note, &operations.UpdateNoteParams{
+ BookUUID: params.BookUUID,
+ Content: params.Content,
+ Public: params.Public,
+ })
if err != nil {
tx.Rollback()
- handleError(w, "updating note", err, http.StatusInternalServerError)
+ HandleError(w, "updating note", err, http.StatusInternalServerError)
return
}
var book database.Book
if err := tx.Where("uuid = ? AND user_id = ?", note.BookUUID, user.ID).First(&book).Error; err != nil {
tx.Rollback()
- handleError(w, fmt.Sprintf("finding book %s to preload", note.BookUUID), err, http.StatusInternalServerError)
+ HandleError(w, fmt.Sprintf("finding book %s to preload", note.BookUUID), err, http.StatusInternalServerError)
return
}
@@ -118,13 +123,13 @@ func (a *App) DeleteNote(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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var note database.Note
if err := db.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).Error; err != nil {
- handleError(w, "finding note", err, http.StatusInternalServerError)
+ HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
@@ -133,7 +138,7 @@ func (a *App) DeleteNote(w http.ResponseWriter, r *http.Request) {
n, err := operations.DeleteNote(tx, user, note)
if err != nil {
tx.Rollback()
- handleError(w, "deleting note", err, http.StatusInternalServerError)
+ HandleError(w, "deleting note", err, http.StatusInternalServerError)
return
}
@@ -170,33 +175,33 @@ type CreateNoteResp struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
var params createNotePayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
- handleError(w, "decoding payload", err, http.StatusInternalServerError)
+ HandleError(w, "decoding payload", err, http.StatusInternalServerError)
return
}
err = validateCreateNotePayload(params)
if err != nil {
- handleError(w, "validating payload", err, http.StatusBadRequest)
+ HandleError(w, "validating payload", err, http.StatusBadRequest)
return
}
var book database.Book
db := database.DBConn
if err := db.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
- handleError(w, "finding book", err, http.StatusInternalServerError)
+ HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
note, err := operations.CreateNote(user, a.Clock, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false)
if err != nil {
- handleError(w, "creating note", err, http.StatusInternalServerError)
+ HandleError(w, "creating note", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/v3_notes_test.go b/pkg/server/handlers/v3_notes_test.go
similarity index 77%
rename from pkg/server/api/handlers/v3_notes_test.go
rename to pkg/server/handlers/v3_notes_test.go
index a1b05c99..9b2f221b 100644
--- a/pkg/server/api/handlers/v3_notes_test.go
+++ b/pkg/server/handlers/v3_notes_test.go
@@ -96,10 +96,12 @@ func TestUpdateNote(t *testing.T) {
noteUUID string
noteBookUUID string
noteBody string
+ notePublic bool
noteDeleted bool
expectedNoteBody string
expectedNoteBookName string
expectedNoteBookUUID string
+ expectedNotePublic bool
}{
{
payload: fmt.Sprintf(`{
@@ -107,11 +109,13 @@ func TestUpdateNote(t *testing.T) {
}`, updatedBody),
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
noteBookUUID: b1UUID,
+ notePublic: false,
noteBody: "original content",
noteDeleted: false,
expectedNoteBookUUID: b1UUID,
expectedNoteBody: "some updated content",
expectedNoteBookName: "css",
+ expectedNotePublic: false,
},
{
payload: fmt.Sprintf(`{
@@ -119,11 +123,13 @@ func TestUpdateNote(t *testing.T) {
}`, b1UUID),
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
noteBookUUID: b1UUID,
+ notePublic: false,
noteBody: "original content",
noteDeleted: false,
expectedNoteBookUUID: b1UUID,
expectedNoteBody: "original content",
expectedNoteBookName: "css",
+ expectedNotePublic: false,
},
{
payload: fmt.Sprintf(`{
@@ -131,11 +137,13 @@ func TestUpdateNote(t *testing.T) {
}`, b2UUID),
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
noteBookUUID: b1UUID,
+ notePublic: false,
noteBody: "original content",
noteDeleted: false,
expectedNoteBookUUID: b2UUID,
expectedNoteBody: "original content",
expectedNoteBookName: "js",
+ expectedNotePublic: false,
},
{
payload: fmt.Sprintf(`{
@@ -144,11 +152,13 @@ func TestUpdateNote(t *testing.T) {
}`, b2UUID, updatedBody),
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
noteBookUUID: b1UUID,
+ notePublic: false,
noteBody: "original content",
noteDeleted: false,
expectedNoteBookUUID: b2UUID,
expectedNoteBody: "some updated content",
expectedNoteBookName: "js",
+ expectedNotePublic: false,
},
{
payload: fmt.Sprintf(`{
@@ -157,16 +167,77 @@ func TestUpdateNote(t *testing.T) {
}`, b1UUID, updatedBody),
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
noteBookUUID: b1UUID,
+ notePublic: false,
noteBody: "",
noteDeleted: true,
expectedNoteBookUUID: b1UUID,
expectedNoteBody: updatedBody,
expectedNoteBookName: "js",
+ expectedNotePublic: false,
+ },
+ {
+ payload: fmt.Sprintf(`{
+ "public": %t
+ }`, true),
+ noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
+ noteBookUUID: b1UUID,
+ notePublic: false,
+ noteBody: "original content",
+ noteDeleted: false,
+ expectedNoteBookUUID: b1UUID,
+ expectedNoteBody: "original content",
+ expectedNoteBookName: "css",
+ expectedNotePublic: true,
+ },
+ {
+ payload: fmt.Sprintf(`{
+ "public": %t
+ }`, false),
+ noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
+ noteBookUUID: b1UUID,
+ notePublic: true,
+ noteBody: "original content",
+ noteDeleted: false,
+ expectedNoteBookUUID: b1UUID,
+ expectedNoteBody: "original content",
+ expectedNoteBookName: "css",
+ expectedNotePublic: false,
+ },
+ {
+ payload: fmt.Sprintf(`{
+ "content": "%s",
+ "public": %t
+ }`, updatedBody, false),
+ noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
+ noteBookUUID: b1UUID,
+ notePublic: true,
+ noteBody: "original content",
+ noteDeleted: false,
+ expectedNoteBookUUID: b1UUID,
+ expectedNoteBody: updatedBody,
+ expectedNoteBookName: "css",
+ expectedNotePublic: false,
+ },
+ {
+ payload: fmt.Sprintf(`{
+ "book_uuid": "%s",
+ "content": "%s",
+ "public": %t
+ }`, b2UUID, updatedBody, true),
+ noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
+ noteBookUUID: b1UUID,
+ notePublic: false,
+ noteBody: "original content",
+ noteDeleted: false,
+ expectedNoteBookUUID: b2UUID,
+ expectedNoteBody: updatedBody,
+ expectedNoteBookName: "js",
+ expectedNotePublic: true,
},
}
for idx, tc := range testCases {
- func() {
+ t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
defer testutils.ClearData()
db := database.DBConn
@@ -198,6 +269,7 @@ func TestUpdateNote(t *testing.T) {
BookUUID: tc.noteBookUUID,
Body: tc.noteBody,
Deleted: tc.noteDeleted,
+ Public: tc.notePublic,
}
testutils.MustExec(t, db.Save(¬e), "preparing note")
@@ -207,7 +279,7 @@ func TestUpdateNote(t *testing.T) {
res := testutils.HTTPAuthDo(t, req, user)
// Test
- assert.StatusCodeEquals(t, res, http.StatusOK, fmt.Sprintf("status code mismatch for test case %d", idx))
+ assert.StatusCodeEquals(t, res, http.StatusOK, "status code mismatch for test case")
var bookRecord database.Book
var noteRecord database.Note
@@ -222,13 +294,14 @@ func TestUpdateNote(t *testing.T) {
assert.Equalf(t, bookCount, 2, "book count mismatch")
assert.Equalf(t, noteCount, 1, "note count mismatch")
- assert.Equal(t, noteRecord.UUID, tc.noteUUID, fmt.Sprintf("note uuid mismatch for test case %d", idx))
- assert.Equal(t, noteRecord.Body, tc.expectedNoteBody, fmt.Sprintf("note content mismatch for test case %d", idx))
- assert.Equal(t, noteRecord.BookUUID, tc.expectedNoteBookUUID, fmt.Sprintf("note book_uuid mismatch for test case %d", idx))
- assert.Equal(t, noteRecord.USN, 102, fmt.Sprintf("note usn mismatch for test case %d", idx))
+ assert.Equal(t, noteRecord.UUID, tc.noteUUID, "note uuid mismatch for test case")
+ assert.Equal(t, noteRecord.Body, tc.expectedNoteBody, "note content mismatch for test case")
+ assert.Equal(t, noteRecord.BookUUID, tc.expectedNoteBookUUID, "note book_uuid mismatch for test case")
+ assert.Equal(t, noteRecord.Public, tc.expectedNotePublic, "note public mismatch for test case")
+ assert.Equal(t, noteRecord.USN, 102, "note usn mismatch for test case")
- assert.Equal(t, userRecord.MaxUSN, 102, fmt.Sprintf("user max_usn mismatch for test case %d", idx))
- }()
+ assert.Equal(t, userRecord.MaxUSN, 102, "user max_usn mismatch for test case")
+ })
}
}
diff --git a/pkg/server/api/handlers/v3_sync.go b/pkg/server/handlers/v3_sync.go
similarity index 96%
rename from pkg/server/api/handlers/v3_sync.go
rename to pkg/server/handlers/v3_sync.go
index 197a2d39..0994fa94 100644
--- a/pkg/server/api/handlers/v3_sync.go
+++ b/pkg/server/handlers/v3_sync.go
@@ -26,7 +26,7 @@ import (
"strconv"
"time"
- "github.com/dnote/dnote/pkg/server/api/helpers"
+ "github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/pkg/errors"
@@ -252,19 +252,19 @@ type GetSyncFragmentResp struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
afterUSN, limit, err := parseGetSyncFragmentQuery(r.URL.Query())
if err != nil {
- handleError(w, "parsing query params", err, http.StatusInternalServerError)
+ HandleError(w, "parsing query params", err, http.StatusInternalServerError)
return
}
fragment, err := a.newFragment(user.ID, user.MaxUSN, afterUSN, limit)
if err != nil {
- handleError(w, "getting fragment", err, http.StatusInternalServerError)
+ HandleError(w, "getting fragment", err, http.StatusInternalServerError)
return
}
@@ -285,7 +285,7 @@ type GetSyncStateResp struct {
func (a *App) 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)
+ HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/api/handlers/v3_sync_test.go b/pkg/server/handlers/v3_sync_test.go
similarity index 100%
rename from pkg/server/api/handlers/v3_sync_test.go
rename to pkg/server/handlers/v3_sync_test.go
diff --git a/pkg/server/api/helpers/const.go b/pkg/server/helpers/const.go
similarity index 100%
rename from pkg/server/api/helpers/const.go
rename to pkg/server/helpers/const.go
diff --git a/pkg/server/api/helpers/helpers.go b/pkg/server/helpers/helpers.go
similarity index 100%
rename from pkg/server/api/helpers/helpers.go
rename to pkg/server/helpers/helpers.go
diff --git a/pkg/server/mailer/mailer.go b/pkg/server/mailer/mailer.go
index 410e97bb..55177c37 100644
--- a/pkg/server/mailer/mailer.go
+++ b/pkg/server/mailer/mailer.go
@@ -155,7 +155,7 @@ func (e *Email) Send() error {
if os.Getenv("GO_ENV") != "PRODUCTION" {
fmt.Println("Not sending email because not production")
fmt.Println(e.subject, e.to, e.from)
- fmt.Println("Body", e.Body)
+ // fmt.Println("Body", e.Body)
return nil
}
diff --git a/pkg/server/main.go b/pkg/server/main.go
index 211a649c..9615a653 100644
--- a/pkg/server/main.go
+++ b/pkg/server/main.go
@@ -26,13 +26,13 @@ import (
"os"
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/handlers"
"github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/handlers"
"github.com/dnote/dnote/pkg/server/job"
"github.com/dnote/dnote/pkg/server/mailer"
+ "github.com/dnote/dnote/pkg/server/web"
"github.com/gobuffalo/packr/v2"
- "github.com/gorilla/mux"
"github.com/pkg/errors"
)
@@ -53,44 +53,18 @@ func mustFind(box *packr.Box, path string) []byte {
return b
}
-func getStaticHandler() http.Handler {
- box := packr.New("static", "../../web/public/static")
+func initContext() web.Context {
+ staticBox := packr.New("static", "../../web/public/static")
- return http.StripPrefix("/static/", http.FileServer(box))
-}
-
-// getRootHandler returns an HTTP handler that serves the app shell
-func getRootHandler() http.HandlerFunc {
- b := mustFind(rootBox, "index.html")
-
- return func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Cache-Control", "no-cache")
- w.Write(b)
+ return web.Context{
+ IndexHTML: mustFind(rootBox, "index.html"),
+ RobotsTxt: mustFind(rootBox, "robots.txt"),
+ ServiceWorkerJs: mustFind(rootBox, "service-worker.js"),
+ StaticFileSystem: staticBox,
}
}
-func getRobotsHandler() http.HandlerFunc {
- b := mustFind(rootBox, "robots.txt")
-
- return func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Cache-Control", "no-cache")
- w.Write(b)
- }
-}
-
-func getSWHandler() http.HandlerFunc {
- b := mustFind(rootBox, "service-worker.js")
-
- return func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Content-Type", "application/javascript")
- w.Write(b)
- }
-}
-
-func initServer() (*mux.Router, error) {
- srv := mux.NewRouter()
-
+func initServer() (*http.ServeMux, error) {
apiRouter, err := handlers.NewRouter(&handlers.App{
Clock: clock.New(),
StripeAPIBackend: nil,
@@ -100,15 +74,16 @@ func initServer() (*mux.Router, error) {
return nil, errors.Wrap(err, "initializing router")
}
- srv.PathPrefix("/api").Handler(http.StripPrefix("/api", apiRouter))
- srv.PathPrefix("/static").Handler(getStaticHandler())
- srv.Handle("/service-worker.js", getSWHandler())
- srv.Handle("/robots.txt", getRobotsHandler())
+ ctx := initContext()
- // For all other requests, serve the index.html file
- srv.PathPrefix("/").Handler(getRootHandler())
+ mux := http.NewServeMux()
+ mux.Handle("/api/", http.StripPrefix("/api", apiRouter))
+ mux.Handle("/static/", web.GetStaticHandler(ctx.StaticFileSystem))
+ mux.HandleFunc("/service-worker.js", web.GetSWHandler(ctx.ServiceWorkerJs))
+ mux.HandleFunc("/robots.txt", web.GetRobotsHandler(ctx.RobotsTxt))
+ mux.HandleFunc("/", web.GetRootHandler(ctx.IndexHTML))
- return srv, nil
+ return mux, nil
}
func startCmd() {
@@ -137,8 +112,7 @@ func startCmd() {
}
log.Printf("Dnote version %s is running on port %s", versionTag, *port)
- addr := fmt.Sprintf(":%s", *port)
- log.Fatalln(http.ListenAndServe(addr, srv))
+ log.Fatalln(http.ListenAndServe(":"+*port, srv))
}
func versionCmd() {
diff --git a/pkg/server/api/operations/books.go b/pkg/server/operations/books.go
similarity index 98%
rename from pkg/server/api/operations/books.go
rename to pkg/server/operations/books.go
index dcaadc62..cb3bc77d 100644
--- a/pkg/server/api/operations/books.go
+++ b/pkg/server/operations/books.go
@@ -20,7 +20,7 @@ package operations
import (
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/helpers"
+ "github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/database"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
diff --git a/pkg/server/api/operations/books_test.go b/pkg/server/operations/books_test.go
similarity index 100%
rename from pkg/server/api/operations/books_test.go
rename to pkg/server/operations/books_test.go
diff --git a/pkg/server/api/operations/helpers.go b/pkg/server/operations/helpers.go
similarity index 100%
rename from pkg/server/api/operations/helpers.go
rename to pkg/server/operations/helpers.go
diff --git a/pkg/server/api/operations/helpers_test.go b/pkg/server/operations/helpers_test.go
similarity index 100%
rename from pkg/server/api/operations/helpers_test.go
rename to pkg/server/operations/helpers_test.go
diff --git a/pkg/server/api/operations/notes.go b/pkg/server/operations/notes.go
similarity index 65%
rename from pkg/server/api/operations/notes.go
rename to pkg/server/operations/notes.go
index 297124aa..0395c6d3 100644
--- a/pkg/server/api/operations/notes.go
+++ b/pkg/server/operations/notes.go
@@ -20,8 +20,9 @@ package operations
import (
"github.com/dnote/dnote/pkg/clock"
- "github.com/dnote/dnote/pkg/server/api/helpers"
"github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/helpers"
+ "github.com/dnote/dnote/pkg/server/permissions"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
@@ -78,18 +79,55 @@ func CreateNote(user database.User, clock clock.Clock, bookUUID, content string,
return note, nil
}
+// UpdateNoteParams is the parameters for updating a note
+type UpdateNoteParams struct {
+ BookUUID *string
+ Content *string
+ Public *bool
+}
+
+// GetBookUUID gets the bookUUID from the UpdateNoteParams
+func (r UpdateNoteParams) GetBookUUID() string {
+ if r.BookUUID == nil {
+ return ""
+ }
+
+ return *r.BookUUID
+}
+
+// GetContent gets the content from the UpdateNoteParams
+func (r UpdateNoteParams) GetContent() string {
+ if r.Content == nil {
+ return ""
+ }
+
+ return *r.Content
+}
+
+// GetPublic gets the public field from the UpdateNoteParams
+func (r UpdateNoteParams) GetPublic() bool {
+ if r.Public == nil {
+ return false
+ }
+
+ return *r.Public
+}
+
// 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, bookUUID, content *string) (database.Note, error) {
+func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, 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")
}
- if bookUUID != nil {
- note.BookUUID = *bookUUID
+ if p.BookUUID != nil {
+ note.BookUUID = p.GetBookUUID()
}
- if content != nil {
- note.Body = *content
+ if p.Content != nil {
+ note.Body = p.GetContent()
+ }
+ if p.Public != nil {
+ note.Public = p.GetPublic()
}
note.USN = nextUSN
@@ -123,3 +161,31 @@ func DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.N
return note, nil
}
+
+// GetNote retrieves a note for the given user
+func GetNote(uuid string, user database.User) (database.Note, bool, error) {
+ zeroNote := database.Note{}
+ if !helpers.ValidateUUID(uuid) {
+ return zeroNote, false, nil
+ }
+
+ db := database.DBConn
+
+ conn := db.Where("notes.uuid = ? AND deleted = ?", uuid, false)
+ conn = database.PreloadNote(conn)
+
+ var note database.Note
+ conn = conn.Find(¬e)
+
+ if conn.RecordNotFound() {
+ return zeroNote, false, nil
+ } else if err := conn.Error; err != nil {
+ return zeroNote, false, errors.Wrap(err, "finding note")
+ }
+
+ if ok := permissions.ViewNote(&user, note); !ok {
+ return zeroNote, false, nil
+ }
+
+ return note, true, nil
+}
diff --git a/pkg/server/api/operations/notes_test.go b/pkg/server/operations/notes_test.go
similarity index 65%
rename from pkg/server/api/operations/notes_test.go
rename to pkg/server/operations/notes_test.go
index 53c1a391..17da24c6 100644
--- a/pkg/server/api/operations/notes_test.go
+++ b/pkg/server/operations/notes_test.go
@@ -123,45 +123,45 @@ func TestCreateNote(t *testing.T) {
func TestUpdateNote(t *testing.T) {
testCases := []struct {
- userUSN int
- expectedUSN int
+ userUSN int
}{
{
- userUSN: 8,
- expectedUSN: 9,
+ userUSN: 8,
},
{
- userUSN: 102229,
- expectedUSN: 102230,
+ userUSN: 102229,
},
{
- userUSN: 8099,
- expectedUSN: 8100,
+ userUSN: 8099,
},
}
for idx, tc := range testCases {
- func() {
+ t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
defer testutils.ClearData()
db := database.DBConn
user := testutils.SetupUserData()
- testutils.MustExec(t, db.Model(&user).Update("max_usn", tc.userUSN), fmt.Sprintf("preparing user max_usn for test case %d", idx))
+ testutils.MustExec(t, db.Model(&user).Update("max_usn", tc.userUSN), "preparing user max_usn for test case")
anotherUser := testutils.SetupUserData()
- testutils.MustExec(t, db.Model(&anotherUser).Update("max_usn", 55), fmt.Sprintf("preparing user max_usn for test case %d", idx))
+ testutils.MustExec(t, db.Model(&anotherUser).Update("max_usn", 55), "preparing user max_usn for test case")
b1 := database.Book{UserID: user.ID, Label: "js", Deleted: false}
- testutils.MustExec(t, db.Save(&b1), fmt.Sprintf("preparing b1 for test case %d", idx))
+ testutils.MustExec(t, db.Save(&b1), "preparing b1 for test case")
note := database.Note{UserID: user.ID, Deleted: false, Body: "test content", BookUUID: b1.UUID}
- testutils.MustExec(t, db.Save(¬e), fmt.Sprintf("preparing note for test case %d", idx))
+ testutils.MustExec(t, db.Save(¬e), "preparing note for test case")
c := clock.NewMock()
content := "updated test content"
+ public := true
tx := db.Begin()
- if _, err := UpdateNote(tx, user, c, note, nil, &content); err != nil {
+ if _, err := UpdateNote(tx, user, c, note, &UpdateNoteParams{
+ Content: &content,
+ Public: &public,
+ }); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting note"))
}
@@ -171,20 +171,21 @@ func TestUpdateNote(t *testing.T) {
var noteRecord database.Note
var userRecord database.User
- testutils.MustExec(t, db.Model(&database.Book{}).Count(&bookCount), fmt.Sprintf("counting book for test case %d", idx))
- testutils.MustExec(t, db.Model(&database.Note{}).Count(¬eCount), fmt.Sprintf("counting notes for test case %d", idx))
- testutils.MustExec(t, db.First(¬eRecord), fmt.Sprintf("finding note for test case %d", idx))
- testutils.MustExec(t, db.Where("id = ?", user.ID).First(&userRecord), fmt.Sprintf("finding user for test case %d", idx))
+ testutils.MustExec(t, db.Model(&database.Book{}).Count(&bookCount), "counting book for test case")
+ testutils.MustExec(t, db.Model(&database.Note{}).Count(¬eCount), "counting notes for test case")
+ testutils.MustExec(t, db.First(¬eRecord), "finding note for test case")
+ testutils.MustExec(t, db.Where("id = ?", user.ID).First(&userRecord), "finding user for test case")
+ expectedUSN := tc.userUSN + 1
assert.Equal(t, bookCount, 1, "book count mismatch")
assert.Equal(t, noteCount, 1, "note count mismatch")
assert.Equal(t, noteRecord.UserID, user.ID, "note UserID mismatch")
assert.Equal(t, noteRecord.Body, content, "note Body mismatch")
+ assert.Equal(t, noteRecord.Public, public, "note Public mismatch")
assert.Equal(t, noteRecord.Deleted, false, "note Deleted mismatch")
- assert.Equal(t, noteRecord.USN, tc.expectedUSN, "note USN mismatch")
-
- assert.Equal(t, userRecord.MaxUSN, tc.expectedUSN, "user MaxUSN mismatch")
- }()
+ assert.Equal(t, noteRecord.USN, expectedUSN, "note USN mismatch")
+ assert.Equal(t, userRecord.MaxUSN, expectedUSN, "user MaxUSN mismatch")
+ })
}
}
@@ -255,3 +256,128 @@ func TestDeleteNote(t *testing.T) {
}()
}
}
+
+func TestGetNote(t *testing.T) {
+ user := testutils.SetupUserData()
+ anotherUser := testutils.SetupUserData()
+
+ db := database.DBConn
+ defer testutils.ClearData()
+
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+
+ privateNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "privateNote content",
+ Deleted: false,
+ Public: false,
+ }
+ testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
+
+ publicNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "privateNote content",
+ Deleted: false,
+ Public: true,
+ }
+ testutils.MustExec(t, db.Save(&publicNote), "preparing privateNote")
+
+ var privateNoteRecord, publicNoteRecord database.Note
+ testutils.MustExec(t, db.Where("uuid = ?", privateNote.UUID).Preload("Book").Preload("User").First(&privateNoteRecord), "finding privateNote")
+ testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).Preload("Book").Preload("User").First(&publicNoteRecord), "finding publicNote")
+
+ testCases := []struct {
+ name string
+ user database.User
+ note database.Note
+ expectedOK bool
+ expectedNote database.Note
+ }{
+ {
+ name: "owner accessing private note",
+ user: user,
+ note: privateNote,
+ expectedOK: true,
+ expectedNote: privateNoteRecord,
+ },
+ {
+ name: "non-owner accessing private note",
+ user: anotherUser,
+ note: privateNote,
+ expectedOK: false,
+ expectedNote: database.Note{},
+ },
+ {
+ name: "non-owner accessing public note",
+ user: anotherUser,
+ note: publicNote,
+ expectedOK: true,
+ expectedNote: publicNoteRecord,
+ },
+ {
+ name: "guest accessing private note",
+ user: database.User{},
+ note: privateNote,
+ expectedOK: false,
+ expectedNote: database.Note{},
+ },
+ {
+ name: "guest accessing public note",
+ user: database.User{},
+ note: publicNote,
+ expectedOK: true,
+ expectedNote: publicNoteRecord,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ note, ok, err := GetNote(tc.note.UUID, tc.user)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ assert.Equal(t, ok, tc.expectedOK, "ok mismatch")
+ assert.DeepEqual(t, note, tc.expectedNote, "note mismatch")
+ })
+ }
+}
+
+func TestGetNote_nonexistent(t *testing.T) {
+ user := testutils.SetupUserData()
+
+ db := database.DBConn
+ defer testutils.ClearData()
+
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+
+ n1UUID := "4fd19336-671e-4ff3-8f22-662b80e22edc"
+ n1 := database.Note{
+ UUID: n1UUID,
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "n1 content",
+ Deleted: false,
+ Public: false,
+ }
+ testutils.MustExec(t, db.Save(&n1), "preparing n1")
+
+ nonexistentUUID := "4fd19336-671e-4ff3-8f22-662b80e22edd"
+ note, ok, err := GetNote(nonexistentUUID, user)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ assert.Equal(t, ok, false, "ok mismatch")
+ assert.DeepEqual(t, note, database.Note{}, "note mismatch")
+}
diff --git a/pkg/server/api/operations/sessions.go b/pkg/server/operations/sessions.go
similarity index 93%
rename from pkg/server/api/operations/sessions.go
rename to pkg/server/operations/sessions.go
index ea261db5..980d5c7c 100644
--- a/pkg/server/api/operations/sessions.go
+++ b/pkg/server/operations/sessions.go
@@ -21,7 +21,7 @@ package operations
import (
"time"
- "github.com/dnote/dnote/pkg/server/api/crypt"
+ "github.com/dnote/dnote/pkg/server/crypt"
"github.com/dnote/dnote/pkg/server/database"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
@@ -60,7 +60,7 @@ 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.Debug().Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
+ if err := db.Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting the session")
}
diff --git a/pkg/server/api/operations/subscriptions.go b/pkg/server/operations/subscriptions.go
similarity index 100%
rename from pkg/server/api/operations/subscriptions.go
rename to pkg/server/operations/subscriptions.go
diff --git a/pkg/server/api/operations/users.go b/pkg/server/operations/users.go
similarity index 98%
rename from pkg/server/api/operations/users.go
rename to pkg/server/operations/users.go
index f55a6437..fd88dd18 100644
--- a/pkg/server/api/operations/users.go
+++ b/pkg/server/operations/users.go
@@ -21,7 +21,7 @@ package operations
import (
"time"
- "github.com/dnote/dnote/pkg/server/api/crypt"
+ "github.com/dnote/dnote/pkg/server/crypt"
"github.com/dnote/dnote/pkg/server/database"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
diff --git a/pkg/server/permissions/permissions.go b/pkg/server/permissions/permissions.go
new file mode 100644
index 00000000..c2004547
--- /dev/null
+++ b/pkg/server/permissions/permissions.go
@@ -0,0 +1,38 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package permissions
+
+import (
+ "github.com/dnote/dnote/pkg/server/database"
+)
+
+// ViewNote checks if the given user can view the given note
+func ViewNote(user *database.User, note database.Note) bool {
+ if note.Public {
+ return true
+ }
+ if user == nil {
+ return false
+ }
+ if note.UserID == 0 {
+ return false
+ }
+
+ return note.UserID == user.ID
+}
diff --git a/pkg/server/permissions/permissions_test.go b/pkg/server/permissions/permissions_test.go
new file mode 100644
index 00000000..a273d4df
--- /dev/null
+++ b/pkg/server/permissions/permissions_test.go
@@ -0,0 +1,93 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package permissions
+
+import (
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/testutils"
+)
+
+func init() {
+ testutils.InitTestDB()
+}
+
+func TestViewNote(t *testing.T) {
+ user := testutils.SetupUserData()
+ anotherUser := testutils.SetupUserData()
+
+ db := database.DBConn
+ defer testutils.ClearData()
+
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+
+ privateNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "privateNote content",
+ Deleted: false,
+ Public: false,
+ }
+ testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
+
+ publicNote := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Body: "privateNote content",
+ Deleted: false,
+ Public: true,
+ }
+ testutils.MustExec(t, db.Save(&publicNote), "preparing privateNote")
+
+ t.Run("owner accessing private note", func(t *testing.T) {
+ result := ViewNote(&user, privateNote)
+ assert.Equal(t, result, true, "result mismatch")
+ })
+
+ t.Run("owner accessing public note", func(t *testing.T) {
+ result := ViewNote(&user, publicNote)
+ assert.Equal(t, result, true, "result mismatch")
+ })
+
+ t.Run("non-owner accessing private note", func(t *testing.T) {
+ result := ViewNote(&anotherUser, privateNote)
+ assert.Equal(t, result, false, "result mismatch")
+ })
+
+ t.Run("non-owner accessing public note", func(t *testing.T) {
+ result := ViewNote(&anotherUser, publicNote)
+ assert.Equal(t, result, true, "result mismatch")
+ })
+
+ t.Run("guest accessing private note", func(t *testing.T) {
+ result := ViewNote(nil, privateNote)
+ assert.Equal(t, result, false, "result mismatch")
+ })
+
+ t.Run("guest accessing public note", func(t *testing.T) {
+ result := ViewNote(nil, publicNote)
+ assert.Equal(t, result, true, "result mismatch")
+ })
+}
diff --git a/pkg/server/api/presenters/book.go b/pkg/server/presenters/book.go
similarity index 100%
rename from pkg/server/api/presenters/book.go
rename to pkg/server/presenters/book.go
diff --git a/pkg/server/api/presenters/digest.go b/pkg/server/presenters/digest.go
similarity index 100%
rename from pkg/server/api/presenters/digest.go
rename to pkg/server/presenters/digest.go
diff --git a/pkg/server/api/presenters/email_preference.go b/pkg/server/presenters/email_preference.go
similarity index 100%
rename from pkg/server/api/presenters/email_preference.go
rename to pkg/server/presenters/email_preference.go
diff --git a/pkg/server/api/presenters/helpers.go b/pkg/server/presenters/helpers.go
similarity index 100%
rename from pkg/server/api/presenters/helpers.go
rename to pkg/server/presenters/helpers.go
diff --git a/pkg/server/api/presenters/note.go b/pkg/server/presenters/note.go
similarity index 100%
rename from pkg/server/api/presenters/note.go
rename to pkg/server/presenters/note.go
diff --git a/pkg/server/api/presenters/repetition_rule.go b/pkg/server/presenters/repetition_rule.go
similarity index 100%
rename from pkg/server/api/presenters/repetition_rule.go
rename to pkg/server/presenters/repetition_rule.go
diff --git a/pkg/server/api/presenters/repetition_rule_test.go b/pkg/server/presenters/repetition_rule_test.go
similarity index 100%
rename from pkg/server/api/presenters/repetition_rule_test.go
rename to pkg/server/presenters/repetition_rule_test.go
diff --git a/pkg/server/api/scripts/makeDemoDigests/main.go b/pkg/server/scripts/makeDemoDigests/main.go
similarity index 99%
rename from pkg/server/api/scripts/makeDemoDigests/main.go
rename to pkg/server/scripts/makeDemoDigests/main.go
index b41d39cb..b878a41a 100644
--- a/pkg/server/api/scripts/makeDemoDigests/main.go
+++ b/pkg/server/scripts/makeDemoDigests/main.go
@@ -19,7 +19,7 @@
package main
import (
- "github.com/dnote/dnote/pkg/server/api/helpers"
+ "github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/database"
"os"
"time"
diff --git a/pkg/server/testutils/main.go b/pkg/server/testutils/main.go
index cacd6f1a..2621ba2d 100644
--- a/pkg/server/testutils/main.go
+++ b/pkg/server/testutils/main.go
@@ -20,8 +20,10 @@
package testutils
import (
+ "encoding/base64"
"encoding/json"
"fmt"
+ "math/rand"
"net/http"
"net/http/httptest"
"os"
@@ -36,6 +38,10 @@ import (
"golang.org/x/crypto/bcrypt"
)
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
// InitTestDB establishes connection pool with the test database specified by
// the environment variable configuration and initalizes a new schema
func InitTestDB() {
@@ -209,8 +215,13 @@ func HTTPDo(t *testing.T, req *http.Request) *http.Response {
func HTTPAuthDo(t *testing.T, req *http.Request, user database.User) *http.Response {
db := database.DBConn
+ b := make([]byte, 32)
+ if _, err := rand.Read(b); err != nil {
+ t.Fatal(errors.Wrap(err, "reading random bits"))
+ }
+
session := database.Session{
- Key: "Vvgm3eBXfXGEFWERI7faiRJ3DAzJw+7DdT9J1LEyNfI=",
+ Key: base64.StdEncoding.EncodeToString(b),
UserID: user.ID,
ExpiresAt: time.Now().Add(time.Hour * 10 * 24),
}
@@ -225,8 +236,8 @@ func HTTPAuthDo(t *testing.T, req *http.Request, user database.User) *http.Respo
}
// MakeReq makes an HTTP request and returns a response
-func MakeReq(server *httptest.Server, method, url, data string) *http.Request {
- endpoint := fmt.Sprintf("%s%s", server.URL, url)
+func MakeReq(server *httptest.Server, method, path, data string) *http.Request {
+ endpoint := fmt.Sprintf("%s%s", server.URL, path)
req, err := http.NewRequest(method, endpoint, strings.NewReader(data))
if err != nil {
diff --git a/pkg/server/tmpl/app.go b/pkg/server/tmpl/app.go
new file mode 100644
index 00000000..abbc53b7
--- /dev/null
+++ b/pkg/server/tmpl/app.go
@@ -0,0 +1,101 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package tmpl
+
+import (
+ "bytes"
+ "html/template"
+ "net/http"
+ "regexp"
+
+ "github.com/pkg/errors"
+)
+
+// routes
+var notesPathRegex = regexp.MustCompile("^/notes/([^/]+)$")
+
+// template names
+var templateIndex = "index"
+var templateNoteMetaTags = "note_metatags"
+
+// AppShell represents the application in HTML
+type AppShell struct {
+ 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) {
+ t, err := template.New(templateIndex).Parse(string(content))
+ if err != nil {
+ return AppShell{}, errors.Wrap(err, "parsing the index template")
+ }
+
+ _, err = t.New(templateNoteMetaTags).Parse(noteMetaTags)
+ if err != nil {
+ return AppShell{}, errors.Wrap(err, "parsing the note meta tags template")
+ }
+
+ return AppShell{t}, nil
+}
+
+// Execute executes the index template
+func (a AppShell) Execute(r *http.Request) ([]byte, error) {
+ data, err := a.getData(r)
+ if err != nil {
+ return nil, errors.Wrap(err, "getting data")
+ }
+
+ var buf bytes.Buffer
+ if err := a.T.ExecuteTemplate(&buf, templateIndex, data); err != nil {
+ return nil, errors.Wrap(err, "executing template")
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (a AppShell) getData(r *http.Request) (tmplData, error) {
+ path := r.URL.Path
+
+ if ok, params := matchPath(path, notesPathRegex); ok {
+ p, err := a.newNotePage(r, params[0])
+ if err != nil {
+ return tmplData{}, errors.Wrap(err, "instantiating note page")
+ }
+
+ return p.getData()
+ }
+
+ p := defaultPage{}
+ return p.getData(), nil
+}
+
+// matchPath checks if the given path matches the given regular expressions
+// and returns a boolean as well as any parameters from regex capture groups.
+func matchPath(p string, reg *regexp.Regexp) (bool, []string) {
+ match := notesPathRegex.FindStringSubmatch(p)
+
+ if len(match) > 0 {
+ return true, match[1:]
+ }
+
+ return false, nil
+}
diff --git a/pkg/server/tmpl/app_test.go b/pkg/server/tmpl/app_test.go
new file mode 100644
index 00000000..6431eb57
--- /dev/null
+++ b/pkg/server/tmpl/app_test.go
@@ -0,0 +1,92 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package tmpl
+
+import (
+ "fmt"
+ "net/http"
+ "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 init() {
+ testutils.InitTestDB()
+}
+
+func TestAppShellExecute(t *testing.T) {
+ t.Run("home", func(t *testing.T) {
+ a, err := NewAppShell([]byte("
{{ .Title }}{{ .MetaTags }}"))
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "preparing app shell"))
+ }
+
+ r, err := http.NewRequest("GET", "http://mock.url/", nil)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "preparing request"))
+ }
+
+ b, err := a.Execute(r)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ assert.Equal(t, string(b), "Dnote", "result mismatch")
+ })
+
+ t.Run("note", func(t *testing.T) {
+ defer testutils.ClearData()
+ db := database.DBConn
+
+ user := testutils.SetupUserData()
+ b1 := database.Book{
+ UserID: user.ID,
+ Label: "js",
+ }
+ testutils.MustExec(t, db.Save(&b1), "preparing b1")
+ n1 := database.Note{
+ UserID: user.ID,
+ BookUUID: b1.UUID,
+ Public: true,
+ Body: "n1 content",
+ }
+ testutils.MustExec(t, db.Save(&n1), "preparing note")
+
+ a, err := NewAppShell([]byte("{{ .MetaTags }}"))
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "preparing app shell"))
+ }
+
+ endpoint := fmt.Sprintf("http://mock.url/notes/%s", n1.UUID)
+ r, err := http.NewRequest("GET", endpoint, nil)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "preparing request"))
+ }
+
+ b, err := a.Execute(r)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ assert.NotEqual(t, string(b), "", "result should not be empty")
+ })
+}
diff --git a/pkg/server/tmpl/data.go b/pkg/server/tmpl/data.go
new file mode 100644
index 00000000..b0c9790d
--- /dev/null
+++ b/pkg/server/tmpl/data.go
@@ -0,0 +1,141 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package tmpl
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/handlers"
+ "github.com/dnote/dnote/pkg/server/operations"
+ "github.com/pkg/errors"
+)
+
+var newlineRegexp = regexp.MustCompile(`\r?\n`)
+
+// tmplData is the data to be passed to the app shell template
+type tmplData struct {
+ Title string
+ MetaTags template.HTML
+}
+
+type noteMetaTagsData struct {
+ Title string
+ Description string
+}
+
+type notePage struct {
+ Note database.Note
+ T *template.Template
+}
+
+func (a AppShell) newNotePage(r *http.Request, noteUUID string) (notePage, error) {
+ user, _, err := handlers.AuthWithSession(r, nil)
+ if err != nil {
+ return notePage{}, errors.Wrap(err, "authenticating with session")
+ }
+
+ note, ok, err := operations.GetNote(noteUUID, user)
+
+ if !ok {
+ return notePage{}, ErrNotFound
+ }
+ if err != nil {
+ return notePage{}, errors.Wrap(err, "getting note")
+ }
+
+ return notePage{note, a.T}, nil
+}
+
+func (p notePage) getTitle() string {
+ note := p.Note
+ date := time.Unix(0, note.AddedOn).Format("Jan 2 2006")
+
+ return fmt.Sprintf("Note: %s (%s)", note.Book.Label, date)
+}
+
+func excerpt(s string, maxLen int) string {
+ if len(s) > maxLen {
+
+ var lastIdx int
+ if maxLen > 3 {
+ lastIdx = maxLen - 3
+ } else {
+ lastIdx = maxLen
+ }
+
+ return s[:lastIdx] + "..."
+ }
+
+ return s
+}
+
+func formatMetaDescContent(s string) string {
+ desc := excerpt(s, 200)
+ desc = strings.Trim(desc, " ")
+
+ return newlineRegexp.ReplaceAllString(desc, " ")
+}
+
+func (p notePage) getMetaTags() (template.HTML, error) {
+ title := p.getTitle()
+ desc := formatMetaDescContent(p.Note.Body)
+
+ data := noteMetaTagsData{
+ Title: title,
+ Description: desc,
+ }
+
+ var buf bytes.Buffer
+ if err := p.T.ExecuteTemplate(&buf, templateNoteMetaTags, data); err != nil {
+ return "", errors.Wrap(err, "executing template")
+ }
+
+ return template.HTML(buf.String()), nil
+}
+
+func (p notePage) getData() (tmplData, error) {
+ mt, err := p.getMetaTags()
+ if err != nil {
+ return tmplData{}, errors.Wrap(err, "getting meta tags")
+ }
+
+ dat := tmplData{
+ Title: p.getTitle(),
+ MetaTags: mt,
+ }
+
+ return dat, nil
+}
+
+type defaultPage struct {
+}
+
+func (p defaultPage) getData() tmplData {
+ return tmplData{
+ Title: "Dnote",
+ MetaTags: "",
+ }
+}
diff --git a/pkg/server/tmpl/data_test.go b/pkg/server/tmpl/data_test.go
new file mode 100644
index 00000000..d0709ec5
--- /dev/null
+++ b/pkg/server/tmpl/data_test.go
@@ -0,0 +1,63 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package tmpl
+
+import (
+ "html/template"
+ "testing"
+ "time"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/database"
+ "github.com/pkg/errors"
+)
+
+func TestDefaultPageGetData(t *testing.T) {
+ p := defaultPage{}
+
+ result := p.getData()
+
+ assert.Equal(t, result.MetaTags, template.HTML(""), "MetaTags mismatch")
+ assert.Equal(t, result.Title, "Dnote", "Title mismatch")
+}
+
+func TestNotePageGetData(t *testing.T) {
+ a, err := NewAppShell(nil)
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "preparing app shell"))
+ }
+
+ p := notePage{
+ Note: database.Note{
+ Book: database.Book{
+ Label: "vocabulary",
+ },
+ AddedOn: time.Date(2019, time.January, 2, 0, 0, 0, 0, time.UTC).UnixNano(),
+ },
+ T: a.T,
+ }
+
+ result, err := p.getData()
+ if err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ assert.NotEqual(t, result.MetaTags, template.HTML(""), "MetaTags should not be empty")
+ assert.Equal(t, result.Title, "Note: vocabulary (Jan 2 2019)", "Title mismatch")
+}
diff --git a/pkg/server/tmpl/tmpl.go b/pkg/server/tmpl/tmpl.go
new file mode 100644
index 00000000..1409300d
--- /dev/null
+++ b/pkg/server/tmpl/tmpl.go
@@ -0,0 +1,29 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+package tmpl
+
+var noteMetaTags = `
+
+
+
+
+
+
+
+`
diff --git a/pkg/server/watcher/main.go b/pkg/server/watcher/main.go
index 3ff3c75f..b99c4486 100644
--- a/pkg/server/watcher/main.go
+++ b/pkg/server/watcher/main.go
@@ -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 .
+ */
+
package main
import (
diff --git a/pkg/server/web/handlers.go b/pkg/server/web/handlers.go
new file mode 100644
index 00000000..45814b52
--- /dev/null
+++ b/pkg/server/web/handlers.go
@@ -0,0 +1,83 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Dnote. If not, see .
+ */
+
+// Package web provides handlers for the web application
+package web
+
+import (
+ "net/http"
+
+ "github.com/dnote/dnote/pkg/server/handlers"
+ "github.com/dnote/dnote/pkg/server/tmpl"
+ "github.com/pkg/errors"
+)
+
+// Context contains contents of web assets
+type Context struct {
+ IndexHTML []byte
+ RobotsTxt []byte
+ ServiceWorkerJs []byte
+ StaticFileSystem http.FileSystem
+}
+
+// GetRootHandler returns an HTTP handler that serves the app shell
+func GetRootHandler(b []byte) http.HandlerFunc {
+ appShell, err := tmpl.NewAppShell(b)
+ if err != nil {
+ panic(errors.Wrap(err, "initializing app shell"))
+ }
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ // index.html must not be cached
+ w.Header().Set("Cache-Control", "no-cache")
+
+ buf, err := appShell.Execute(r)
+ if err != nil {
+ if errors.Cause(err) == tmpl.ErrNotFound {
+ handlers.RespondNotFound(w)
+ } else {
+ handlers.HandleError(w, "executing app shell", err, http.StatusInternalServerError)
+ }
+ return
+ }
+
+ w.Write(buf)
+ }
+}
+
+// GetRobotsHandler returns an HTTP handler that serves robots.txt
+func GetRobotsHandler(b []byte) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Write(b)
+ }
+}
+
+// GetSWHandler returns an HTTP handler that serves service worker
+func GetSWHandler(b []byte) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Content-Type", "application/javascript")
+ w.Write(b)
+ }
+}
+
+// GetStaticHandler returns an HTTP handler that serves static files from a filesystem
+func GetStaticHandler(root http.FileSystem) http.Handler {
+ return http.StripPrefix("/static/", http.FileServer(root))
+}
diff --git a/scripts/vagrant/install_utils.sh b/scripts/vagrant/install_utils.sh
index abd29340..1c06d413 100755
--- a/scripts/vagrant/install_utils.sh
+++ b/scripts/vagrant/install_utils.sh
@@ -2,7 +2,7 @@
set -eux
sudo apt-get update
-sudo apt-get install -y htop git wget build-essential
+sudo apt-get install -y htop git wget build-essential inotify-tools
# Install Chrome
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add
diff --git a/web/assets/index.html b/web/assets/index.html
index e87032ee..99e92b2a 100644
--- a/web/assets/index.html
+++ b/web/assets/index.html
@@ -3,7 +3,7 @@
- Dnote
+ {{ .Title }}