-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add endpoints for storing push notification device tokens
The mongodb collection maintains a unique index on the combination of the userID and some key value for the token itself (in this case a SHA-256 checksum hash). This allows for a simple upsert whenever a key is received, without having to check for duplicates or worry about appending to a list or anything like that. If the received key is a duplicate, it will replace the old one. Otherwise a new one is inserted. While only Apple device tokens are supported now, care has been taken to allow for other manufacturers' tokens in the future. In theory, that would mean adding a property to the DeviceToken struct, presumably "Android". Then modifying the DeviceToken's key() method to produce a key from either the Apple token, or Android token, whichever is found. Validation should ensure that only one or the other exists. BACK-2506
- Loading branch information
Showing
15 changed files
with
562 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package v1 | ||
|
||
import ( | ||
data "github.com/tidepool-org/platform/data/service" | ||
"github.com/tidepool-org/platform/devicetokens" | ||
"github.com/tidepool-org/platform/request" | ||
platform "github.com/tidepool-org/platform/service" | ||
"github.com/tidepool-org/platform/service/api" | ||
) | ||
|
||
func DeviceTokensRoutes() []data.Route { | ||
return []data.Route{ | ||
data.MakeRoute("POST", "/v1/device-tokens/:userID", UpsertDeviceToken, api.RequireAuth), | ||
} | ||
} | ||
|
||
func UpsertDeviceToken(dCtx data.Context) { | ||
r := dCtx.Request() | ||
ctx := r.Context() | ||
authDetails := request.GetAuthDetails(ctx) | ||
repo := dCtx.DeviceTokensRepository() | ||
|
||
if err := checkAuthentication(authDetails); err != nil { | ||
dCtx.RespondWithError(platform.ErrorUnauthorized()) | ||
return | ||
} | ||
|
||
if err := checkUserIDConsistency(authDetails, r.PathParam("userID")); err != nil { | ||
dCtx.RespondWithError(platform.ErrorUnauthorized()) | ||
return | ||
} | ||
|
||
deviceToken := devicetokens.DeviceToken{} | ||
if err := request.DecodeRequestBody(r.Request, &deviceToken); err != nil { | ||
dCtx.RespondWithError(platform.ErrorJSONMalformed()) | ||
return | ||
} | ||
|
||
userID := userIDWithServiceFallback(authDetails, r.PathParam("userID")) | ||
doc := devicetokens.NewDocument(userID, deviceToken) | ||
if err := repo.Upsert(ctx, doc); err != nil { | ||
dCtx.RespondWithError(platform.ErrorInternalServerFailure()) | ||
return | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package v1 | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"net/http" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
|
||
"github.com/tidepool-org/platform/data/service/api/v1/mocks" | ||
"github.com/tidepool-org/platform/devicetokens" | ||
"github.com/tidepool-org/platform/request" | ||
) | ||
|
||
var _ = Describe("Device tokens endpoints", func() { | ||
|
||
Describe("Upsert", func() { | ||
It("succeeds with valid input", func() { | ||
t := GinkgoT() | ||
body := buff(`{"apple":{"environment":"sandbox","token":"b3BhcXVldG9rZW4="}}`) | ||
dCtx := mocks.NewContext(t, "", "", body) | ||
repo := newMockDeviceTokensRepo() | ||
dCtx.MockDeviceTokensRepository = repo | ||
|
||
UpsertDeviceToken(dCtx) | ||
|
||
rec := dCtx.Recorder() | ||
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String()) | ||
}) | ||
|
||
It("rejects unauthenticated users", func() { | ||
t := GinkgoT() | ||
body := buff(`{"apple":{"environment":"sandbox","token":"blah"}}`) | ||
dCtx := mocks.NewContext(t, "", "", body) | ||
dCtx.MockAlertsRepository = newMockRepo() | ||
badDetails := mocks.NewAuthDetails(request.MethodSessionToken, "", "") | ||
dCtx.WithAuthDetails(badDetails) | ||
|
||
UpsertDeviceToken(dCtx) | ||
|
||
rec := dCtx.Recorder() | ||
Expect(rec.Code).To(Equal(http.StatusForbidden)) | ||
}) | ||
|
||
It("accepts authenticated service users", func() { | ||
t := GinkgoT() | ||
body := buff(`{"apple":{"environment":"sandbox","token":"blah"}}`) | ||
dCtx := mocks.NewContext(t, "", "", body) | ||
dCtx.WithAuthDetails(mocks.ServiceAuthDetails()) | ||
dCtx.MockDeviceTokensRepository = newMockDeviceTokensRepo() | ||
|
||
UpsertDeviceToken(dCtx) | ||
|
||
rec := dCtx.Recorder() | ||
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String()) | ||
}) | ||
|
||
It("requires that the user's token matches the userID path param", func() { | ||
t := GinkgoT() | ||
dCtx := mocks.NewContext(t, "", "", nil) | ||
dCtx.RESTRequest.PathParams["userID"] = "bad" | ||
repo := newMockDeviceTokensRepo() | ||
dCtx.MockDeviceTokensRepository = repo | ||
|
||
UpsertDeviceToken(dCtx) | ||
|
||
rec := dCtx.Recorder() | ||
Expect(rec.Code).To(Equal(http.StatusForbidden)) | ||
}) | ||
|
||
It("errors on invalid JSON for device tokens", func() { | ||
t := GinkgoT() | ||
body := bytes.NewBuffer([]byte(`"improper JSON data"`)) | ||
dCtx := mocks.NewContext(t, "", "", body) | ||
repo := newMockDeviceTokensRepo() | ||
dCtx.MockDeviceTokensRepository = repo | ||
|
||
UpsertDeviceToken(dCtx) | ||
|
||
rec := dCtx.Recorder() | ||
Expect(rec.Code).To(Equal(http.StatusBadRequest)) | ||
}) | ||
|
||
}) | ||
|
||
}) | ||
|
||
type mockDeviceTokensRepo struct { | ||
UserID string | ||
Error error | ||
} | ||
|
||
func newMockDeviceTokensRepo() *mockDeviceTokensRepo { | ||
return &mockDeviceTokensRepo{} | ||
} | ||
|
||
func (r *mockDeviceTokensRepo) ReturnsError(err error) { | ||
r.Error = err | ||
} | ||
|
||
func (r *mockDeviceTokensRepo) Upsert(ctx context.Context, conf *devicetokens.Document) error { | ||
if r.Error != nil { | ||
return r.Error | ||
} | ||
if conf != nil { | ||
r.UserID = conf.UserID | ||
} | ||
return nil | ||
} | ||
|
||
func (r *mockDeviceTokensRepo) EnsureIndexes() error { | ||
return nil | ||
} | ||
|
||
func buff(template string, args ...any) *bytes.Buffer { | ||
return bytes.NewBuffer([]byte(fmt.Sprintf(template, args...))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package mongo | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"go.mongodb.org/mongo-driver/bson" | ||
"go.mongodb.org/mongo-driver/mongo" | ||
"go.mongodb.org/mongo-driver/mongo/options" | ||
|
||
"github.com/tidepool-org/platform/devicetokens" | ||
structuredmongo "github.com/tidepool-org/platform/store/structured/mongo" | ||
) | ||
|
||
// deviceTokensRepo implements devicetokens.Repository, writing data to a | ||
// MongoDB collection. | ||
type deviceTokensRepo structuredmongo.Repository | ||
|
||
// Upsert will create or update the given Config. | ||
func (r *deviceTokensRepo) Upsert(ctx context.Context, doc *devicetokens.Document) error { | ||
// The presence of UserID and TokenID should be enforced with a mongodb | ||
// index, but better safe than sorry. | ||
if doc.UserID == "" { | ||
return fmt.Errorf("UserID may not be empty") | ||
} | ||
if doc.TokenID == "" { | ||
return fmt.Errorf("TokenID may not be empty") | ||
} | ||
|
||
opts := options.Update().SetUpsert(true) | ||
_, err := r.UpdateOne(ctx, r.filter(doc), bson.M{"$set": doc}, opts) | ||
if err != nil { | ||
return fmt.Errorf("upserting device token: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// EnsureIndexes to maintain index constraints. | ||
func (r *deviceTokensRepo) EnsureIndexes() error { | ||
repo := structuredmongo.Repository(*r) | ||
return (&repo).CreateAllIndexes(context.Background(), []mongo.IndexModel{ | ||
{ | ||
Keys: bson.D{ | ||
{Key: "userId", Value: 1}, | ||
{Key: "tokenId", Value: 1}, | ||
}, | ||
Options: options.Index(). | ||
SetUnique(true). | ||
SetName("UserIdTokenIdTypeUnique"), | ||
}, | ||
}) | ||
} | ||
|
||
func (r *deviceTokensRepo) filter(doc *devicetokens.Document) interface{} { | ||
return &devicetokens.Document{ | ||
UserID: doc.UserID, | ||
TokenID: doc.TokenID, | ||
} | ||
} |
Oops, something went wrong.