Skip to content

Commit

Permalink
RJS-2727: Re-using anonymous users by default in realm-web (#6592)
Browse files Browse the repository at this point in the history
* Adding ability to reuse users with the same type of credentials

* Upgrading tsx to fix sourcemaps

* Fixing existing test

* Adding a test to verify anonymous users are being reused be default

* Fixing existing test

* Adding a note in the changelog

* Moved changelog entry to "fixed"
  • Loading branch information
kraenhansen authored Apr 10, 2024
1 parent e51a793 commit 11c4de6
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 23 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-istanbul": "^5.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"tsx": "^4.7.0",
"tsx": "^4.7.2",
"typedoc": "^0.25.7",
"typescript": "5.0.4",
"wireit": "^0.14.4"
Expand Down
2 changes: 1 addition & 1 deletion packages/realm-web-integration-tests/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("App#constructor", () => {

it("can log in two users, switch between them and log out", async () => {
const app = createApp();
const credentials = Credentials.anonymous();
const credentials = Credentials.anonymous(false);
// Authenticate the first user
const user1 = await app.logIn(credentials);
expect(app.currentUser).equals(user1);
Expand Down
1 change: 1 addition & 0 deletions packages/realm-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Fixed
* Fixed an endless loop of requests that would happen if linking credentials failed due to an authentication failure. ([#6588](https://github.com/realm/realm-js/pull/6588), since v0.6.0)
* Logging in with `Credentials.anonymous()` credentials will now reuse any existing anonymous user which is already authenticated with the app. This aligns with the behaviour of the `realm` package and will result in less users being created. Use `Credentials.anonymous(false)` to disable this behaviour and achieve the old behaviour of creating new anonymous users on every login. ([#6592](https://github.com/realm/realm-js/pull/6592))

### Internal
* None
Expand Down
12 changes: 12 additions & 0 deletions packages/realm-web/src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ export class App<
credentials: Credentials,
fetchProfile = true,
): Promise<User<FunctionsFactoryType, CustomDataType>> {
if (credentials.reuse) {
// TODO: Consider exposing providerName on "User" and match against that instead?
const existingUser = this.users.find((user) => user.providerType === credentials.providerType);
if (existingUser) {
this.switchUser(existingUser);
// If needed, fetch and set the profile on the user
if (fetchProfile) {
await existingUser.refreshProfile();
}
return existingUser;
}
}
const response = await this.authenticator.authenticate(credentials);
const user = this.createOrUpdateUser(response, credentials.providerType);
// Let's ensure this will be the current user, in case the user object was reused.
Expand Down
42 changes: 26 additions & 16 deletions packages/realm-web/src/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ type SimpleObject = Record<string, unknown>;
export class Credentials<PayloadType extends SimpleObject = SimpleObject> implements Realm.Credentials<PayloadType> {
/**
* Creates credentials that logs in using the [Anonymous Provider](https://docs.mongodb.com/realm/authentication/anonymous/).
* @param reuse - Reuse any existing anonymous user already logged in.
* @returns The credentials instance, which can be passed to `app.logIn`.
*/
static anonymous(): Credentials<AnonymousPayload> {
return new Credentials<AnonymousPayload>("anon-user", "anon-user", {});
static anonymous(reuse = true): Credentials<AnonymousPayload> {
return new Credentials<AnonymousPayload>("anon-user", "anon-user", reuse, {});
}

/**
Expand All @@ -65,7 +66,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
* @returns The credentials instance, which can be passed to `app.logIn`.
*/
static apiKey(key: string): Credentials<ApiKeyPayload> {
return new Credentials<ApiKeyPayload>("api-key", "api-key", { key });
return new Credentials<ApiKeyPayload>("api-key", "api-key", false, { key });
}

/**
Expand All @@ -76,7 +77,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
* @returns The credentials instance, which can be passed to `app.logIn`.
*/
static emailPassword(email: string, password: string): Credentials<EmailPasswordPayload> {
return new Credentials<EmailPasswordPayload>("local-userpass", "local-userpass", {
return new Credentials<EmailPasswordPayload>("local-userpass", "local-userpass", false, {
username: email,
password,
});
Expand All @@ -90,7 +91,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
static function<PayloadType extends FunctionPayload = FunctionPayload>(
payload: PayloadType,
): Credentials<PayloadType> {
return new Credentials<PayloadType>("custom-function", "custom-function", payload);
return new Credentials<PayloadType>("custom-function", "custom-function", false, payload);
}

/**
Expand All @@ -99,7 +100,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
* @returns The credentials instance, which can be passed to `app.logIn`.
*/
static jwt(token: string): Credentials<JWTPayload> {
return new Credentials<JWTPayload>("custom-token", "custom-token", {
return new Credentials<JWTPayload>("custom-token", "custom-token", false, {
token,
});
}
Expand All @@ -110,7 +111,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
* @returns The credentials instance, which can be passed to `app.logIn`.
*/
static google<P extends OAuth2RedirectPayload | GooglePayload>(payload: GoogleOptions): Credentials<P> {
return new Credentials<P>("oauth2-google", "oauth2-google", Credentials.derivePayload(payload) as P);
return new Credentials<P>("oauth2-google", "oauth2-google", false, Credentials.derivePayload(payload) as P);
}

/**
Expand Down Expand Up @@ -144,6 +145,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
return new Credentials<PayloadType>(
"oauth2-facebook",
"oauth2-facebook",
false,
redirectUrlOrAccessToken.includes("://")
? { redirectUrl: redirectUrlOrAccessToken }
: { accessToken: redirectUrlOrAccessToken },
Expand All @@ -161,6 +163,7 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
return new Credentials<PayloadType>(
"oauth2-apple",
"oauth2-apple",
false,
redirectUrlOrIdToken.includes("://") ? { redirectUrl: redirectUrlOrIdToken } : { id_token: redirectUrlOrIdToken },
);
}
Expand All @@ -176,29 +179,36 @@ export class Credentials<PayloadType extends SimpleObject = SimpleObject> implem
*/
public readonly providerType: ProviderType;

/**
* Reuse any user already authenticated with this provider.
*/
public readonly reuse: boolean;

/**
* The data being sent to the service when authenticating.
*/
public readonly payload: PayloadType;

constructor(name: string, type: "anon-user", payload: AnonymousPayload);
constructor(name: string, type: "api-key", payload: ApiKeyPayload);
constructor(name: string, type: "local-userpass", payload: EmailPasswordPayload);
constructor(name: string, type: "custom-function", payload: FunctionPayload);
constructor(name: string, type: "custom-token", payload: JWTPayload);
constructor(name: string, type: "oauth2-google", payload: OAuth2RedirectPayload | GooglePayload);
constructor(name: string, type: "oauth2-facebook", payload: OAuth2RedirectPayload | FacebookPayload);
constructor(name: string, type: "oauth2-apple", payload: OAuth2RedirectPayload | ApplePayload);
constructor(name: string, type: "anon-user", reuse: boolean, payload: AnonymousPayload);
constructor(name: string, type: "api-key", reuse: false, payload: ApiKeyPayload);
constructor(name: string, type: "local-userpass", reuse: false, payload: EmailPasswordPayload);
constructor(name: string, type: "custom-function", reuse: false, payload: FunctionPayload);
constructor(name: string, type: "custom-token", reuse: false, payload: JWTPayload);
constructor(name: string, type: "oauth2-google", reuse: false, payload: OAuth2RedirectPayload | GooglePayload);
constructor(name: string, type: "oauth2-facebook", reuse: false, payload: OAuth2RedirectPayload | FacebookPayload);
constructor(name: string, type: "oauth2-apple", reuse: false, payload: OAuth2RedirectPayload | ApplePayload);

/**
* Constructs an instance of credentials.
* @param providerName The name of the authentication provider used when authenticating.
* @param providerType The type of the authentication provider used when authenticating.
* @param reuse Reuse any user already authenticated with this provider.
* @param payload The data being sent to the service when authenticating.
*/
constructor(providerName: string, providerType: ProviderType, payload: PayloadType) {
constructor(providerName: string, providerType: ProviderType, reuse: boolean, payload: PayloadType) {
this.providerName = providerName;
this.providerType = providerType;
this.reuse = reuse;
this.payload = payload;
}
}
32 changes: 31 additions & 1 deletion packages/realm-web/src/tests/App.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ describe("App", () => {
baseUrl: "http://localhost:1337",
});

const credentials = App.Credentials.anonymous();
const credentials = App.Credentials.anonymous(false);
await app1.logIn(credentials, false); // Alice
await app2.logIn(credentials, false); // Charlie
const bob = await app1.logIn(credentials, true);
Expand Down Expand Up @@ -961,4 +961,34 @@ describe("App", () => {
expect(refreshToken).equals("gilfoyles-forth-refresh-token");
}
});

it("will reuse anonymous users by default and avoid it when asked not to", async () => {
const fetch = createMockFetch([
LOCATION_RESPONSE,
{
user_id: "alices-id",
access_token: "alices-access-token",
refresh_token: "alices-refresh-token",
device_id: "000000000000000000000000",
},
{
user_id: "bobs-id",
access_token: "bobs-access-token",
refresh_token: "bobs-refresh-token",
device_id: "000000000000000000000000",
},
]);
const app = new App({
id: "my-mocked-app",
storage: new MemoryStorage(),
fetch,
baseUrl: "http://localhost:1337",
});
const user1 = await app.logIn(Credentials.anonymous(), false);
const user2 = await app.logIn(Credentials.anonymous(), false);
expect(user2).equals(user1);
const user3 = await app.logIn(Credentials.anonymous(false), false);
expect(user3).not.equals(user1);
expect(user3.id).not.equals(user1.id);
});
});

0 comments on commit 11c4de6

Please sign in to comment.