Expose user serialize and deserialize methods (#1996)

* Expose serialize and deserialize methods

* Changelog and docs

* Forgot to save changelog 🤦‍♂️

* Add input validation
This commit is contained in:
Nikola Irinchev 2018-08-28 15:01:32 +02:00 committed by GitHub
parent a10ffae469
commit 477b900530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 110 deletions

View File

@ -1,3 +1,26 @@
X.Y.Z Release notes
=============================================================
### Compatibility
* Sync protocol: 24
* Server-side history format: 4
* File format: 7
* Realm Object Server: 3.0.0 or later
### Breaking changes
* None.
### Enhancements
* Exposed `User.serialize` to create a persistable representation of a user instance, as well as
`User.deserialize` to later inflate a `User` instance that can be used to connect to Realm Object
Server and open synchronized Realms (#1276).
### Bug fixes
* Removed a false negative warning when using `User.createConfiguration`.
### Internal
* Realm Core v5.7.2.
* Realm Sync v3.9.1.
2.15.3 Release notes (2018-8-24)
=============================================================
### Compatibility
@ -71,7 +94,7 @@
* Updated external packages with help from `npm audit`.
* Upgraded to Realm Sync v3.9.1 (to match the devtoolset-6 upgrade).
* Upgraded to devtoolset-6 on Centos for Linux builds.
2.14.2 Release notes (2018-8-8)
=============================================================
@ -380,7 +403,7 @@
The feature known as Partial synchronization has been renamed to Query-based synchronization and is now the default mode for synchronized Realms. This has impacted a number of APIs. See below for the details.
### Deprecated
* [Sync] `Realm.Configuration.SyncConfiguration.partial` has been deprecated in favor of `Realm.Configuration.SyncConfiguration.fullSynchronization`.
* [Sync] `Realm.Configuration.SyncConfiguration.partial` has been deprecated in favor of `Realm.Configuration.SyncConfiguration.fullSynchronization`.
* [Sync] `Realm.automaticSyncConfiguration()` has been deprecated in favor of `Realm.Sync.User.createConfiguration()`.
### Breaking changes

View File

@ -344,22 +344,22 @@ class User {
/**
* Request a password reset email to be sent to a user's email.
* This will not throw an exception, even if the email doesn't belong to a Realm Object Server user.
*
*
* This can only be used for users who authenticated with the 'password' provider, and passed a valid email address as a username.
*
*
* @param {string} server - authentication server
* @param {string} email - The email that corresponds to the user's username.
* @return {Promise<void>} A promise which is resolved when the request has been sent.
*/
static requestPasswordReset(server, email) {}
/**
* Complete the password reset flow by using the reset token sent to the user's email as a one-time authorization token to change the password.
*
*
* By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password.
* If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can
* open the app, extract the token, and navigate to a view that allows to change the password within the app.
*
*
* @param {string} server - authentication server
* @param {string} reset_token - The token that was sent to the user's email address.
* @param {string} new_password - The user's new password.
@ -370,20 +370,20 @@ class User {
/**
* Request an email confirmation email to be sent to a user's email.
* This will not throw an exception, even if the email doesn't belong to a Realm Object Server user.
*
*
* @param {string} server - authentication server
* @param {string} email - The email that corresponds to the user's username.
* @return {Promise<void>} A promise which is resolved when the request has been sent.
*/
static requestEmailConfirmation(server, email) {}
/**
* Complete the email confirmation flow by using the confirmation token sent to the user's email as a one-time authorization token to confirm their email.
*
*
* By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password.
* If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can
* open the app, extract the token, and navigate to a view that allows to confirm the email within the app.
*
*
* @param {string} server - authentication server
* @param {string} confirmation_token - The token that was sent to the user's email address.
* @return {Promise<void>} A promise which is resolved when the request has been sent.
@ -398,6 +398,12 @@ class User {
*/
static adminUser(adminToken, server) {}
/**
* Creates a new sync user instance from the serialized representation.
* @param {object} serialized - the serialized version of the user, obtained by calling {@link User#serialize}.
*/
static deserialize(serialized) {}
/**
* A dictionary containing users that are currently logged in.
* The keys in the dictionary are user identities, values are corresponding User objects.
@ -452,6 +458,14 @@ class User {
*/
createConfiguration(config) {}
/**
* Serializes a user to an object, that can be persisted or passed to another component to create a new instance
* by calling {@link User.deserialize}. The serialized user instance includes the user's refresh token and should
* be treated as sensitive data.
* @returns {object} an object, containing the user identity, server url, and refresh token.
*/
serialize() {}
/**
* Logs out the user from the Realm Object Server.
*/

12
lib/index.d.ts vendored
View File

@ -283,6 +283,13 @@ declare namespace Realm.Sync {
user: UserInfo
}
interface SerializedUser {
server: string;
refreshToken: string;
identity: string;
isAdmin: boolean;
}
/**
* User
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.User.html }
@ -318,13 +325,16 @@ declare namespace Realm.Sync {
static requestPasswordReset(server: string, email: string): Promise<void>;
static completePasswordReset(server:string, reset_token:string, new_password:string): Promise<void>;
static requestEmailConfirmation(server:string, email:string): Promise<void>;
static confirmEmail(server:string, confirmation_token:string): Promise<void>;
static deserialize(serialized: SerializedUser): Realm.Sync.User;
createConfiguration(config?: Realm.PartialConfiguration): Realm.Configuration
authenticate(server: string, provider: string, options: any): Promise<Realm.Sync.User>;
serialize(): SerializedUser;
logout(): void;
openManagementRealm(): Realm;
retrieveAccount(provider: string, username: string): Promise<Account>;

View File

@ -38,6 +38,25 @@ function checkTypes(args, types) {
}
}
function checkObjectTypes(obj, types) {
for (const name of Object.getOwnPropertyNames(types)) {
const actualType = typeof obj[name];
let targetType = types[name];
const isOptional = targetType[targetType.length - 1] === '?';
if (isOptional) {
targetType = targetType.slice(0, -1);
}
if (!isOptional && actualType === 'undefined') {
throw new Error(`${name} is required, but a value was not provided.`);
}
if (actualType !== targetType) {
throw new TypeError(`${name} must be of type '${targetType}' but was of type '${actualType}' instead.`);
}
}
}
// Perform a HTTP request, enqueuing it if too many requests are already in
// progress to avoid hammering the server.
const performFetch = (function() {
@ -296,7 +315,7 @@ function _updateAccount(userConstructor, server, json) {
body,
});
});
}
}
if (!response.ok) {
return response.json().then((body) => Promise.reject(new AuthError(body)));
}
@ -407,6 +426,17 @@ const staticMethods = {
return _authenticate(this, server, json)
},
deserialize(serialized) {
checkObjectTypes(serialized, {
server: 'string',
identity: 'string',
refreshToken: 'string',
isAdmin: 'boolean',
});
return this.createUser(serialized.server, serialized.identity, serialized.refreshToken, false, serialized.isAdmin || false);
},
requestPasswordReset(server, email) {
checkTypes(arguments, ['string', 'string']);
const json = {
@ -419,8 +449,8 @@ const staticMethods = {
completePasswordReset(server, reset_token, new_password) {
checkTypes(arguments, ['string', 'string']);
const json = {
data: {
const json = {
data: {
action: 'complete_reset',
token: reset_token,
new_password: new_password
@ -442,8 +472,8 @@ const staticMethods = {
confirmEmail(server, confirmation_token) {
checkTypes(arguments, ['string', 'string']);
const json = {
data: {
const json = {
data: {
action: 'confirm_email',
token: confirmation_token
}
@ -478,98 +508,106 @@ const instanceMethods = {
.then(() => console.log('User is logged out'))
.catch((e) => print_error(e));
},
openManagementRealm() {
let url = url_parse(this.server);
if (url.protocol === 'http:') {
url.set('protocol', 'realm:');
} else if (url.protocol === 'https:') {
url.set('protocol', 'realms:');
} else {
throw new Error(`Unexpected user auth url: ${this.server}`);
serialize() {
return {
server: this.server,
refreshToken: this.token,
identity: this.identity,
isAdmin: this.isAdmin,
};
},
openManagementRealm() {
let url = url_parse(this.server);
if (url.protocol === 'http:') {
url.set('protocol', 'realm:');
} else if (url.protocol === 'https:') {
url.set('protocol', 'realms:');
} else {
throw new Error(`Unexpected user auth url: ${this.server}`);
}
url.set('pathname', '/~/__management');
return new this.constructor._realmConstructor({
schema: require('./management-schema'),
sync: {
user: this,
url: url.href
}
url.set('pathname', '/~/__management');
return new this.constructor._realmConstructor({
schema: require('./management-schema'),
sync: {
user: this,
url: url.href
});
},
retrieveAccount(provider, provider_id) {
checkTypes(arguments, ['string', 'string']);
const url = url_parse(this.server);
url.set('pathname', `/auth/users/${provider}/${provider_id}`);
const headers = {
Authorization: this.token
};
const options = {
method: 'GET',
headers,
open_timeout: 5000
};
return performFetch(url.href, options)
.then((response) => {
if (response.status !== 200) {
return response.json()
.then(body => {
throw new AuthError(body);
});
} else {
return response.json();
}
});
},
retrieveAccount(provider, provider_id) {
checkTypes(arguments, ['string', 'string']);
const url = url_parse(this.server);
url.set('pathname', `/auth/users/${provider}/${provider_id}`);
const headers = {
Authorization: this.token
};
const options = {
method: 'GET',
headers,
open_timeout: 5000
};
return performFetch(url.href, options)
.then((response) => {
if (response.status !== 200) {
return response.json()
.then(body => {
throw new AuthError(body);
});
} else {
return response.json();
}
});
},
createConfiguration(config) {
},
createConfiguration(config) {
if (config && config.sync) {
if (console.warn !== undefined) {
console.warn(`'user' property will be overridden by ${this.identity}`);
}
if (config.sync.partial !== undefined && config.sync.fullSynchronization !== undefined) {
throw new Error("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
}
if (config && config.sync) {
if (config.sync.user && console.warn !== undefined) {
console.warn(`'user' property will be overridden by ${this.identity}`);
}
// Create default config
let url = new URL(this.server);
let secure = (url.protocol === 'https:')?'s':'';
let port = (url.port === undefined)?'9080':url.port;
let realmUrl = `realm${secure}://${url.hostname}:${port}/default`;
let defaultConfig = {
sync: {
user: this,
url: realmUrl,
},
schema: [],
};
// Set query-based as the default setting if the user doesn't specified any other behaviour.
if (!(config && config.sync && config.sync.partial)) {
defaultConfig.sync.fullSynchronization = false;
if (config.sync.partial !== undefined && config.sync.fullSynchronization !== undefined) {
throw new Error("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
}
}
// Automatically add Permission classes to the schema if Query-based sync is enabled
if (defaultConfig.sync.fullSynchronization === false || (config && config.sync && config.sync.partial === true)) {
defaultConfig.schema = [
Realm.Permissions.Class,
Realm.Permissions.Permission,
Realm.Permissions.Role,
Realm.Permissions.User,
];
}
// Create default config
let url = new URL(this.server);
let secure = (url.protocol === 'https:')?'s':'';
let port = (url.port === undefined)?'9080':url.port;
let realmUrl = `realm${secure}://${url.hostname}:${port}/default`;
// Merge default configuration with user provided config. User defined properties should aways win.
// Doing the naive merge in JS break objects that are backed by native objects, so these needs to
// be merged manually. This is currently only `sync.user`.
let mergedConfig = (config === undefined) ? defaultConfig : merge(defaultConfig, config);
mergedConfig.sync.user = this;
return mergedConfig;
},
};
let defaultConfig = {
sync: {
user: this,
url: realmUrl,
},
schema: [],
};
// Set query-based as the default setting if the user doesn't specified any other behaviour.
if (!(config && config.sync && config.sync.partial)) {
defaultConfig.sync.fullSynchronization = false;
}
// Automatically add Permission classes to the schema if Query-based sync is enabled
if (defaultConfig.sync.fullSynchronization === false || (config && config.sync && config.sync.partial === true)) {
defaultConfig.schema = [
Realm.Permissions.Class,
Realm.Permissions.Permission,
Realm.Permissions.Role,
Realm.Permissions.User,
];
}
// Merge default configuration with user provided config. User defined properties should aways win.
// Doing the naive merge in JS break objects that are backed by native objects, so these needs to
// be merged manually. This is currently only `sync.user`.
let mergedConfig = (config === undefined) ? defaultConfig : merge(defaultConfig, config);
mergedConfig.sync.user = this;
return mergedConfig;
},
};
// Append the permission apis
Object.assign(instanceMethods, permissionApis);

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "realm",
"version": "2.15.2",
"version": "2.15.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -343,18 +343,18 @@ module.exports = {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
TestCase.assertThrowsContaining(() => {
let config = {
sync: {
let config = {
sync: {
url: 'http://localhost:9080/~/default',
partial: true,
fullSynchronization: false
}
partial: true,
fullSynchronization: false
}
};
user.createConfiguration(config);
}, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
});
},
testOpen_partialAndFullSynchronizationSetThrows() {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
@ -369,6 +369,87 @@ module.exports = {
})
}, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
});
},
testSerialize() {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
const serialized = user.serialize();
TestCase.assertFalse(serialized.isAdmin);
TestCase.assertEqual(serialized.identity, user.identity);
TestCase.assertEqual(serialized.server, 'http://localhost:9080');
TestCase.assertEqual(serialized.refreshToken, user.token);
});
},
testDeserialize() {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password')
.then((user) => {
const userConfig = user.createConfiguration({
schema: [{ name: 'Dog', properties: { name: 'string' } }],
sync: {
url: 'realm://localhost:9080/~/foo',
fullSynchronization: true,
}
});
const realm = new Realm(userConfig);
realm.write(() => {
realm.create('Dog', {
name: 'Doggo'
});
});
const session = realm.syncSession;
return new Promise((resolve, reject) => {
let callback = (transferred, total) => {
if (transferred >= total) {
session.removeProgressNotification(callback);
realm.close();
Realm.deleteFile(userConfig);
resolve(user.serialize());
}
}
session.addProgressNotification('upload', 'forCurrentlyOutstandingWork', callback);
});
}).then((serialized) => {
const deserialized = Realm.Sync.User.deserialize(serialized);
const config = deserialized.createConfiguration({
schema: [{ name: 'Dog', properties: { name: 'string' } }],
sync: {
url: 'realm://localhost:9080/~/foo',
fullSynchronization: true,
}
});
return Realm.open(config);
}).then((realm) => {
const dogs = realm.objects('Dog');
TestCase.assertEqual(dogs.length, 1);
TestCase.assertEqual(dogs[0].name, 'Doggo');
});
},
testDeserializeInvalidInput() {
const dummy = {
server: '123',
identity: '123',
refreshToken: '123',
isAdmin: false,
};
for (const name of Object.getOwnPropertyNames(dummy)) {
const clone = Object.assign({}, dummy);
// Set to invalid type
clone[name] = 123;
TestCase.assertThrowsContaining(() => Realm.Sync.User.deserialize(clone), `${name} must be of type '${typeof dummy[name]}'`);
// Set to undefined
clone[name] = undefined;
TestCase.assertThrowsContaining(() => Realm.Sync.User.deserialize(clone), `${name} is required, but a value was not provided.`);
}
}
/* This test fails because of realm-object-store #243 . We should use 2 users.

View File

@ -15,8 +15,8 @@
"typescript": "^2.5.2"
},
"scripts": {
"check-typescript" : "tsc --types --noEmit --alwaysStrict ./../lib/index.d.ts",
"js-tests" : "jasmine spec/unit_tests.js",
"check-typescript": "tsc --types --noEmit --alwaysStrict ./../lib/index.d.ts",
"js-tests": "jasmine spec/unit_tests.js",
"test": "npm run check-typescript && npm run js-tests"
}
}