diff --git a/packages/realm-network-transport/package-lock.json b/packages/realm-network-transport/package-lock.json index 6fa732c3ca..a9faaead8a 100644 --- a/packages/realm-network-transport/package-lock.json +++ b/packages/realm-network-transport/package-lock.json @@ -1,6 +1,6 @@ { "name": "realm-network-transport", - "version": "0.4.0", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -309,9 +309,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", + "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", "dev": true }, "@babel/highlight": { @@ -328,8 +328,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "acorn": { "version": "7.2.0", @@ -344,9 +343,9 @@ "dev": true }, "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -379,11 +378,12 @@ "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "requires": { - "color-convert": "^1.9.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, "argparse": { @@ -497,17 +497,17 @@ "dev": true }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "concat-map": { "version": "0.0.1", @@ -561,9 +561,9 @@ } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "escape-string-regexp": { "version": "1.0.5", @@ -572,9 +572,9 @@ "dev": true }, "eslint-scope": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", - "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -600,8 +600,8 @@ }, "eslint-visitor-keys": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz", + "integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==", "dev": true }, "espree": { @@ -768,9 +768,9 @@ } }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "iconv-lite": { "version": "0.4.24", @@ -914,9 +914,9 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.1", @@ -946,8 +946,8 @@ }, "js-yaml": { "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -1304,11 +1304,11 @@ "dev": true }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } }, "table": { diff --git a/packages/realm-network-transport/package.json b/packages/realm-network-transport/package.json index 06d9fc294f..c6f0a00418 100644 --- a/packages/realm-network-transport/package.json +++ b/packages/realm-network-transport/package.json @@ -1,6 +1,6 @@ { "name": "realm-network-transport", - "version": "0.4.0", + "version": "0.6.0", "description": "Implements cross-platform fetching used by Realm JS", "main": "dist/bundle.cjs.js", "module": "dist/bundle.es.js", diff --git a/packages/realm-network-transport/src/MongoDBRealmError.ts b/packages/realm-network-transport/src/MongoDBRealmError.ts index 93366fdb3a..0334213ded 100644 --- a/packages/realm-network-transport/src/MongoDBRealmError.ts +++ b/packages/realm-network-transport/src/MongoDBRealmError.ts @@ -16,17 +16,27 @@ // //////////////////////////////////////////////////////////////////////////// +import { Method } from "./types"; + /** * TODO: Determine if the shape of an error response is specific to each service or widely used */ export class MongoDBRealmError extends Error { + public readonly method: Method; + public readonly url: string; public readonly statusCode: number; public readonly statusText: string; public readonly errorCode: string | undefined; public readonly link: string | undefined; - constructor(statusCode: number, statusText: string, response: any) { + constructor( + method: Method, + url: string, + statusCode: number, + statusText: string, + response: any, + ) { if ( typeof response === "object" && typeof response.error === "string" @@ -34,7 +44,11 @@ export class MongoDBRealmError extends Error { const statusSummary = statusText ? `status ${statusCode} ${statusText}` : `status ${statusCode}`; - super(`${response.error} (${statusSummary})`); + super( + `Request failed (${method} ${url}): ${response.error} (${statusSummary})`, + ); + this.method = method; + this.url = url; this.statusText = statusText; this.statusCode = statusCode; this.errorCode = response.error_code; diff --git a/packages/realm-network-transport/src/NetworkTransport.ts b/packages/realm-network-transport/src/NetworkTransport.ts index bac1b8a0e3..6f7d87a3ff 100644 --- a/packages/realm-network-transport/src/NetworkTransport.ts +++ b/packages/realm-network-transport/src/NetworkTransport.ts @@ -17,49 +17,13 @@ //////////////////////////////////////////////////////////////////////////// import { MongoDBRealmError } from "./MongoDBRealmError"; +import { NetworkTransport, Request, ResponseHandler, Headers } from "./types"; declare const process: any; declare const require: ((id: string) => any) | undefined; const isNodeProcess = typeof process === "object"; -export type Method = "GET" | "POST" | "DELETE" | "PUT"; - -export type Headers = { [name: string]: string }; - -export interface Request { - method: Method; - url: string; - timeoutMs?: number; - headers?: Headers; - body?: RequestBody | string; -} - -export interface Response { - statusCode: number; - headers: Headers; - body: string; -} - -export type SuccessCallback = (response: Response) => void; - -export type ErrorCallback = (err: Error) => void; - -export interface ResponseHandler { - onSuccess: SuccessCallback; - onError: ErrorCallback; -} - -export interface NetworkTransport { - fetchAndParse( - request: Request, - ): Promise; - fetchWithCallbacks( - request: Request, - handler: ResponseHandler, - ): void; -} - export class DefaultNetworkTransport implements NetworkTransport { public static fetch: typeof fetch; public static AbortController: typeof AbortController; @@ -128,6 +92,8 @@ export class DefaultNetworkTransport implements NetworkTransport { contentType.startsWith("application/json") ) { throw new MongoDBRealmError( + request.method, + request.url, response.status, response.statusText, await response.json(), @@ -138,9 +104,13 @@ export class DefaultNetworkTransport implements NetworkTransport { ); } } catch (err) { - throw new Error( - `Request failed (${request.method} ${request.url}): ${err.message}`, - ); + if (err instanceof MongoDBRealmError) { + throw err; + } else { + throw new Error( + `Request failed (${request.method} ${request.url}): ${err.message}`, + ); + } } } diff --git a/packages/realm-network-transport/src/index.ts b/packages/realm-network-transport/src/index.ts index 6d064d1dc3..8491d4becf 100644 --- a/packages/realm-network-transport/src/index.ts +++ b/packages/realm-network-transport/src/index.ts @@ -17,7 +17,6 @@ //////////////////////////////////////////////////////////////////////////// export { - DefaultNetworkTransport, NetworkTransport, Request, Response, @@ -25,4 +24,6 @@ export { SuccessCallback, ErrorCallback, ResponseHandler, -} from "./NetworkTransport"; +} from "./types"; +export { DefaultNetworkTransport } from "./NetworkTransport"; +export { MongoDBRealmError } from "./MongoDBRealmError"; diff --git a/packages/realm-network-transport/src/types.ts b/packages/realm-network-transport/src/types.ts new file mode 100644 index 0000000000..8bd3c46fe4 --- /dev/null +++ b/packages/realm-network-transport/src/types.ts @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2020 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +export type Method = "GET" | "POST" | "DELETE" | "PUT"; + +export type Headers = { [name: string]: string }; + +export interface Request { + method: Method; + url: string; + timeoutMs?: number; + headers?: Headers; + body?: RequestBody | string; +} + +export interface Response { + statusCode: number; + headers: Headers; + body: string; +} + +export type SuccessCallback = (response: Response) => void; + +export type ErrorCallback = (err: Error) => void; + +export interface ResponseHandler { + onSuccess: SuccessCallback; + onError: ErrorCallback; +} + +export interface NetworkTransport { + fetchAndParse( + request: Request, + ): Promise; + fetchWithCallbacks( + request: Request, + handler: ResponseHandler, + ): void; +} diff --git a/packages/realm-web-integration-tests/src/user.test.ts b/packages/realm-web-integration-tests/src/user.test.ts index fd036c1c57..edd517f411 100644 --- a/packages/realm-web-integration-tests/src/user.test.ts +++ b/packages/realm-web-integration-tests/src/user.test.ts @@ -20,7 +20,7 @@ import { expect } from "chai"; import { Credentials, UserState } from "realm-web"; -import { createApp } from "./utils"; +import { createApp, INVALID_TOKEN } from "./utils"; describe("User", () => { it("can login a user", async () => { @@ -37,4 +37,18 @@ describe("User", () => { expect(user.profile.name).equals(undefined); expect(user.customData).deep.equals({}); }); + + it("refresh invalid access tokens", async () => { + const app = createApp(); + const credentials = Credentials.anonymous(); + const user = await app.logIn(credentials, false); + // Invalidate the token + (user as any)._accessToken = INVALID_TOKEN; + expect(user.accessToken).equals(INVALID_TOKEN); + // Try using the broken access token + const response = await user.functions.translate("hello", "en_fr"); + expect(response).to.equal("bonjour"); + // Expect the user to have a diffent token now + expect(user.accessToken).not.equals(INVALID_TOKEN); + }); }); diff --git a/packages/realm-web-integration-tests/src/utils.ts b/packages/realm-web-integration-tests/src/utils.ts index 363e40cc2b..634958249d 100644 --- a/packages/realm-web-integration-tests/src/utils.ts +++ b/packages/realm-web-integration-tests/src/utils.ts @@ -48,3 +48,6 @@ export function describeIf( describe.skip(title, fn); } } + +export const INVALID_TOKEN = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwMDAwMDAsImlhdCI6MTUwMDAwMDAwMCwic3ViIjoiMTIzNDU2Nzg5MCJ9.x3-ZWVJGjEltXWWa1uaBaN4oo7dOjPbgA46STVD5KKY"; diff --git a/packages/realm-web/CHANGELOG.md b/packages/realm-web/CHANGELOG.md index 83445f1134..9584625bf5 100644 --- a/packages/realm-web/CHANGELOG.md +++ b/packages/realm-web/CHANGELOG.md @@ -1,12 +1,25 @@ ?.?.? Release notes (2020-??-??) ============================================================= +### Enhancements +* None + +### Fixed +* None + +### Internal +* None + +0.6.0 Release notes (2020-07-01) +============================================================= + ### Enhancements * Added more credentials enabling logins via additional authentication providers: Custom Functions, Custom JWT, Google, Facebook & Apple ID. ([#3019](https://github.com/realm/realm-js/pull/3019)) * Custom data can now be retrieved from an active User. ([#3019](https://github.com/realm/realm-js/pull/3019)) ### Fixed * Fixed an error "Cannot use 'in' operator to search for 'node' in undefined", which could occur when bundling the package without Node.js stubs available. ([#3001](https://github.com/realm/realm-js/pull/3001)) +* Fixed refreshing of access tokens upon 401 responses from the server. ([#3020](https://github.com/realm/realm-js/pull/3020)) ### Internal * None diff --git a/packages/realm-web/README.md b/packages/realm-web/README.md index fbac92f323..cd091db855 100644 --- a/packages/realm-web/README.md +++ b/packages/realm-web/README.md @@ -13,17 +13,12 @@ npm install realm-web As this is a beta release, it comes with a few caveats: - Most importantly, the Realm Web project *will not* include a Realm Sync client in any foreseeable future. -- A limited selection of types of [credentials for authentication providers](https://docs.mongodb.com/stitch/authentication/providers/) are implemented at the moment: - - Anonymous. - - API key. - - Email & password. - A limited selection of [services](https://docs.mongodb.com/stitch/services/) are implemented at the moment: - MongoDB (watching a collection is not yet implemented). - - HTTP (send requests using the MongoDB service as a proxy). + - HTTP: Send requests using the MongoDB service as a proxy. Some parts of the legacy Stitch SDK is still missing, most notably: - The ability to link a user to another identity. -- The types for the `Realm.Credentials` namespace is not fully implemented. - No device information is sent to the service when authenticating a user. ## Using Realm Web from Node.js diff --git a/packages/realm-web/package-lock.json b/packages/realm-web/package-lock.json index 888762dc2e..034f51815c 100644 --- a/packages/realm-web/package-lock.json +++ b/packages/realm-web/package-lock.json @@ -1,6 +1,6 @@ { "name": "realm-web", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/realm-web/package.json b/packages/realm-web/package.json index d16da9d3ef..4e15b25a46 100644 --- a/packages/realm-web/package.json +++ b/packages/realm-web/package.json @@ -1,6 +1,6 @@ { "name": "realm-web", - "version": "0.5.0", + "version": "0.6.0", "description": "Authenticate and communicate with the MongoDB Realm platform, from your web-browser", "main": "dist/bundle.cjs.js", "module": "dist/bundle.es.js", @@ -56,7 +56,7 @@ "eslint": "file:../../node_modules/eslint", "mocha": "^5.2.0", "node-fetch": "^2.6.0", - "realm-network-transport": "^0.4.0", + "realm-network-transport": "^0.6.0", "rollup": "^2.6.1", "rollup-plugin-dts": "^1.4.0", "ts-node": "^8.8.2", diff --git a/packages/realm-web/src/App.test.ts b/packages/realm-web/src/App.test.ts index 10bdb8f460..1c46ca6dfb 100644 --- a/packages/realm-web/src/App.test.ts +++ b/packages/realm-web/src/App.test.ts @@ -20,14 +20,10 @@ import { expect } from "chai"; import { App } from "./App"; import { User, UserState } from "./User"; -import { MockNetworkTransport } from "./test/MockNetworkTransport"; +import { DEFAULT_HEADERS, MockApp, MockNetworkTransport } from "./test"; import { Credentials } from "./Credentials"; import { MemoryStorage } from "./storage"; - -const DEFAULT_HEADERS = { - Accept: "application/json", - "Content-Type": "application/json", -}; +import { MongoDBRealmError } from "./transports/Transport"; /* eslint-disable @typescript-eslint/camelcase */ @@ -372,6 +368,7 @@ describe("App", () => { }); it("throws if asked to switch to or remove an unknown user", async () => { + const storage = new MemoryStorage(); const transport = new MockNetworkTransport([ { user_id: "totally-valid-user-id", @@ -381,8 +378,10 @@ describe("App", () => { ]); const app = new App({ id: "default-app-id", + storage, transport, baseUrl: "http://localhost:1337", + fetchLocation: false, }); const credentials = Credentials.anonymous(); const user = await app.logIn(credentials, false); @@ -424,6 +423,108 @@ describe("App", () => { ]); }); + it("refreshes access token and retries request exacly once, upon an 'invalid session' (401) response", async () => { + const invalidSessionError = new MongoDBRealmError( + "POST", + "http://invalid", + 401, + "", + { + error: "invalid session", + }, + ); + const app = new MockApp("default-app-id", [ + { + user_id: "bobs-id", + access_token: "first-access-token", + refresh_token: "very-refreshing", + }, + invalidSessionError, + invalidSessionError, + invalidSessionError, + { + user_id: "bobs-id", + access_token: "second-access-token", + refresh_token: "very-refreshing", + }, + { bar: "baz" }, + ]); + // Login with an anonymous user + const credentials = Credentials.anonymous(); + await app.logIn(credentials, false); + // Send a request (which will fail) + try { + await app.functions.foo({ bar: "baz" }); + throw new Error("Expected the request to fail"); + } catch (err) { + expect(err).instanceOf(MongoDBRealmError); + if (err instanceof MongoDBRealmError) { + expect(err.message).equals( + "Request failed (POST http://invalid): invalid session (status 401)", + ); + } + } + // Manually try again - this time refreshing the access token correctly + const response = await app.functions.foo({ bar: "baz" }); + expect(response).deep.equals({ bar: "baz" }); + // Expect something of the request and response + expect(app.mockTransport.requests).deep.equals([ + { + method: "POST", + url: + "http://localhost:1337/api/client/v2.0/app/default-app-id/auth/providers/anon-user/login", + body: {}, + headers: DEFAULT_HEADERS, + }, + { + method: "POST", + url: + "http://localhost:1337/api/client/v2.0/app/default-app-id/functions/call", + body: { name: "foo", arguments: [{ bar: "baz" }] }, + headers: { + Authorization: "Bearer first-access-token", + ...DEFAULT_HEADERS, + }, + }, + { + method: "POST", + url: "http://localhost:1337/api/client/v2.0/auth/session", + headers: { + Authorization: "Bearer very-refreshing", + ...DEFAULT_HEADERS, + }, + }, + { + method: "POST", + url: + "http://localhost:1337/api/client/v2.0/app/default-app-id/functions/call", + body: { name: "foo", arguments: [{ bar: "baz" }] }, + headers: { + Authorization: "Bearer first-access-token", + ...DEFAULT_HEADERS, + }, + }, + { + method: "POST", + url: "http://localhost:1337/api/client/v2.0/auth/session", + headers: { + Authorization: "Bearer very-refreshing", + ...DEFAULT_HEADERS, + }, + }, + { + method: "POST", + url: + "http://localhost:1337/api/client/v2.0/app/default-app-id/functions/call", + body: { name: "foo", arguments: [{ bar: "baz" }] }, + headers: { + Authorization: "Bearer second-access-token", + ...DEFAULT_HEADERS, + }, + }, + ]); + }); + it("expose a callable functions factory", async () => { const storage = new MemoryStorage(); const transport = new MockNetworkTransport([ diff --git a/packages/realm-web/src/User.ts b/packages/realm-web/src/User.ts index 708b9efe42..0c12115e21 100644 --- a/packages/realm-web/src/User.ts +++ b/packages/realm-web/src/User.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import type { App } from "./App"; -import { AuthenticatedTransport } from "./transports"; +import { AuthenticatedTransport, AppTransport } from "./transports"; import { UserProfile } from "./UserProfile"; import { UserStorage } from "./UserStorage"; import { FunctionsFactory } from "./FunctionsFactory"; @@ -152,7 +152,8 @@ export class User< this.transport = new AuthenticatedTransport(app.baseTransport, { currentUser: this, }); - this.functions = FunctionsFactory.create(this.transport); + const appTransport = new AppTransport(this.transport, app.id); + this.functions = FunctionsFactory.create(appTransport); this.storage = new UserStorage(app.storage, id); // Store tokens in storage for later hydration if (accessToken) { @@ -264,6 +265,23 @@ export class User< throw new Error("Not yet implemented"); } + public async refreshAccessToken() { + const response = await this.app.baseTransport.fetch({ + method: "POST", + path: "/auth/session", + headers: { + Authorization: `Bearer ${this._refreshToken}`, + }, + }); + const { access_token: accessToken } = response; + if (typeof accessToken === "string") { + this._accessToken = accessToken; + this.storage.accessToken = accessToken; + } else { + throw new Error("Expected an 'access_token' in the response"); + } + } + public async refreshCustomData() { await this.refreshAccessToken(); return this.customData; @@ -292,11 +310,6 @@ export class User< } } - private async refreshAccessToken() { - // TODO: this.storage.set(User.ACCESS_TOKEN_STORAGE_KEY, accessToken); - throw new Error("Not yet implemented"); - } - push(serviceName = ""): Realm.Services.Push { throw new Error("Not yet implemented"); } diff --git a/packages/realm-web/src/test/MockNetworkTransport.ts b/packages/realm-web/src/test/MockNetworkTransport.ts index 41a787bbbb..96edf88e80 100644 --- a/packages/realm-web/src/test/MockNetworkTransport.ts +++ b/packages/realm-web/src/test/MockNetworkTransport.ts @@ -20,6 +20,7 @@ import { NetworkTransport, Request, ResponseHandler, + MongoDBRealmError, } from "realm-network-transport"; /** @@ -58,7 +59,11 @@ export class MockNetworkTransport implements NetworkTransport { this.requests.push(request); if (this.responses.length > 0) { const [response] = this.responses.splice(0, 1); - return Promise.resolve(response); + if (response instanceof MongoDBRealmError) { + return Promise.reject(response); + } else { + return Promise.resolve(response); + } } else { throw new Error( `Unexpected request (method = ${request.method}, url = ${ diff --git a/packages/realm-web/src/test/index.ts b/packages/realm-web/src/test/index.ts new file mode 100644 index 0000000000..6a02d10c9d --- /dev/null +++ b/packages/realm-web/src/test/index.ts @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2020 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +export const DEFAULT_HEADERS = { + Accept: "application/json", + "Content-Type": "application/json", +}; + +export { MockApp } from "./MockApp"; +export { MockNetworkTransport } from "./MockNetworkTransport"; +export { MockTransport } from "./MockTransport"; diff --git a/packages/realm-web/src/transports/AppTransport.ts b/packages/realm-web/src/transports/AppTransport.ts index 0fa3c4d253..8ac5814e3b 100644 --- a/packages/realm-web/src/transports/AppTransport.ts +++ b/packages/realm-web/src/transports/AppTransport.ts @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////// -import { BaseTransport } from "./BaseTransport"; import { PrefixedTransport } from "./PrefixedTransport"; import { Transport, Request } from "./Transport"; @@ -27,7 +26,7 @@ export class AppTransport implements Transport { /** * The underlying transport used to issue requests. */ - private readonly transport: BaseTransport; + private readonly transport: Transport; /** * The id of the app. @@ -40,7 +39,7 @@ export class AppTransport implements Transport { * @param transport The base transport used to issue requests. * @param appId The id of the app. */ - constructor(transport: BaseTransport, appId: string) { + constructor(transport: Transport, appId: string) { this.transport = transport; this.appId = appId; } diff --git a/packages/realm-web/src/transports/AuthenticatedTransport.test.ts b/packages/realm-web/src/transports/AuthenticatedTransport.test.ts index c1fee63779..a69af5e4a9 100644 --- a/packages/realm-web/src/transports/AuthenticatedTransport.test.ts +++ b/packages/realm-web/src/transports/AuthenticatedTransport.test.ts @@ -19,7 +19,8 @@ import { expect } from "chai"; import { AuthenticatedTransport } from "./AuthenticatedTransport"; -import { MockTransport } from "../test/MockTransport"; +import { MockTransport } from "../test"; +import { User } from "../User"; describe("AuthenticatedTransport", () => { it("constructs", () => { @@ -32,7 +33,7 @@ describe("AuthenticatedTransport", () => { it("sends access token when requesting", async () => { const baseTransport = new MockTransport([{ foo: "bar" }]); const transport = new AuthenticatedTransport(baseTransport, { - currentUser: { accessToken: "my-access-token" } as Realm.User, + currentUser: { accessToken: "my-access-token" } as User, }); // Send a request const response = await transport.fetch({ @@ -61,7 +62,7 @@ describe("AuthenticatedTransport", () => { it("allows overwriting headers", async () => { const baseTransport = new MockTransport([{}]); const transport = new AuthenticatedTransport(baseTransport, { - currentUser: { accessToken: "my-access-token" } as Realm.User, + currentUser: { accessToken: "my-access-token" } as User, }); // Send a request await transport.fetch({ @@ -88,7 +89,7 @@ describe("AuthenticatedTransport", () => { it("returns an AuthenticatedTransport when prefixed", async () => { const baseTransport = new MockTransport([{}]); const transport = new AuthenticatedTransport(baseTransport, { - currentUser: { accessToken: "my-access-token" } as Realm.User, + currentUser: { accessToken: "my-access-token" } as User, }); const prefixedTransport = transport.prefix("/prefixed-path"); expect(prefixedTransport).to.be.instanceOf(AuthenticatedTransport); diff --git a/packages/realm-web/src/transports/AuthenticatedTransport.ts b/packages/realm-web/src/transports/AuthenticatedTransport.ts index 30baa647eb..ebead2cc85 100644 --- a/packages/realm-web/src/transports/AuthenticatedTransport.ts +++ b/packages/realm-web/src/transports/AuthenticatedTransport.ts @@ -16,7 +16,8 @@ // //////////////////////////////////////////////////////////////////////////// -import { Transport, Request } from "./Transport"; +import { Transport, Request, MongoDBRealmError } from "./Transport"; +import { User } from "../User"; /** * Used to control which user is currently active - this would most likely be the {App} instance. @@ -25,7 +26,7 @@ interface UserContext { /** * The currently active user. */ - currentUser: Realm.User | null; + currentUser: User | null; } /** @@ -59,19 +60,37 @@ export class AuthenticatedTransport implements Transport { * @param request The request to issue towards the server. * @param user The user used when fetching, defaults to the `app.currentUser`. * If `null`, the fetch will be unauthenticated. + * @param retries How many times was this request retried? * @returns A response from requesting with authentication. */ - public fetch( + public async fetch( request: Request, - user: Realm.User | null = this.userContext.currentUser, + user: User | null = this.userContext.currentUser, + retries = 0, ): Promise { - return this.transport.fetch({ - ...request, - headers: { - ...this.buildAuthorizationHeader(user), - ...request.headers, - }, - }); + try { + // Awaiting to intercept errors being thrown + return await this.transport.fetch({ + ...request, + headers: { + ...this.buildAuthorizationHeader(user), + ...request.headers, + }, + }); + } catch (err) { + if ( + user && + retries === 0 && + err instanceof MongoDBRealmError && + err.statusCode === 401 + ) { + // Refresh the access token + await user.refreshAccessToken(); + // Retry + return this.fetch(request, user, retries + 1); + } + throw err; + } } /** @inheritdoc */ @@ -86,7 +105,7 @@ export class AuthenticatedTransport implements Transport { * @param user An optional user to generate the header for * @returns An object containing with the users access token as authorization header or undefined if no user is given. */ - private buildAuthorizationHeader(user: Realm.User | null) { + private buildAuthorizationHeader(user: User | null) { if (user) { // TODO: Ensure the access token is valid return { diff --git a/packages/realm-web/src/transports/BaseTransport.ts b/packages/realm-web/src/transports/BaseTransport.ts index 30bbe25b1a..af31070e28 100644 --- a/packages/realm-web/src/transports/BaseTransport.ts +++ b/packages/realm-web/src/transports/BaseTransport.ts @@ -92,9 +92,10 @@ export class BaseTransport implements Transport { } /** @inheritdoc */ - public async fetch( - request: BaseRequest, - ): Promise { + public async fetch< + RequestBody extends any = any, + ResponseBody extends any = any + >(request: BaseRequest): Promise { const { path, headers, diff --git a/packages/realm-web/src/transports/Transport.ts b/packages/realm-web/src/transports/Transport.ts index 75a7950c80..6e96bb12f4 100644 --- a/packages/realm-web/src/transports/Transport.ts +++ b/packages/realm-web/src/transports/Transport.ts @@ -16,7 +16,9 @@ // //////////////////////////////////////////////////////////////////////////// -import { Method } from "realm-network-transport"; +import { Method, MongoDBRealmError } from "realm-network-transport"; + +export { MongoDBRealmError }; /** * A request to be sent via the transport.