Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Anonymous Users #750

Merged
merged 9 commits into from Mar 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 70 additions & 0 deletions integration/test/ParseUserTest.js
Expand Up @@ -439,4 +439,74 @@ describe('Parse User', () => {
done();
});
});

it('can save anonymous user', async () => {
Parse.User.enableUnsafeCurrentUser();

const user = await Parse.AnonymousUtils.logIn();
user.set('field', 'hello');
await user.save();

const query = new Parse.Query(Parse.User);
const result = await query.get(user.id);
expect(result.get('field')).toBe('hello');
});

it('can not recover anonymous user if logged out', async () => {
Parse.User.enableUnsafeCurrentUser();

const user = await Parse.AnonymousUtils.logIn();
user.set('field', 'hello');
await user.save();

await Parse.User.logOut();

const query = new Parse.Query(Parse.User);
try {
await query.get(user.id);
} catch (error) {
expect(error.message).toBe('Object not found.');
}
});

it('can signUp anonymous user and retain data', async () => {
Parse.User.enableUnsafeCurrentUser();

const user = await Parse.AnonymousUtils.logIn();
user.set('field', 'hello world');
await user.save();

expect(user.get('authData').anonymous).toBeDefined();

user.setUsername('foo');
user.setPassword('baz');

await user.signUp();

const query = new Parse.Query(Parse.User);
const result = await query.get(user.id);
expect(result.get('username')).toBe('foo');
expect(result.get('authData')).toBeUndefined();
expect(result.get('field')).toBe('hello world');
expect(user.get('authData').anonymous).toBeUndefined();
});

it('can logIn user without converting anonymous user', async () => {
Parse.User.enableUnsafeCurrentUser();

await Parse.User.signUp('foobaz', '1234');

const user = await Parse.AnonymousUtils.logIn();
user.set('field', 'hello world');
await user.save();

await Parse.User.logIn('foobaz', '1234');

const query = new Parse.Query(Parse.User);
try {
await query.get(user.id);
} catch (error) {
expect(error.message).toBe('Object not found.');
}
});
});
113 changes: 113 additions & 0 deletions src/AnonymousUtils.js
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow-weak
*/
import ParseUser from './ParseUser';
const uuidv4 = require('uuid/v4');

let registered = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice if this were exposed on the parse user.....


/**
* Provides utility functions for working with Anonymously logged-in users. <br />
* Anonymous users have some unique characteristics:
* <ul>
* <li>Anonymous users don't need a user name or password.</li>
* <ul>
* <li>Once logged out, an anonymous user cannot be recovered.</li>
* </ul>
* <li>signUp converts an anonymous user to a standard user with the given username and password.</li>
* <ul>
* <li>Data associated with the anonymous user is retained.</li>
* </ul>
* <li>logIn switches users without converting the anonymous user.</li>
* <ul>
* <li>Data associated with the anonymous user will be lost.</li>
* </ul>
* <li>Service logIn (e.g. Facebook, Twitter) will attempt to convert
* the anonymous user into a standard user by linking it to the service.</li>
* <ul>
* <li>If a user already exists that is linked to the service, it will instead switch to the existing user.</li>
* </ul>
* <li>Service linking (e.g. Facebook, Twitter) will convert the anonymous user
* into a standard user by linking it to the service.</li>
* </ul>
* @class Parse.AnonymousUtils
* @static
*/
const AnonymousUtils = {
/**
* Gets whether the user has their account linked to anonymous user.
*
* @method isLinked
* @name Parse.AnonymousUtils.isLinked
* @param {Parse.User} user User to check for.
* The user must be logged in on this device.
* @return {Boolean} <code>true</code> if the user has their account
* linked to an anonymous user.
* @static
*/
isLinked(user: ParseUser) {
const provider = this._getAuthProvider();
return user._isLinked(provider.getAuthType());
},

/**
* Logs in a user Anonymously.
*
* @method logIn
* @name Parse.AnonymousUtils.logIn
* @returns {Promise}
* @static
*/
logIn() {
const provider = this._getAuthProvider();
return ParseUser._logInWith(provider.getAuthType(), provider.getAuthData());
},

/**
* Links Anonymous User to an existing PFUser.
*
* @method link
* @name Parse.AnonymousUtils.link
* @param {Parse.User} user User to link. This must be the current user.
* @returns {Promise}
* @static
*/
link(user: ParseUser) {
const provider = this._getAuthProvider();
return user._linkWith(provider.getAuthType(), provider.getAuthData());
},

_getAuthProvider() {
const provider = {
restoreAuthentication() {
return true;
},

getAuthType() {
return 'anonymous';
},

getAuthData() {
return {
authData: {
id: uuidv4(),
},
};
},
};
if (!registered) {
ParseUser._registerAuthenticationProvider(provider);
registered = true;
}
return provider;
}
};

export default AnonymousUtils;
1 change: 1 addition & 0 deletions src/Parse.js
Expand Up @@ -173,6 +173,7 @@ Object.defineProperty(Parse, 'liveQueryServerURL', {

Parse.ACL = require('./ParseACL').default;
Parse.Analytics = require('./Analytics');
Parse.AnonymousUtils = require('./AnonymousUtils').default;
Parse.Cloud = require('./Cloud');
Parse.CoreManager = require('./CoreManager');
Parse.Config = require('./ParseConfig').default;
Expand Down
5 changes: 4 additions & 1 deletion src/ParseObject.js
Expand Up @@ -1157,7 +1157,10 @@ class ParseObject {
if (options.hasOwnProperty('sessionToken') && typeof options.sessionToken === 'string') {
saveOptions.sessionToken = options.sessionToken;
}

// Pass sessionToken if saving currentUser
if (typeof this.getSessionToken === 'function' && this.getSessionToken()) {
saveOptions.sessionToken = this.getSessionToken();
}
const controller = CoreManager.getObjectController();
const unsaved = unsavedChildren(this);
return controller.save(unsaved, saveOptions).then(() => {
Expand Down
14 changes: 12 additions & 2 deletions src/ParseUser.js
Expand Up @@ -9,6 +9,7 @@
* @flow
*/

import AnonymousUtils from './AnonymousUtils';
import CoreManager from './CoreManager';
import isRevocableSession from './isRevocableSession';
import ParseError from './ParseError';
Expand Down Expand Up @@ -119,7 +120,6 @@ class ParseUser extends ParseObject {
/**
* Synchronizes auth data for a provider (e.g. puts the access token in the
* right place to be used by the Facebook SDK).

*/
_synchronizeAuthData(provider: string) {
if (!this.isCurrent() || !provider) {
Expand Down Expand Up @@ -814,10 +814,15 @@ const DefaultController = {
},

setCurrentUser(user) {
const currentUser = this.currentUser();
let promise = Promise.resolve();
if (currentUser && !user.equals(currentUser) && AnonymousUtils.isLinked(currentUser)) {
promise = currentUser.destroy({ sessionToken: currentUser.getSessionToken() })
}
currentUserCache = user;
user._cleanupAuthData();
user._synchronizeAllAuthData();
return DefaultController.updateUserOnDisk(user);
return promise.then(() => DefaultController.updateUserOnDisk(user));
},

currentUser(): ?ParseUser {
Expand Down Expand Up @@ -986,9 +991,14 @@ const DefaultController = {
let promise = Storage.removeItemAsync(path);
const RESTController = CoreManager.getRESTController();
if (currentUser !== null) {
const isAnonymous = AnonymousUtils.isLinked(currentUser);
const currentSession = currentUser.getSessionToken();
if (currentSession && isRevocableSession(currentSession)) {
promise = promise.then(() => {
if (isAnonymous) {
return currentUser.destroy({ sessionToken: currentSession });
}
}).then(() => {
return RESTController.request(
'POST', 'logout', {}, { sessionToken: currentSession }
);
Expand Down
88 changes: 88 additions & 0 deletions src/__tests__/AnonymousUtils-test.js
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

jest.dontMock('../AnonymousUtils');

class MockUser {
constructor () {
this.className = '_User';
this.attributes = {};
}
_isLinked() {}
_linkWith() {}
static _registerAuthenticationProvider() {}
static _logInWith() {}
}

jest.setMock('../ParseUser', MockUser);

const mockProvider = {
restoreAuthentication() {
return true;
},

getAuthType() {
return 'anonymous';
},

getAuthData() {
return {
authData: {
id: '1234',
},
};
},
};

const AnonymousUtils = require('../AnonymousUtils').default;

describe('AnonymousUtils', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(
AnonymousUtils,
'_getAuthProvider'
)
.mockImplementation(() => mockProvider);
});

it('can register provider', () => {
AnonymousUtils._getAuthProvider.mockRestore();
jest.spyOn(MockUser, '_registerAuthenticationProvider');
AnonymousUtils._getAuthProvider();
AnonymousUtils._getAuthProvider();
expect(MockUser._registerAuthenticationProvider).toHaveBeenCalledTimes(1);
});

it('can check user isLinked', () => {
const user = new MockUser();
jest.spyOn(user, '_isLinked');
AnonymousUtils.isLinked(user);
expect(user._isLinked).toHaveBeenCalledTimes(1);
expect(user._isLinked).toHaveBeenCalledWith('anonymous');
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
});

it('can link user', () => {
const user = new MockUser();
jest.spyOn(user, '_linkWith');
AnonymousUtils.link(user);
expect(user._linkWith).toHaveBeenCalledTimes(1);
expect(user._linkWith).toHaveBeenCalledWith('anonymous', mockProvider.getAuthData());
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
});

it('can login user', () => {
jest.spyOn(MockUser, '_logInWith');
AnonymousUtils.logIn();
expect(MockUser._logInWith).toHaveBeenCalledTimes(1);
expect(MockUser._logInWith).toHaveBeenCalledWith('anonymous', mockProvider.getAuthData());
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
});
});