diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1033e2d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# 2 space indentation +[**.*] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f674032 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/aurelia-tools/.eslintrc" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8aa7bc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +jspm_packages +bower_components +.idea +.DS_STORE +build/reports +*.bat \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..a5aaaed --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esnext": true +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..cafe9b6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +jspm_packages +bower_components +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b9a7eae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Alain Mereaux + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff65c74 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# aurelia-firebase + +[![Join the chat at https://gitter.im/PulsarBlow/aurelia-firebase](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/PulsarBlow/aurelia-firebase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Circle CI](https://circleci.com/gh/PulsarBlow/aurelia-firebase/tree/master.svg?style=svg)](https://circleci.com/gh/PulsarBlow/aurelia-firebase/tree/master) + +A Firebase plugin for [Aurelia](http://aurelia.io/) that supports Promises. +Developed from scratch following aurelia's spirit. + +This is an early version which comes with : + +- A complete firebase password authentication support +- A reactive collection providing auto-sync with Firebase server. + +This version is a work in progress, and lacks some Firebase features : + +- Full Query support (order, startAt, endAt etc..) +- Priorities +- Transactions + +Play with the demo : https://aureliaonfire.azurewebsites.net + +# Installation + + +#### Install via JSPM +Go into your project and verify it's already `npm install`'ed and `jspm install`'ed. Now execute following command to install the plugin via JSPM: + +``` +jspm install aurelia-firebase +``` + +this will add the plugin into your `jspm_packages` folder as well as an mapping-line into your `config.js` as: + +``` +"aurelia-firebase": "github:aurelia-firebase@X.X.X", +``` + +If you're feeling experimental or cannot wait for the next release, you could also install the latest version by executing: +``` +jspm install aurelia-firebase=github:pulsarblow/aurelia-firebase@master +``` + + +#### Migrate from aurelia-app to aurelia-app="main" +You'll need to register the plugin when your aurelia app is bootstrapping. If you have an aurelia app because you cloned a sample, there's a good chance that the app is bootstrapping based on default conventions. In that case, open your **index.html** file and look at the *body* tag. +``` html + +``` +Change the *aurelia-app* attribute to *aurelia-app="main"*. +``` html + +``` +The aurelia framework will now bootstrap the application by looking for your **main.js** file and executing the exported *configure* method. Go ahead and add a new **main.js** file with these contents: +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging(); + + aurelia.start().then(a => a.setRoot('app', document.body)); +} + +``` + +#### Load the plugin +During bootstrapping phase, you can now include the validation plugin: + +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging() + .plugin('aurelia-firebase'); //Add this line to load the plugin + + aurelia.start().then(a => a.setRoot('app', document.body)); +} +``` + +# Getting started + +TBD... + +#Configuration +##One config to rule them all +The firebase plugin has one global configuration instance, which is passed to an optional callback function when you first install the plugin: +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging() + .plugin('aurelia-firebase', (config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/'); }); + + aurelia.start().then(a => a.setRoot('app', document.body)); +} +``` + +>Note: if you want to access the global configuration instance at a later point in time, you can inject it: +``` javascript +import {Configuration} from 'aurelia-firebase'; +import {inject} from 'aurelia-framework'; + +>@inject(Configuration) +export class MyVM{ + constructor(config) + { + +> } +} +``` + +##Possible configuration +>Note: all these can be chained: +``` javascript +(config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/').setMonitorAuthChange(true); } +``` + +###config.setFirebaseUrl(firebaseUrl: string) +``` javascript +(config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/'); } +``` +Sets the Firebase URL where your app answers. +This is required and the plugin will not start if not provided. + +###config.setMonitorAuthChange(monitorAuthChange: boolean) +``` javascript +(config) => { config.setMonitorAuthChange(true); } +``` +When set to true, the authentication manager will monitor authentication changes for the current user +The default value is false. + +#AuthenticationManager + +The authentication manager handles authentication aspects in the plugin. + +#ReactiveCollection + +The ReactiveCollection class handles firebase data synchronization. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..f9caaad --- /dev/null +++ b/bower.json @@ -0,0 +1,19 @@ +{ + "name": "aurelia-firebase", + "version": "0.1.0-beta", + "description": "A Firebase plugin for Aurelia.", + "keywords": [ + "aurelia", + "firebase", + "plugin" + ], + "homepage": "https://github.com/PulsarBlow/aurelia-firebase", + "license": "MIT", + "authors": [ + "Alain Méreaux " + ], + "repository": { + "type": "git", + "url": "https://github.com/PulsarBlow/aurelia-firebase" + } +} diff --git a/build/args.js b/build/args.js new file mode 100644 index 0000000..db342fc --- /dev/null +++ b/build/args.js @@ -0,0 +1,13 @@ +var yargs = require('yargs'); + +var argv = yargs.argv, + validBumpTypes = "major|minor|patch|prerelease".split("|"), + bump = (argv.bump || 'patch').toLowerCase(); + +if(validBumpTypes.indexOf(bump) === -1) { + throw new Error('Unrecognized bump "' + bump + '".'); +} + +module.exports = { + bump: bump +}; diff --git a/build/babel-options.js b/build/babel-options.js new file mode 100644 index 0000000..9504abb --- /dev/null +++ b/build/babel-options.js @@ -0,0 +1,11 @@ +module.exports = { + modules: 'system', + moduleIds: false, + comments: false, + compact: false, + stage:2, + optional: [ + "es7.decorators", + "es7.classProperties" + ] +}; diff --git a/build/paths.js b/build/paths.js new file mode 100644 index 0000000..0ca8b7a --- /dev/null +++ b/build/paths.js @@ -0,0 +1,16 @@ +var path = require('path'); + +var appRoot = 'src/'; +var outputRoot = 'dist/'; + +module.exports = { + root: appRoot, + source: appRoot + '**/*.js', + html: appRoot + '**/*.html', + css: appRoot + '**/*.css', + style: 'styles/**/*.css', + output: outputRoot, + doc:'./doc', + e2eSpecsSrc: 'test/e2e/src/*.js', + e2eSpecsDist: 'test/e2e/dist/' +}; diff --git a/build/tasks/build.js b/build/tasks/build.js new file mode 100644 index 0000000..30b6ad9 --- /dev/null +++ b/build/tasks/build.js @@ -0,0 +1,57 @@ +var gulp = require('gulp'); +var runSequence = require('run-sequence'); +var to5 = require('gulp-babel'); +var paths = require('../paths'); +var compilerOptions = require('../babel-options'); +var assign = Object.assign || require('object.assign'); + +gulp.task('build-html-es6', function () { + return gulp.src(paths.html) + .pipe(gulp.dest(paths.output + 'es6')); +}); + +gulp.task('build-es6', ['build-html-es6'], function () { + return gulp.src(paths.source) + .pipe(gulp.dest(paths.output + 'es6')); +}); + +gulp.task('build-html-commonjs', function () { + return gulp.src(paths.html) + .pipe(gulp.dest(paths.output + 'commonjs')); +}); + +gulp.task('build-commonjs', ['build-html-commonjs'], function () { + return gulp.src(paths.source) + .pipe(to5(assign({}, compilerOptions, {modules:'common'}))) + .pipe(gulp.dest(paths.output + 'commonjs')); +}); + +gulp.task('build-html-amd', function () { + return gulp.src(paths.html) + .pipe(gulp.dest(paths.output + 'amd')); +}); + +gulp.task('build-amd', ['build-html-amd'], function () { + return gulp.src(paths.source) + .pipe(to5(assign({}, compilerOptions, {modules:'amd'}))) + .pipe(gulp.dest(paths.output + 'amd')); +}); + +gulp.task('build-html-system', function () { + return gulp.src(paths.html) + .pipe(gulp.dest(paths.output + 'system')); +}); + +gulp.task('build-system', ['build-html-system'], function () { + return gulp.src(paths.source) + .pipe(to5(assign({}, compilerOptions, {modules:'system'}))) + .pipe(gulp.dest(paths.output + 'system')); +}); + +gulp.task('build', function(callback) { + return runSequence( + 'clean', + ['build-es6', 'build-commonjs', 'build-amd', 'build-system'], + callback + ); +}); diff --git a/build/tasks/clean.js b/build/tasks/clean.js new file mode 100644 index 0000000..897eed3 --- /dev/null +++ b/build/tasks/clean.js @@ -0,0 +1,10 @@ +var gulp = require('gulp'); +var paths = require('../paths'); +var del = require('del'); +var vinylPaths = require('vinyl-paths'); + +// deletes all files in the output path +gulp.task('clean', function() { + return gulp.src([paths.output]) + .pipe(vinylPaths(del)); +}); diff --git a/build/tasks/dev.js b/build/tasks/dev.js new file mode 100644 index 0000000..860b46f --- /dev/null +++ b/build/tasks/dev.js @@ -0,0 +1,20 @@ +var gulp = require('gulp'); +var tools = require('aurelia-tools'); + +// source code for the tasks called in this file +// is located at: https://github.com/aurelia/tools/blob/master/src/dev.js + +// updates dependencies in this folder +// from folders in the parent directory +gulp.task('update-own-deps', function() { + tools.updateOwnDependenciesFromLocalRepositories(); +}); + +// quickly pulls in all of the aurelia +// github repos, placing them up one directory +// from where the command is executed, +// then runs `npm install` +// and `gulp build` for each repo +gulp.task('build-dev-env', function () { + tools.buildDevEnv(); +}); diff --git a/build/tasks/doc.js b/build/tasks/doc.js new file mode 100644 index 0000000..2a5c936 --- /dev/null +++ b/build/tasks/doc.js @@ -0,0 +1,33 @@ +var gulp = require('gulp'); +var paths = require('../paths'); +var typedoc = require('gulp-typedoc'); +var typedocExtractor = require('gulp-typedoc-extractor'); +var runSequence = require('run-sequence'); + +gulp.task('doc-generate', function(){ + return gulp.src([paths.output + 'amd/*.d.ts', paths.doc + '/core-js.d.ts', './jspm_packages/github/aurelia/*/*.d.ts']) + .pipe(typedoc({ + target: 'es6', + includeDeclarations: true, + json: paths.doc + '/api.json', + name: paths.packageName + '-docs', + mode: 'modules', + excludeExternals: true, + ignoreCompilerErrors: false, + version: true + })); +}); + +gulp.task('doc-extract', function(){ + return gulp.src([paths.doc + '/api.json']) + .pipe(typedocExtractor(paths.output + 'amd/' + paths.packageName)) + .pipe(gulp.dest(paths.doc)); +}); + +gulp.task('doc', function(callback){ + return runSequence( + 'doc-generate', + 'doc-extract', + callback + ); +}); diff --git a/build/tasks/lint.js b/build/tasks/lint.js new file mode 100644 index 0000000..e758acc --- /dev/null +++ b/build/tasks/lint.js @@ -0,0 +1,11 @@ +var gulp = require('gulp'); +var paths = require('../paths'); +var eslint = require('gulp-eslint'); + +// runs eslint on all .js files +gulp.task('lint', function() { + return gulp.src(paths.source) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failOnError()); +}); diff --git a/build/tasks/prepare-release.js b/build/tasks/prepare-release.js new file mode 100644 index 0000000..fb7a249 --- /dev/null +++ b/build/tasks/prepare-release.js @@ -0,0 +1,41 @@ +var gulp = require('gulp'); +var runSequence = require('run-sequence'); +var paths = require('../paths'); +var changelog = require('conventional-changelog'); +var fs = require('fs'); +var bump = require('gulp-bump'); +var args = require('../args'); + +// utilizes the bump plugin to bump the +// semver for the repo +gulp.task('bump-version', function() { + return gulp.src(['./package.json']) + .pipe(bump({type:args.bump})) //major|minor|patch|prerelease + .pipe(gulp.dest('./')); +}); + +// generates the CHANGELOG.md file based on commit +// from git commit messages +gulp.task('changelog', function(callback) { + var pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8')); + + return changelog({ + repository: pkg.repository.url, + version: pkg.version, + file: paths.doc + '/CHANGELOG.md' + }, function(err, log) { + fs.writeFileSync(paths.doc + '/CHANGELOG.md', log); + }); +}); + +// calls the listed sequence of tasks in order +gulp.task('prepare-release', function(callback){ + return runSequence( + 'build', + 'lint', + 'bump-version', + 'doc', + 'changelog', + callback + ); +}); diff --git a/build/tasks/test.js b/build/tasks/test.js new file mode 100644 index 0000000..fe8631b --- /dev/null +++ b/build/tasks/test.js @@ -0,0 +1,46 @@ +var gulp = require('gulp'); +var karma = require('karma').server; + +/** + * Run test once and exit + */ +gulp.task('test', function (done) { + karma.start({ + configFile: __dirname + '/../../karma.conf.js', + singleRun: true + }, function(e) { + done(); + }); +}); + +/** + * Watch for file changes and re-run tests on each change + */ +gulp.task('tdd', function (done) { + karma.start({ + configFile: __dirname + '/../../karma.conf.js' + }, function(e) { + done(); + }); +}); + +/** + * Run test once with code coverage and exit + */ +gulp.task('cover', function (done) { + karma.start({ + configFile: __dirname + '/../../karma.conf.js', + singleRun: true, + reporters: ['coverage'], + preprocessors: { + 'test/**/*.js': ['babel'], + 'src/**/*.js': ['babel', 'coverage'] + }, + coverageReporter: { + type: 'html', + dir: 'build/reports/coverage' + } + }, function (e) { + done(); + }); +}); diff --git a/config.js b/config.js new file mode 100644 index 0000000..693a062 --- /dev/null +++ b/config.js @@ -0,0 +1,86 @@ +System.config({ + defaultJSExtensions: true, + transpiler: "babel", + babelOptions: { + "optional": [ + "es7.decorators", + "es7.classProperties", + "runtime" + ] + }, + paths: { + "*": "dist/*", + "github:*": "jspm_packages/github/*", + "npm:*": "jspm_packages/npm/*" + }, + + map: { + "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.11.0", + "aurelia-event-aggregator": "github:aurelia/event-aggregator@0.9.0", + "aurelia-logging": "github:aurelia/logging@0.8.0", + "babel": "npm:babel-core@5.8.25", + "babel-runtime": "npm:babel-runtime@5.8.25", + "bluebird": "npm:bluebird@2.10.2", + "core-js": "npm:core-js@0.9.18", + "faker": "npm:faker@3.0.1", + "firebase": "github:firebase/firebase-bower@2.3.1", + "text": "github:systemjs/plugin-text@0.0.2", + "github:aurelia/dependency-injection@0.11.0": { + "aurelia-logging": "github:aurelia/logging@0.8.0", + "aurelia-metadata": "github:aurelia/metadata@0.9.0", + "aurelia-pal": "github:aurelia/pal@0.2.0", + "core-js": "npm:core-js@0.9.18" + }, + "github:aurelia/event-aggregator@0.9.0": { + "aurelia-logging": "github:aurelia/logging@0.8.0" + }, + "github:aurelia/metadata@0.9.0": { + "aurelia-pal": "github:aurelia/pal@0.2.0", + "core-js": "npm:core-js@0.9.18" + }, + "github:jspm/nodelibs-assert@0.1.0": { + "assert": "npm:assert@1.3.0" + }, + "github:jspm/nodelibs-path@0.1.0": { + "path-browserify": "npm:path-browserify@0.0.0" + }, + "github:jspm/nodelibs-process@0.1.2": { + "process": "npm:process@0.11.2" + }, + "github:jspm/nodelibs-util@0.1.0": { + "util": "npm:util@0.10.3" + }, + "npm:assert@1.3.0": { + "util": "npm:util@0.10.3" + }, + "npm:babel-runtime@5.8.25": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:bluebird@2.10.2": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:core-js@0.9.18": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2", + "systemjs-json": "github:systemjs/plugin-json@0.1.0" + }, + "npm:faker@3.0.1": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:inherits@2.0.1": { + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:path-browserify@0.0.0": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:process@0.11.2": { + "assert": "github:jspm/nodelibs-assert@0.1.0" + }, + "npm:util@0.10.3": { + "inherits": "npm:inherits@2.0.1", + "process": "github:jspm/nodelibs-process@0.1.2" + } + } +}); diff --git a/dist/amd/authentication.js b/dist/amd/authentication.js new file mode 100644 index 0000000..40deed8 --- /dev/null +++ b/dist/amd/authentication.js @@ -0,0 +1,166 @@ +define(['exports', 'bluebird', 'firebase', 'aurelia-dependency-injection', './events', './user', './configuration'], function (exports, _bluebird, _firebase, _aureliaDependencyInjection, _events, _user, _configuration) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _Promise = _interopRequireDefault(_bluebird); + + var _Firebase = _interopRequireDefault(_firebase); + + var AuthenticationManager = (function () { + function AuthenticationManager(configuration, publisher) { + var _this = this; + + _classCallCheck(this, _AuthenticationManager); + + this._firebase = null; + this._publisher = null; + this.currentUser = null; + + this._firebase = new _Firebase['default'](configuration.getFirebaseUrl()); + this._publisher = publisher; + this.currentUser = new _user.User(); + + if (configuration.getMonitorAuthChange() === true) { + this._firebase.onAuth(function (result) { + _this._onUserAuthStateChanged(result); + }, this); + } + } + + _createClass(AuthenticationManager, [{ + key: 'createUser', + value: function createUser(email, password) { + var _this2 = this; + + return new _Promise['default'](function (resolve, reject) { + _this2._firebase.createUser({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new _user.User(result); + user.email = user.email || email; + _this2._publisher.publish(new _events.UserCreatedEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'signIn', + value: function signIn(email, password) { + var _this3 = this; + + return new _Promise['default'](function (resolve, reject) { + _this3._firebase.authWithPassword({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new _user.User(result); + _this3._publisher.publish(new _events.UserSignedInEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'createUserAndSignIn', + value: function createUserAndSignIn(email, password) { + var _this4 = this; + + return this.createUser(email, password).then(function () { + return _this4.signIn(email, password); + }); + } + }, { + key: 'signOut', + value: function signOut() { + var _this5 = this; + + return new _Promise['default'](function (resolve) { + _this5._firebase.unauth(); + _this5.currentUser.reset(); + resolve(); + }); + } + }, { + key: 'changeEmail', + value: function changeEmail(oldEmail, password, newEmail) { + var _this6 = this; + + return new _Promise['default'](function (resolve, reject) { + _this6._firebase.changeEmail({ oldEmail: oldEmail, password: password, newEmail: newEmail }, function (error) { + if (error) { + reject(error); + return; + } + + _this6.currentUser.email = newEmail; + var result = { oldEmail: oldEmail, newEmail: newEmail }; + _this6._publisher.publish(new _events.UserEmailChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'changePassword', + value: function changePassword(email, oldPassword, newPassword) { + var _this7 = this; + + return new _Promise['default'](function (resolve, reject) { + _this7._firebase.changePassword({ email: email, oldPassword: oldPassword, newPassword: newPassword }, function (error) { + if (error) { + reject(error); + return; + } + + var result = { email: email }; + _this7._publisher.publish(new _events.UserPasswordChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'deleteUser', + value: function deleteUser(email, password) { + var _this8 = this; + + return new _Promise['default'](function (resolve, reject) { + _this8._firebase.removeUser({ email: email, password: password }, function (error) { + if (error) { + reject(error); + return; + } + + _this8.currentUser.reset(); + var result = { email: email }; + _this8._publisher.publish(new _events.UserDeletedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: '_onUserAuthStateChanged', + value: function _onUserAuthStateChanged(authData) { + this.currentUser.update(authData); + this._publisher.publish(new _events.UserAuthStateChangedEvent(authData)); + } + }]); + + var _AuthenticationManager = AuthenticationManager; + AuthenticationManager = (0, _aureliaDependencyInjection.inject)(_configuration.Configuration, _events.Publisher)(AuthenticationManager) || AuthenticationManager; + return AuthenticationManager; + })(); + + exports.AuthenticationManager = AuthenticationManager; +}); \ No newline at end of file diff --git a/dist/amd/collection.js b/dist/amd/collection.js new file mode 100644 index 0000000..529060d --- /dev/null +++ b/dist/amd/collection.js @@ -0,0 +1,201 @@ +define(['exports', 'bluebird', 'firebase', 'aurelia-dependency-injection', './configuration'], function (exports, _bluebird, _firebase, _aureliaDependencyInjection, _configuration) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _Promise = _interopRequireDefault(_bluebird); + + var _Firebase = _interopRequireDefault(_firebase); + + var ReactiveCollection = (function () { + function ReactiveCollection(path) { + _classCallCheck(this, ReactiveCollection); + + this._query = null; + this._valueMap = new Map(); + this.items = []; + + if (!_aureliaDependencyInjection.Container || !_aureliaDependencyInjection.Container.instance) throw Error('Container has not been made global'); + var config = _aureliaDependencyInjection.Container.instance.get(_configuration.Configuration); + if (!config) throw Error('Configuration has not been set'); + + this._query = new _Firebase['default'](ReactiveCollection._getChildLocation(config.getFirebaseUrl(), path)); + this._listenToQuery(this._query); + } + + _createClass(ReactiveCollection, [{ + key: 'add', + value: function add(item) { + var _this = this; + + return new _Promise['default'](function (resolve, reject) { + var query = _this._query.ref().push(); + query.set(item, function (error) { + if (error) { + reject(error); + return; + } + resolve(item); + }); + }); + } + }, { + key: 'remove', + value: function remove(item) { + if (item === null || item.__firebaseKey__ === null) { + return _Promise['default'].reject({ message: 'Unknown item' }); + } + return this.removeByKey(item.__firebaseKey__); + } + }, { + key: 'getByKey', + value: function getByKey(key) { + return this._valueMap.get(key); + } + }, { + key: 'removeByKey', + value: function removeByKey(key) { + var _this2 = this; + + return new _Promise['default'](function (resolve, reject) { + _this2._query.ref().child(key).remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(key); + }); + }); + } + }, { + key: 'clear', + value: function clear() { + var _this3 = this; + + return new _Promise['default'](function (resolve, reject) { + var query = _this3._query.ref(); + query.remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + }, { + key: '_listenToQuery', + value: function _listenToQuery(query) { + var _this4 = this; + + query.on('child_added', function (snapshot, previousKey) { + _this4._onItemAdded(snapshot, previousKey); + }); + query.on('child_removed', function (snapshot) { + _this4._onItemRemoved(snapshot); + }); + query.on('child_changed', function (snapshot, previousKey) { + _this4._onItemChanded(snapshot, previousKey); + }); + query.on('child_moved', function (snapshot, previousKey) { + _this4._onItemMoved(snapshot, previousKey); + }); + } + }, { + key: '_stopListeningToQuery', + value: function _stopListeningToQuery(query) { + query.off(); + } + }, { + key: '_onItemAdded', + value: function _onItemAdded(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var index = previousKey !== null ? this.items.indexOf(this._valueMap.get(previousKey)) + 1 : 0; + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(index, 0, value); + } + }, { + key: '_onItemRemoved', + value: function _onItemRemoved(oldSnapshot) { + var key = oldSnapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var index = this.items.indexOf(value); + this._valueMap['delete'](key); + if (index !== -1) { + this.items.splice(index, 1); + } + } + }, { + key: '_onItemChanged', + value: function _onItemChanged(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var oldValue = this._valueMap.get(value.__firebaseKey__); + + if (!oldValue) { + return; + } + + this._valueMap['delete'](oldValue.__firebaseKey__); + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(this.items.indexOf(oldValue), 1, value); + } + }, { + key: '_onItemMoved', + value: function _onItemMoved(snapshot, previousKey) { + var key = snapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var previousValue = this._valueMap.get(previousKey); + var newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0; + this.items.splice(this.items.indexOf(value), 1); + this.items.splice(newIndex, 0, value); + } + }, { + key: '_valueFromSnapshot', + value: function _valueFromSnapshot(snapshot) { + var value = snapshot.val(); + if (!(value instanceof Object)) { + value = { + value: value, + __firebasePrimitive__: true + }; + } + value.__firebaseKey__ = snapshot.key(); + return value; + } + }], [{ + key: '_getChildLocation', + value: function _getChildLocation(root, path) { + if (!path) { + return root; + } + if (!root.endsWith('/')) { + root = root + '/'; + } + + return root + (Array.isArray(path) ? path.join('/') : path); + } + }]); + + return ReactiveCollection; + })(); + + exports.ReactiveCollection = ReactiveCollection; +}); \ No newline at end of file diff --git a/dist/amd/configuration.js b/dist/amd/configuration.js new file mode 100644 index 0000000..eebc92b --- /dev/null +++ b/dist/amd/configuration.js @@ -0,0 +1,82 @@ +define(['exports'], function (exports) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var ConfigurationDefaults = function ConfigurationDefaults() { + _classCallCheck(this, ConfigurationDefaults); + }; + + exports.ConfigurationDefaults = ConfigurationDefaults; + + ConfigurationDefaults._defaults = { + firebaseUrl: null, + monitorAuthChange: false + }; + + ConfigurationDefaults.defaults = function () { + var defaults = {}; + Object.assign(defaults, ConfigurationDefaults._defaults); + return defaults; + }; + + var Configuration = (function () { + function Configuration(innerConfig) { + _classCallCheck(this, Configuration); + + this.innerConfig = innerConfig; + this.values = this.innerConfig ? {} : ConfigurationDefaults.defaults(); + } + + _createClass(Configuration, [{ + key: 'getValue', + value: function getValue(identifier) { + if (this.values.hasOwnProperty(identifier) !== null && this.values[identifier] !== undefined) { + return this.values[identifier]; + } + if (this.innerConfig !== null) { + return this.innerConfig.getValue(identifier); + } + throw new Error('Config not found: ' + identifier); + } + }, { + key: 'setValue', + value: function setValue(identifier, value) { + this.values[identifier] = value; + return this; + } + }, { + key: 'getFirebaseUrl', + value: function getFirebaseUrl() { + return this.getValue('firebaseUrl'); + } + }, { + key: 'setFirebaseUrl', + value: function setFirebaseUrl(firebaseUrl) { + return this.setValue('firebaseUrl', firebaseUrl); + } + }, { + key: 'getMonitorAuthChange', + value: function getMonitorAuthChange() { + return this.getValue('monitorAuthChange'); + } + }, { + key: 'setMonitorAuthChange', + value: function setMonitorAuthChange() { + var monitorAuthChange = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + return this.setValue('monitorAuthChange', monitorAuthChange === true); + } + }]); + + return Configuration; + })(); + + exports.Configuration = Configuration; +}); \ No newline at end of file diff --git a/dist/amd/events.js b/dist/amd/events.js new file mode 100644 index 0000000..0c745ef --- /dev/null +++ b/dist/amd/events.js @@ -0,0 +1,170 @@ +define(['exports', 'aurelia-dependency-injection', 'aurelia-event-aggregator'], function (exports, _aureliaDependencyInjection, _aureliaEventAggregator) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var FirebaseEvent = function FirebaseEvent() { + _classCallCheck(this, FirebaseEvent); + + this.handled = false; + }; + + var UserEvent = (function (_FirebaseEvent) { + _inherits(UserEvent, _FirebaseEvent); + + function UserEvent() { + var uid = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, UserEvent); + + _get(Object.getPrototypeOf(UserEvent.prototype), 'constructor', this).call(this); + this.uid = uid; + } + + return UserEvent; + })(FirebaseEvent); + + var UserCreatedEvent = (function (_UserEvent) { + _inherits(UserCreatedEvent, _UserEvent); + + function UserCreatedEvent(data) { + _classCallCheck(this, UserCreatedEvent); + + _get(Object.getPrototypeOf(UserCreatedEvent.prototype), 'constructor', this).call(this, data.uid); + this.email = data.email; + } + + return UserCreatedEvent; + })(UserEvent); + + exports.UserCreatedEvent = UserCreatedEvent; + + var UserSignedInEvent = (function (_UserEvent2) { + _inherits(UserSignedInEvent, _UserEvent2); + + function UserSignedInEvent(data) { + _classCallCheck(this, UserSignedInEvent); + + _get(Object.getPrototypeOf(UserSignedInEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider; + this.email = data.email; + } + + return UserSignedInEvent; + })(UserEvent); + + exports.UserSignedInEvent = UserSignedInEvent; + + var UserSignedOutEvent = (function (_UserEvent3) { + _inherits(UserSignedOutEvent, _UserEvent3); + + function UserSignedOutEvent(data) { + _classCallCheck(this, UserSignedOutEvent); + + _get(Object.getPrototypeOf(UserSignedOutEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserSignedOutEvent; + })(UserEvent); + + exports.UserSignedOutEvent = UserSignedOutEvent; + + var UserEmailChangedEvent = (function (_UserEvent4) { + _inherits(UserEmailChangedEvent, _UserEvent4); + + function UserEmailChangedEvent(data) { + _classCallCheck(this, UserEmailChangedEvent); + + _get(Object.getPrototypeOf(UserEmailChangedEvent.prototype), 'constructor', this).call(this); + this.oldEmail = data.oldEmail; + this.newEmail = data.newEmail; + } + + return UserEmailChangedEvent; + })(UserEvent); + + exports.UserEmailChangedEvent = UserEmailChangedEvent; + + var UserPasswordChangedEvent = (function (_UserEvent5) { + _inherits(UserPasswordChangedEvent, _UserEvent5); + + function UserPasswordChangedEvent(data) { + _classCallCheck(this, UserPasswordChangedEvent); + + _get(Object.getPrototypeOf(UserPasswordChangedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserPasswordChangedEvent; + })(UserEvent); + + exports.UserPasswordChangedEvent = UserPasswordChangedEvent; + + var UserDeletedEvent = (function (_UserEvent6) { + _inherits(UserDeletedEvent, _UserEvent6); + + function UserDeletedEvent(data) { + _classCallCheck(this, UserDeletedEvent); + + _get(Object.getPrototypeOf(UserDeletedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserDeletedEvent; + })(UserEvent); + + exports.UserDeletedEvent = UserDeletedEvent; + + var UserAuthStateChangedEvent = (function (_UserEvent7) { + _inherits(UserAuthStateChangedEvent, _UserEvent7); + + function UserAuthStateChangedEvent(data) { + _classCallCheck(this, UserAuthStateChangedEvent); + + data = data || {}; + _get(Object.getPrototypeOf(UserAuthStateChangedEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider || null; + this.auth = data.auth || null; + this.expires = data.expires || 0; + } + + return UserAuthStateChangedEvent; + })(UserEvent); + + exports.UserAuthStateChangedEvent = UserAuthStateChangedEvent; + + var Publisher = (function () { + function Publisher(eventAggregator) { + _classCallCheck(this, _Publisher); + + this._eventAggregator = eventAggregator; + } + + _createClass(Publisher, [{ + key: 'publish', + value: function publish(event) { + if (event.handled) { + return; + } + this._eventAggregator.publish(event); + } + }]); + + var _Publisher = Publisher; + Publisher = (0, _aureliaDependencyInjection.inject)(_aureliaEventAggregator.EventAggregator)(Publisher) || Publisher; + return Publisher; + })(); + + exports.Publisher = Publisher; +}); \ No newline at end of file diff --git a/dist/amd/index.js b/dist/amd/index.js new file mode 100644 index 0000000..811728e --- /dev/null +++ b/dist/amd/index.js @@ -0,0 +1,48 @@ +define(['exports', './configuration', './user', './authentication', './collection', './events'], function (exports, _configuration, _user, _authentication, _collection, _events) { + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + exports.configure = configure; + + function _interopExportWildcard(obj, defaults) { var newObj = defaults({}, obj); delete newObj['default']; return newObj; } + + function _defaults(obj, defaults) { var keys = Object.getOwnPropertyNames(defaults); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = Object.getOwnPropertyDescriptor(defaults, key); if (value && value.configurable && obj[key] === undefined) { Object.defineProperty(obj, key, value); } } return obj; } + + Object.defineProperty(exports, 'Configuration', { + enumerable: true, + get: function get() { + return _configuration.Configuration; + } + }); + Object.defineProperty(exports, 'User', { + enumerable: true, + get: function get() { + return _user.User; + } + }); + Object.defineProperty(exports, 'AuthenticationManager', { + enumerable: true, + get: function get() { + return _authentication.AuthenticationManager; + } + }); + Object.defineProperty(exports, 'ReactiveCollection', { + enumerable: true, + get: function get() { + return _collection.ReactiveCollection; + } + }); + + _defaults(exports, _interopExportWildcard(_events, _defaults)); + + function configure(aurelia, configCallback) { + var config = new _configuration.Configuration(_configuration.Configuration.defaults); + + if (configCallback !== undefined && typeof configCallback === 'function') { + configCallback(config); + } + aurelia.instance(_configuration.Configuration, config); + } +}); \ No newline at end of file diff --git a/dist/amd/user.js b/dist/amd/user.js new file mode 100644 index 0000000..754b4d8 --- /dev/null +++ b/dist/amd/user.js @@ -0,0 +1,63 @@ +define(["exports"], function (exports) { + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var User = (function () { + _createClass(User, [{ + key: "isAuthenticated", + get: function get() { + return this.token && this.auth && this.expires > 0 || false; + } + }]); + + function User() { + var userData = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, User); + + this.uid = null; + this.provider = null; + this.token = null; + this.auth = null; + this.expires = 0; + this.email = null; + this.isTemporaryPassword = null; + this.profileImageUrl = null; + + this.update(userData); + } + + _createClass(User, [{ + key: "update", + value: function update(userData) { + userData = userData || {}; + this.uid = userData.uid || null; + this.provider = userData.provider || null; + this.token = userData.token || null; + this.auth = userData.auth || null; + this.expires = userData.expires || 0; + + userData.password = userData.password || {}; + this.isTemporaryPassword = userData.password.isTemporaryPassword || false; + this.profileImageUrl = userData.password.profileImageURL || null; + this.email = userData.password.email || null; + } + }, { + key: "reset", + value: function reset() { + this.update({}); + } + }]); + + return User; + })(); + + exports.User = User; +}); \ No newline at end of file diff --git a/dist/commonjs/authentication.js b/dist/commonjs/authentication.js new file mode 100644 index 0000000..d53efbb --- /dev/null +++ b/dist/commonjs/authentication.js @@ -0,0 +1,180 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _firebase = require('firebase'); + +var _firebase2 = _interopRequireDefault(_firebase); + +var _aureliaDependencyInjection = require('aurelia-dependency-injection'); + +var _events = require('./events'); + +var events = _interopRequireWildcard(_events); + +var _user = require('./user'); + +var _configuration = require('./configuration'); + +var AuthenticationManager = (function () { + function AuthenticationManager(configuration, publisher) { + var _this = this; + + _classCallCheck(this, _AuthenticationManager); + + this._firebase = null; + this._publisher = null; + this.currentUser = null; + + this._firebase = new _firebase2['default'](configuration.getFirebaseUrl()); + this._publisher = publisher; + this.currentUser = new _user.User(); + + if (configuration.getMonitorAuthChange() === true) { + this._firebase.onAuth(function (result) { + _this._onUserAuthStateChanged(result); + }, this); + } + } + + _createClass(AuthenticationManager, [{ + key: 'createUser', + value: function createUser(email, password) { + var _this2 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this2._firebase.createUser({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new _user.User(result); + user.email = user.email || email; + _this2._publisher.publish(new events.UserCreatedEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'signIn', + value: function signIn(email, password) { + var _this3 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this3._firebase.authWithPassword({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new _user.User(result); + _this3._publisher.publish(new events.UserSignedInEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'createUserAndSignIn', + value: function createUserAndSignIn(email, password) { + var _this4 = this; + + return this.createUser(email, password).then(function () { + return _this4.signIn(email, password); + }); + } + }, { + key: 'signOut', + value: function signOut() { + var _this5 = this; + + return new _bluebird2['default'](function (resolve) { + _this5._firebase.unauth(); + _this5.currentUser.reset(); + resolve(); + }); + } + }, { + key: 'changeEmail', + value: function changeEmail(oldEmail, password, newEmail) { + var _this6 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this6._firebase.changeEmail({ oldEmail: oldEmail, password: password, newEmail: newEmail }, function (error) { + if (error) { + reject(error); + return; + } + + _this6.currentUser.email = newEmail; + var result = { oldEmail: oldEmail, newEmail: newEmail }; + _this6._publisher.publish(new events.UserEmailChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'changePassword', + value: function changePassword(email, oldPassword, newPassword) { + var _this7 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this7._firebase.changePassword({ email: email, oldPassword: oldPassword, newPassword: newPassword }, function (error) { + if (error) { + reject(error); + return; + } + + var result = { email: email }; + _this7._publisher.publish(new events.UserPasswordChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'deleteUser', + value: function deleteUser(email, password) { + var _this8 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this8._firebase.removeUser({ email: email, password: password }, function (error) { + if (error) { + reject(error); + return; + } + + _this8.currentUser.reset(); + var result = { email: email }; + _this8._publisher.publish(new events.UserDeletedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: '_onUserAuthStateChanged', + value: function _onUserAuthStateChanged(authData) { + this.currentUser.update(authData); + this._publisher.publish(new events.UserAuthStateChangedEvent(authData)); + } + }]); + + var _AuthenticationManager = AuthenticationManager; + AuthenticationManager = (0, _aureliaDependencyInjection.inject)(_configuration.Configuration, events.Publisher)(AuthenticationManager) || AuthenticationManager; + return AuthenticationManager; +})(); + +exports.AuthenticationManager = AuthenticationManager; \ No newline at end of file diff --git a/dist/commonjs/collection.js b/dist/commonjs/collection.js new file mode 100644 index 0000000..47b21e1 --- /dev/null +++ b/dist/commonjs/collection.js @@ -0,0 +1,207 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _bluebird = require('bluebird'); + +var _bluebird2 = _interopRequireDefault(_bluebird); + +var _firebase = require('firebase'); + +var _firebase2 = _interopRequireDefault(_firebase); + +var _aureliaDependencyInjection = require('aurelia-dependency-injection'); + +var _configuration = require('./configuration'); + +var ReactiveCollection = (function () { + function ReactiveCollection(path) { + _classCallCheck(this, ReactiveCollection); + + this._query = null; + this._valueMap = new Map(); + this.items = []; + + if (!_aureliaDependencyInjection.Container || !_aureliaDependencyInjection.Container.instance) throw Error('Container has not been made global'); + var config = _aureliaDependencyInjection.Container.instance.get(_configuration.Configuration); + if (!config) throw Error('Configuration has not been set'); + + this._query = new _firebase2['default'](ReactiveCollection._getChildLocation(config.getFirebaseUrl(), path)); + this._listenToQuery(this._query); + } + + _createClass(ReactiveCollection, [{ + key: 'add', + value: function add(item) { + var _this = this; + + return new _bluebird2['default'](function (resolve, reject) { + var query = _this._query.ref().push(); + query.set(item, function (error) { + if (error) { + reject(error); + return; + } + resolve(item); + }); + }); + } + }, { + key: 'remove', + value: function remove(item) { + if (item === null || item.__firebaseKey__ === null) { + return _bluebird2['default'].reject({ message: 'Unknown item' }); + } + return this.removeByKey(item.__firebaseKey__); + } + }, { + key: 'getByKey', + value: function getByKey(key) { + return this._valueMap.get(key); + } + }, { + key: 'removeByKey', + value: function removeByKey(key) { + var _this2 = this; + + return new _bluebird2['default'](function (resolve, reject) { + _this2._query.ref().child(key).remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(key); + }); + }); + } + }, { + key: 'clear', + value: function clear() { + var _this3 = this; + + return new _bluebird2['default'](function (resolve, reject) { + var query = _this3._query.ref(); + query.remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + }, { + key: '_listenToQuery', + value: function _listenToQuery(query) { + var _this4 = this; + + query.on('child_added', function (snapshot, previousKey) { + _this4._onItemAdded(snapshot, previousKey); + }); + query.on('child_removed', function (snapshot) { + _this4._onItemRemoved(snapshot); + }); + query.on('child_changed', function (snapshot, previousKey) { + _this4._onItemChanded(snapshot, previousKey); + }); + query.on('child_moved', function (snapshot, previousKey) { + _this4._onItemMoved(snapshot, previousKey); + }); + } + }, { + key: '_stopListeningToQuery', + value: function _stopListeningToQuery(query) { + query.off(); + } + }, { + key: '_onItemAdded', + value: function _onItemAdded(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var index = previousKey !== null ? this.items.indexOf(this._valueMap.get(previousKey)) + 1 : 0; + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(index, 0, value); + } + }, { + key: '_onItemRemoved', + value: function _onItemRemoved(oldSnapshot) { + var key = oldSnapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var index = this.items.indexOf(value); + this._valueMap['delete'](key); + if (index !== -1) { + this.items.splice(index, 1); + } + } + }, { + key: '_onItemChanged', + value: function _onItemChanged(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var oldValue = this._valueMap.get(value.__firebaseKey__); + + if (!oldValue) { + return; + } + + this._valueMap['delete'](oldValue.__firebaseKey__); + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(this.items.indexOf(oldValue), 1, value); + } + }, { + key: '_onItemMoved', + value: function _onItemMoved(snapshot, previousKey) { + var key = snapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var previousValue = this._valueMap.get(previousKey); + var newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0; + this.items.splice(this.items.indexOf(value), 1); + this.items.splice(newIndex, 0, value); + } + }, { + key: '_valueFromSnapshot', + value: function _valueFromSnapshot(snapshot) { + var value = snapshot.val(); + if (!(value instanceof Object)) { + value = { + value: value, + __firebasePrimitive__: true + }; + } + value.__firebaseKey__ = snapshot.key(); + return value; + } + }], [{ + key: '_getChildLocation', + value: function _getChildLocation(root, path) { + if (!path) { + return root; + } + if (!root.endsWith('/')) { + root = root + '/'; + } + + return root + (Array.isArray(path) ? path.join('/') : path); + } + }]); + + return ReactiveCollection; +})(); + +exports.ReactiveCollection = ReactiveCollection; \ No newline at end of file diff --git a/dist/commonjs/configuration.js b/dist/commonjs/configuration.js new file mode 100644 index 0000000..90cf687 --- /dev/null +++ b/dist/commonjs/configuration.js @@ -0,0 +1,80 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var ConfigurationDefaults = function ConfigurationDefaults() { + _classCallCheck(this, ConfigurationDefaults); +}; + +exports.ConfigurationDefaults = ConfigurationDefaults; + +ConfigurationDefaults._defaults = { + firebaseUrl: null, + monitorAuthChange: false +}; + +ConfigurationDefaults.defaults = function () { + var defaults = {}; + Object.assign(defaults, ConfigurationDefaults._defaults); + return defaults; +}; + +var Configuration = (function () { + function Configuration(innerConfig) { + _classCallCheck(this, Configuration); + + this.innerConfig = innerConfig; + this.values = this.innerConfig ? {} : ConfigurationDefaults.defaults(); + } + + _createClass(Configuration, [{ + key: 'getValue', + value: function getValue(identifier) { + if (this.values.hasOwnProperty(identifier) !== null && this.values[identifier] !== undefined) { + return this.values[identifier]; + } + if (this.innerConfig !== null) { + return this.innerConfig.getValue(identifier); + } + throw new Error('Config not found: ' + identifier); + } + }, { + key: 'setValue', + value: function setValue(identifier, value) { + this.values[identifier] = value; + return this; + } + }, { + key: 'getFirebaseUrl', + value: function getFirebaseUrl() { + return this.getValue('firebaseUrl'); + } + }, { + key: 'setFirebaseUrl', + value: function setFirebaseUrl(firebaseUrl) { + return this.setValue('firebaseUrl', firebaseUrl); + } + }, { + key: 'getMonitorAuthChange', + value: function getMonitorAuthChange() { + return this.getValue('monitorAuthChange'); + } + }, { + key: 'setMonitorAuthChange', + value: function setMonitorAuthChange() { + var monitorAuthChange = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + return this.setValue('monitorAuthChange', monitorAuthChange === true); + } + }]); + + return Configuration; +})(); + +exports.Configuration = Configuration; \ No newline at end of file diff --git a/dist/commonjs/events.js b/dist/commonjs/events.js new file mode 100644 index 0000000..e1e2b60 --- /dev/null +++ b/dist/commonjs/events.js @@ -0,0 +1,172 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _aureliaDependencyInjection = require('aurelia-dependency-injection'); + +var _aureliaEventAggregator = require('aurelia-event-aggregator'); + +var FirebaseEvent = function FirebaseEvent() { + _classCallCheck(this, FirebaseEvent); + + this.handled = false; +}; + +var UserEvent = (function (_FirebaseEvent) { + _inherits(UserEvent, _FirebaseEvent); + + function UserEvent() { + var uid = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, UserEvent); + + _get(Object.getPrototypeOf(UserEvent.prototype), 'constructor', this).call(this); + this.uid = uid; + } + + return UserEvent; +})(FirebaseEvent); + +var UserCreatedEvent = (function (_UserEvent) { + _inherits(UserCreatedEvent, _UserEvent); + + function UserCreatedEvent(data) { + _classCallCheck(this, UserCreatedEvent); + + _get(Object.getPrototypeOf(UserCreatedEvent.prototype), 'constructor', this).call(this, data.uid); + this.email = data.email; + } + + return UserCreatedEvent; +})(UserEvent); + +exports.UserCreatedEvent = UserCreatedEvent; + +var UserSignedInEvent = (function (_UserEvent2) { + _inherits(UserSignedInEvent, _UserEvent2); + + function UserSignedInEvent(data) { + _classCallCheck(this, UserSignedInEvent); + + _get(Object.getPrototypeOf(UserSignedInEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider; + this.email = data.email; + } + + return UserSignedInEvent; +})(UserEvent); + +exports.UserSignedInEvent = UserSignedInEvent; + +var UserSignedOutEvent = (function (_UserEvent3) { + _inherits(UserSignedOutEvent, _UserEvent3); + + function UserSignedOutEvent(data) { + _classCallCheck(this, UserSignedOutEvent); + + _get(Object.getPrototypeOf(UserSignedOutEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserSignedOutEvent; +})(UserEvent); + +exports.UserSignedOutEvent = UserSignedOutEvent; + +var UserEmailChangedEvent = (function (_UserEvent4) { + _inherits(UserEmailChangedEvent, _UserEvent4); + + function UserEmailChangedEvent(data) { + _classCallCheck(this, UserEmailChangedEvent); + + _get(Object.getPrototypeOf(UserEmailChangedEvent.prototype), 'constructor', this).call(this); + this.oldEmail = data.oldEmail; + this.newEmail = data.newEmail; + } + + return UserEmailChangedEvent; +})(UserEvent); + +exports.UserEmailChangedEvent = UserEmailChangedEvent; + +var UserPasswordChangedEvent = (function (_UserEvent5) { + _inherits(UserPasswordChangedEvent, _UserEvent5); + + function UserPasswordChangedEvent(data) { + _classCallCheck(this, UserPasswordChangedEvent); + + _get(Object.getPrototypeOf(UserPasswordChangedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserPasswordChangedEvent; +})(UserEvent); + +exports.UserPasswordChangedEvent = UserPasswordChangedEvent; + +var UserDeletedEvent = (function (_UserEvent6) { + _inherits(UserDeletedEvent, _UserEvent6); + + function UserDeletedEvent(data) { + _classCallCheck(this, UserDeletedEvent); + + _get(Object.getPrototypeOf(UserDeletedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserDeletedEvent; +})(UserEvent); + +exports.UserDeletedEvent = UserDeletedEvent; + +var UserAuthStateChangedEvent = (function (_UserEvent7) { + _inherits(UserAuthStateChangedEvent, _UserEvent7); + + function UserAuthStateChangedEvent(data) { + _classCallCheck(this, UserAuthStateChangedEvent); + + data = data || {}; + _get(Object.getPrototypeOf(UserAuthStateChangedEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider || null; + this.auth = data.auth || null; + this.expires = data.expires || 0; + } + + return UserAuthStateChangedEvent; +})(UserEvent); + +exports.UserAuthStateChangedEvent = UserAuthStateChangedEvent; + +var Publisher = (function () { + function Publisher(eventAggregator) { + _classCallCheck(this, _Publisher); + + this._eventAggregator = eventAggregator; + } + + _createClass(Publisher, [{ + key: 'publish', + value: function publish(event) { + if (event.handled) { + return; + } + this._eventAggregator.publish(event); + } + }]); + + var _Publisher = Publisher; + Publisher = (0, _aureliaDependencyInjection.inject)(_aureliaEventAggregator.EventAggregator)(Publisher) || Publisher; + return Publisher; +})(); + +exports.Publisher = Publisher; \ No newline at end of file diff --git a/dist/commonjs/index.js b/dist/commonjs/index.js new file mode 100644 index 0000000..64ac3c4 --- /dev/null +++ b/dist/commonjs/index.js @@ -0,0 +1,59 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports.configure = configure; + +function _interopExportWildcard(obj, defaults) { var newObj = defaults({}, obj); delete newObj['default']; return newObj; } + +function _defaults(obj, defaults) { var keys = Object.getOwnPropertyNames(defaults); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = Object.getOwnPropertyDescriptor(defaults, key); if (value && value.configurable && obj[key] === undefined) { Object.defineProperty(obj, key, value); } } return obj; } + +var _configuration = require('./configuration'); + +Object.defineProperty(exports, 'Configuration', { + enumerable: true, + get: function get() { + return _configuration.Configuration; + } +}); + +var _user = require('./user'); + +Object.defineProperty(exports, 'User', { + enumerable: true, + get: function get() { + return _user.User; + } +}); + +var _authentication = require('./authentication'); + +Object.defineProperty(exports, 'AuthenticationManager', { + enumerable: true, + get: function get() { + return _authentication.AuthenticationManager; + } +}); + +var _collection = require('./collection'); + +Object.defineProperty(exports, 'ReactiveCollection', { + enumerable: true, + get: function get() { + return _collection.ReactiveCollection; + } +}); + +var _events = require('./events'); + +_defaults(exports, _interopExportWildcard(_events, _defaults)); + +function configure(aurelia, configCallback) { + var config = new _configuration.Configuration(_configuration.Configuration.defaults); + + if (configCallback !== undefined && typeof configCallback === 'function') { + configCallback(config); + } + aurelia.instance(_configuration.Configuration, config); +} \ No newline at end of file diff --git a/dist/commonjs/user.js b/dist/commonjs/user.js new file mode 100644 index 0000000..35fb719 --- /dev/null +++ b/dist/commonjs/user.js @@ -0,0 +1,61 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var User = (function () { + _createClass(User, [{ + key: "isAuthenticated", + get: function get() { + return this.token && this.auth && this.expires > 0 || false; + } + }]); + + function User() { + var userData = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, User); + + this.uid = null; + this.provider = null; + this.token = null; + this.auth = null; + this.expires = 0; + this.email = null; + this.isTemporaryPassword = null; + this.profileImageUrl = null; + + this.update(userData); + } + + _createClass(User, [{ + key: "update", + value: function update(userData) { + userData = userData || {}; + this.uid = userData.uid || null; + this.provider = userData.provider || null; + this.token = userData.token || null; + this.auth = userData.auth || null; + this.expires = userData.expires || 0; + + userData.password = userData.password || {}; + this.isTemporaryPassword = userData.password.isTemporaryPassword || false; + this.profileImageUrl = userData.password.profileImageURL || null; + this.email = userData.password.email || null; + } + }, { + key: "reset", + value: function reset() { + this.update({}); + } + }]); + + return User; +})(); + +exports.User = User; \ No newline at end of file diff --git a/dist/es6/authentication.js b/dist/es6/authentication.js new file mode 100644 index 0000000..69bd028 --- /dev/null +++ b/dist/es6/authentication.js @@ -0,0 +1,177 @@ +import Promise from 'bluebird'; +import Firebase from 'firebase'; +import {inject} from 'aurelia-dependency-injection'; + +import * as events from './events'; +import {User} from './user'; +import {Configuration} from './configuration'; + +/** + * Handles Firebase authentication features + */ +@inject(Configuration, events.Publisher) +export class AuthenticationManager { + + _firebase = null; + _publisher = null; + currentUser = null; + + /** + * Initializes a new instance of the AuthenticationManager + * @param {Configuration} configuration - The configuration to use + * @param {Publisher} publisher - The publisher used to broadcast system wide user events + */ + constructor( + configuration: Configuration, + publisher: Publisher) { + this._firebase = new Firebase(configuration.getFirebaseUrl()); + this._publisher = publisher; + this.currentUser = new User(); + + // Register auth state changed event + // This will handle user data update now and in the future. + if (configuration.getMonitorAuthChange() === true) { + this._firebase.onAuth((result) => { + this._onUserAuthStateChanged(result); + }, this); + } + } + + /** + * Creates a new user but does not authenticate him. + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} - Returns a promise which on completion will return the user infos + */ + createUser(email, password) : Promise { + return new Promise((resolve, reject) => { + this._firebase.createUser({email: email, password: password}, (error, result) => { + if (error) { + reject(error); + return; + } + + let user = new User(result); + user.email = user.email || email; // Because firebase result doesn't provide the email + this._publisher.publish(new events.UserCreatedEvent(user)); + resolve(user); + }); + }); + } + + /** + * Sign in a user with a password. + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} Returns a promise which on completion will return user infos + */ + signIn(email, password) : Promise { + return new Promise((resolve, reject) => { + this._firebase.authWithPassword({email: email, password: password}, (error, result) => { + if (error) { + reject(error); + return; + } + + let user = new User(result); + this._publisher.publish(new events.UserSignedInEvent(user)); + resolve(user); + }); + }); + } + + /** + * Creates a user and automatically sign in if creation succeed + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} - Returns a promise which on completion will return user infos + */ + createUserAndSignIn(email, password) : Promise { + return this.createUser(email, password).then(() => { + return this.signIn(email, password); + }); + } + + /** + * Sign out any authenticated user + * @returns {Promise} - Returns a promise + */ + signOut() : Promise { + return new Promise((resolve) => { + this._firebase.unauth(); + this.currentUser.reset(); + resolve(); + }); + } + + /** + * Changes the user email. + * User will be disconnected upon email change. + * @param {string} oldEmail - The current user email (email to be changed) + * @param {string} password - The current user password + * @param {string} newEmail - The new email + * @returns {Promise} - Returns a promise which on completion will return an object containing the old and new email + */ + changeEmail(oldEmail, password, newEmail) : Promise { + return new Promise((resolve, reject) => { + this._firebase.changeEmail({oldEmail: oldEmail, password: password, newEmail: newEmail}, (error) => { + if (error) { + reject(error); + return; + } + + this.currentUser.email = newEmail; + let result = {oldEmail: oldEmail, newEmail: newEmail}; + this._publisher.publish(new events.UserEmailChangedEvent(result)); + resolve(result); + }); + }); + } + + /** + * Changes the user password + * @param {string} email - The email of the user to change the password + * @param {string} oldPassword - The current password + * @param {string} newPassword - The new password + */ + changePassword(email, oldPassword, newPassword) : Promise { + return new Promise((resolve, reject) => { + this._firebase.changePassword({email: email, oldPassword: oldPassword, newPassword: newPassword}, (error) => { + if (error) { + reject(error); + return; + } + + let result = {email: email}; + this._publisher.publish(new events.UserPasswordChangedEvent(result)); + resolve(result); + }); + }); + } + + /** + * Deletes a user account + * @param {string} email - The users's email + * @param {string} password - The user's password + */ + deleteUser(email: string, password: string) : Promise { + return new Promise((resolve, reject) => { + this._firebase.removeUser({email: email, password: password}, (error) => { + if (error) { + reject(error); + return; + } + + this.currentUser.reset(); + let result = {email: email}; + this._publisher.publish(new events.UserDeletedEvent(result)); + resolve(result); + }); + }); + } + + _onUserAuthStateChanged(authData) { + this.currentUser.update(authData); + this._publisher.publish(new events.UserAuthStateChangedEvent(authData)); + } +} diff --git a/dist/es6/collection.js b/dist/es6/collection.js new file mode 100644 index 0000000..9f9675c --- /dev/null +++ b/dist/es6/collection.js @@ -0,0 +1,164 @@ +import Promise from 'bluebird'; +import Firebase from 'firebase'; +import {Container} from 'aurelia-dependency-injection'; +import {Configuration} from './configuration'; + +export class ReactiveCollection { + + _query = null; + _valueMap = new Map(); + items = []; + + constructor(path: Array) { + if (!Container || !Container.instance) throw Error('Container has not been made global'); + let config = Container.instance.get(Configuration); + if (!config) throw Error('Configuration has not been set'); + + this._query = new Firebase(ReactiveCollection._getChildLocation( + config.getFirebaseUrl(), + path)); + this._listenToQuery(this._query); + } + + add(item:any) : Promise { + return new Promise((resolve, reject) => { + let query = this._query.ref().push(); + query.set(item, (error) => { + if (error) { + reject(error); + return; + } + resolve(item); + }); + }); + } + + remove(item: any): Promise { + if (item === null || item.__firebaseKey__ === null) { + return Promise.reject({message: 'Unknown item'}); + } + return this.removeByKey(item.__firebaseKey__); + } + + getByKey(key): any { + return this._valueMap.get(key); + } + + removeByKey(key) { + return new Promise((resolve, reject) => { + this._query.ref().child(key).remove((error) =>{ + if (error) { + reject(error); + return; + } + resolve(key); + }); + }); + } + + clear() { + //this._stopListeningToQuery(this._query); + return new Promise((resolve, reject) => { + let query = this._query.ref(); + query.remove((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + + _listenToQuery(query) { + query.on('child_added', (snapshot, previousKey) => { + this._onItemAdded(snapshot, previousKey); + }); + query.on('child_removed', (snapshot) => { + this._onItemRemoved(snapshot); + }); + query.on('child_changed', (snapshot, previousKey) => { + this._onItemChanded(snapshot, previousKey); + }); + query.on('child_moved', (snapshot, previousKey) => { + this._onItemMoved(snapshot, previousKey); + }); + } + + _stopListeningToQuery(query) { + query.off(); + } + + _onItemAdded(snapshot, previousKey) { + let value = this._valueFromSnapshot(snapshot); + let index = previousKey !== null ? + this.items.indexOf(this._valueMap.get(previousKey)) + 1 : 0; + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(index, 0, value); + } + + _onItemRemoved(oldSnapshot) { + let key = oldSnapshot.key(); + let value = this._valueMap.get(key); + + if (!value) { + return; + } + + let index = this.items.indexOf(value); + this._valueMap.delete(key); + if (index !== -1) { + this.items.splice(index, 1); + } + } + + _onItemChanged(snapshot, previousKey) { + let value = this._valueFromSnapshot(snapshot); + let oldValue = this._valueMap.get(value.__firebaseKey__); + + if (!oldValue) { + return; + } + + this._valueMap.delete(oldValue.__firebaseKey__); + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(this.items.indexOf(oldValue), 1, value); + } + + _onItemMoved(snapshot, previousKey) { + let key = snapshot.key(); + let value = this._valueMap.get(key); + + if (!value) { + return; + } + + let previousValue = this._valueMap.get(previousKey); + let newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0; + this.items.splice(this.items.indexOf(value), 1); + this.items.splice(newIndex, 0, value); + } + + _valueFromSnapshot(snapshot) { + let value = snapshot.val(); + if (!(value instanceof Object)) { + value = { + value: value, + __firebasePrimitive__: true + }; + } + value.__firebaseKey__ = snapshot.key(); + return value; + } + + static _getChildLocation(root: string, path: Array) { + if (!path) { + return root; + } + if (!root.endsWith('/')) { + root = root + '/'; + } + + return root + (Array.isArray(path) ? path.join('/') : path); + } +} diff --git a/dist/es6/configuration.js b/dist/es6/configuration.js new file mode 100644 index 0000000..a7e5931 --- /dev/null +++ b/dist/es6/configuration.js @@ -0,0 +1,93 @@ +/** + * The default configuration + */ +export class ConfigurationDefaults { + +} + +ConfigurationDefaults._defaults = { + firebaseUrl: null, + monitorAuthChange: false +}; + +ConfigurationDefaults.defaults = function() { + let defaults = {}; + Object.assign(defaults, ConfigurationDefaults._defaults); + return defaults; +}; + +/** + * Configuration class used by the plugin + */ +export class Configuration { + + /** + * Initializes a new instance of the Configuration class + * @param {Object} innerConfig - The optional initial configuration values. If not provided will initialize using the defaults. + */ + constructor(innerConfig) { + this.innerConfig = innerConfig; + this.values = this.innerConfig ? {} : ConfigurationDefaults.defaults(); + } + + /** + * Gets the value of a configuration option by its identifier + * @param {string} identifier - The configuration option identifier + * @returns {any} - The value of the configuration option + * @throws {Error} - When configuration option is not found + */ + getValue(identifier) { + if (this.values.hasOwnProperty(identifier) !== null && this.values[identifier] !== undefined) { + return this.values[identifier]; + } + if (this.innerConfig !== null) { + return this.innerConfig.getValue(identifier); + } + throw new Error('Config not found: ' + identifier); + } + + /** + * Sets the value of a configuration option + * @param {string} identifier - The key used to store the configuration option value + * @param {any} value - The value of the configuration option + * @returns {Configuration} - The current configuration instance (Fluent API) + */ + setValue(identifier, value) { + this.values[identifier] = value; + return this; // fluent API + } + + /** + * Gets the value of the firebaseUrl configuration option + * @returns {string} - The value of the firebaseUrl configuration option + */ + getFirebaseUrl() { + return this.getValue('firebaseUrl'); + } + + /** + * Sets the value of the firebaseUrl configuration option + * @param {string} firebaseUrl - An URL to a valid Firebase location + * @returns {Configuration} - Returns the configuration instance (fluent API) + */ + setFirebaseUrl(firebaseUrl) { + return this.setValue('firebaseUrl', firebaseUrl); + } + + /** + * Gets the value of the monitorAuthChange configuration option + * @returns {boolean} - The value of the monitorAuthChange configuration option + */ + getMonitorAuthChange() { + return this.getValue('monitorAuthChange'); + } + + /** + * Sets the value of the monitorAuthChange configuration option + * @param monitorAuthChange + * @returns {Configuration} - Returns the configuration instance (fluent API) + */ + setMonitorAuthChange(monitorAuthChange: boolean = true) { + return this.setValue('monitorAuthChange', monitorAuthChange === true); + } +} diff --git a/dist/es6/events.js b/dist/es6/events.js new file mode 100644 index 0000000..8ef4e87 --- /dev/null +++ b/dist/es6/events.js @@ -0,0 +1,130 @@ +import {inject} from 'aurelia-dependency-injection'; +import {EventAggregator} from 'aurelia-event-aggregator'; + +class FirebaseEvent { + handled = false; + + constructor() { + } +} + +class UserEvent extends FirebaseEvent { + uid: string; + + constructor(uid: string = null) { + super(); + this.uid = uid; + } +} + +/** + * An event triggered when a user is created + */ +export class UserCreatedEvent extends UserEvent { + email:string; + constructor(data: Object) { + super(data.uid); + this.email = data.email; + } +} + +/** + * An event triggered when a user signed in + */ +export class UserSignedInEvent extends UserEvent { + provider: string; + email: string; + profileImageUrl:string; + + constructor(data: Object) { + super(data.uid); + this.provider = data.provider; + this.email = data.email; + } +} + +/** + * An event triggered when a user signed out + */ +export class UserSignedOutEvent extends UserEvent { + email: string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user's email has changed + */ +export class UserEmailChangedEvent extends UserEvent { + oldEmail: string; + newEmail: string; + constructor(data: Object) { + super(); + this.oldEmail = data.oldEmail; + this.newEmail = data.newEmail; + } +} + +/** + * An event triggered when a user's password has changed + */ +export class UserPasswordChangedEvent extends UserEvent { + email:string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user has been deleted + */ +export class UserDeletedEvent extends UserEvent { + email: string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user authentication state has changed + */ +export class UserAuthStateChangedEvent extends UserEvent { + provider: string; + auth: any; + expires: number; + + constructor(data: Object) { + data = data || {}; + super(data.uid); + this.provider = data.provider || null; + this.auth = data.auth || null; + this.expires = data.expires || 0; + } +} + +/** + * Handles publishing events in the system + */ +@inject(EventAggregator) +export class Publisher { + _eventAggregator; + + constructor(eventAggregator: EventAggregator) { + this._eventAggregator = eventAggregator; + } + + /** + * Publish an event + * @param {FirebaseEvent} event - The event to publish + */ + publish(event: FirebaseEvent) { + if (event.handled) { + return; + } + this._eventAggregator.publish(event); + } +} diff --git a/dist/es6/index.js b/dist/es6/index.js new file mode 100644 index 0000000..62e93a1 --- /dev/null +++ b/dist/es6/index.js @@ -0,0 +1,16 @@ +import {Configuration} from './configuration'; + +export {Configuration} from './configuration'; +export {User} from './user'; +export {AuthenticationManager} from './authentication'; +export {ReactiveCollection} from './collection'; +export * from './events'; + +export function configure(aurelia: Object, configCallback: Function) { + let config = new Configuration(Configuration.defaults); + + if (configCallback !== undefined && typeof configCallback === 'function') { + configCallback(config); + } + aurelia.instance(Configuration, config); +} diff --git a/dist/es6/user.js b/dist/es6/user.js new file mode 100644 index 0000000..507cb6e --- /dev/null +++ b/dist/es6/user.js @@ -0,0 +1,96 @@ +/** + * Represents a Firebase User + */ +export class User { + + /** + * A unique user ID, intented as the user's unique key accross all providers + * @type {string} + */ + uid = null; + + /** + * The authentication method used + * @type {string} + */ + provider = null; + + /** + * The Firebase authentication token for this session + * @type {string} + */ + token = null; + + /** + * The contents of the authentication token + * @type {Object} + */ + auth = null; + + /** + * A timestamp, in seconds since UNIX epoch, indicated when the authentication token expires + * @type {number} + */ + expires = 0; + + /** + * The user's email address + * @type {string} + */ + email = null; + + /** + * Whether or not the user authenticated using a temporary password, + * as used in password reset flows. + * @type {boolean} + */ + isTemporaryPassword = null; + + /** + * The URL to the user's Gravatar profile image + * @type {string} + */ + profileImageUrl = null; + + /** + * Whether or not the user is authenticated + * @type {boolean} True is the user is authenticated, false otherwise. + */ + get isAuthenticated() { + return (this.token && this.auth && this.expires > 0) || false; + } + + /** + * Initializes a new instance of user + * @param userData {Object} Optional object containing data + * to initialize this user with. + */ + constructor(userData: Object = null) { + this.update(userData); + } + + /** + * Update the current user instance with the provided data + * @param userData {Object} An object containing the data + */ + update(userData: Object) { + userData = userData || {}; + this.uid = userData.uid || null; + this.provider = userData.provider || null; + this.token = userData.token || null; + this.auth = userData.auth || null; + this.expires = userData.expires || 0; + + userData.password = userData.password || {}; + this.isTemporaryPassword = userData.password.isTemporaryPassword || false; + this.profileImageUrl = userData.password.profileImageURL || null; + this.email = userData.password.email || null; + } + + /** + * Reinitializes the current user instance. + */ + reset() { + this.update({}); + } +} diff --git a/dist/system/authentication.js b/dist/system/authentication.js new file mode 100644 index 0000000..95ebc37 --- /dev/null +++ b/dist/system/authentication.js @@ -0,0 +1,175 @@ +System.register(['bluebird', 'firebase', 'aurelia-dependency-injection', './events', './user', './configuration'], function (_export) { + 'use strict'; + + var Promise, Firebase, inject, events, User, Configuration, AuthenticationManager; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + return { + setters: [function (_bluebird) { + Promise = _bluebird['default']; + }, function (_firebase) { + Firebase = _firebase['default']; + }, function (_aureliaDependencyInjection) { + inject = _aureliaDependencyInjection.inject; + }, function (_events) { + events = _events; + }, function (_user) { + User = _user.User; + }, function (_configuration) { + Configuration = _configuration.Configuration; + }], + execute: function () { + AuthenticationManager = (function () { + function AuthenticationManager(configuration, publisher) { + var _this = this; + + _classCallCheck(this, _AuthenticationManager); + + this._firebase = null; + this._publisher = null; + this.currentUser = null; + + this._firebase = new Firebase(configuration.getFirebaseUrl()); + this._publisher = publisher; + this.currentUser = new User(); + + if (configuration.getMonitorAuthChange() === true) { + this._firebase.onAuth(function (result) { + _this._onUserAuthStateChanged(result); + }, this); + } + } + + _createClass(AuthenticationManager, [{ + key: 'createUser', + value: function createUser(email, password) { + var _this2 = this; + + return new Promise(function (resolve, reject) { + _this2._firebase.createUser({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new User(result); + user.email = user.email || email; + _this2._publisher.publish(new events.UserCreatedEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'signIn', + value: function signIn(email, password) { + var _this3 = this; + + return new Promise(function (resolve, reject) { + _this3._firebase.authWithPassword({ email: email, password: password }, function (error, result) { + if (error) { + reject(error); + return; + } + + var user = new User(result); + _this3._publisher.publish(new events.UserSignedInEvent(user)); + resolve(user); + }); + }); + } + }, { + key: 'createUserAndSignIn', + value: function createUserAndSignIn(email, password) { + var _this4 = this; + + return this.createUser(email, password).then(function () { + return _this4.signIn(email, password); + }); + } + }, { + key: 'signOut', + value: function signOut() { + var _this5 = this; + + return new Promise(function (resolve) { + _this5._firebase.unauth(); + _this5.currentUser.reset(); + resolve(); + }); + } + }, { + key: 'changeEmail', + value: function changeEmail(oldEmail, password, newEmail) { + var _this6 = this; + + return new Promise(function (resolve, reject) { + _this6._firebase.changeEmail({ oldEmail: oldEmail, password: password, newEmail: newEmail }, function (error) { + if (error) { + reject(error); + return; + } + + _this6.currentUser.email = newEmail; + var result = { oldEmail: oldEmail, newEmail: newEmail }; + _this6._publisher.publish(new events.UserEmailChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'changePassword', + value: function changePassword(email, oldPassword, newPassword) { + var _this7 = this; + + return new Promise(function (resolve, reject) { + _this7._firebase.changePassword({ email: email, oldPassword: oldPassword, newPassword: newPassword }, function (error) { + if (error) { + reject(error); + return; + } + + var result = { email: email }; + _this7._publisher.publish(new events.UserPasswordChangedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: 'deleteUser', + value: function deleteUser(email, password) { + var _this8 = this; + + return new Promise(function (resolve, reject) { + _this8._firebase.removeUser({ email: email, password: password }, function (error) { + if (error) { + reject(error); + return; + } + + _this8.currentUser.reset(); + var result = { email: email }; + _this8._publisher.publish(new events.UserDeletedEvent(result)); + resolve(result); + }); + }); + } + }, { + key: '_onUserAuthStateChanged', + value: function _onUserAuthStateChanged(authData) { + this.currentUser.update(authData); + this._publisher.publish(new events.UserAuthStateChangedEvent(authData)); + } + }]); + + var _AuthenticationManager = AuthenticationManager; + AuthenticationManager = inject(Configuration, events.Publisher)(AuthenticationManager) || AuthenticationManager; + return AuthenticationManager; + })(); + + _export('AuthenticationManager', AuthenticationManager); + } + }; +}); \ No newline at end of file diff --git a/dist/system/collection.js b/dist/system/collection.js new file mode 100644 index 0000000..a9d9f61 --- /dev/null +++ b/dist/system/collection.js @@ -0,0 +1,206 @@ +System.register(['bluebird', 'firebase', 'aurelia-dependency-injection', './configuration'], function (_export) { + 'use strict'; + + var Promise, Firebase, Container, Configuration, ReactiveCollection; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + return { + setters: [function (_bluebird) { + Promise = _bluebird['default']; + }, function (_firebase) { + Firebase = _firebase['default']; + }, function (_aureliaDependencyInjection) { + Container = _aureliaDependencyInjection.Container; + }, function (_configuration) { + Configuration = _configuration.Configuration; + }], + execute: function () { + ReactiveCollection = (function () { + function ReactiveCollection(path) { + _classCallCheck(this, ReactiveCollection); + + this._query = null; + this._valueMap = new Map(); + this.items = []; + + if (!Container || !Container.instance) throw Error('Container has not been made global'); + var config = Container.instance.get(Configuration); + if (!config) throw Error('Configuration has not been set'); + + this._query = new Firebase(ReactiveCollection._getChildLocation(config.getFirebaseUrl(), path)); + this._listenToQuery(this._query); + } + + _createClass(ReactiveCollection, [{ + key: 'add', + value: function add(item) { + var _this = this; + + return new Promise(function (resolve, reject) { + var query = _this._query.ref().push(); + query.set(item, function (error) { + if (error) { + reject(error); + return; + } + resolve(item); + }); + }); + } + }, { + key: 'remove', + value: function remove(item) { + if (item === null || item.__firebaseKey__ === null) { + return Promise.reject({ message: 'Unknown item' }); + } + return this.removeByKey(item.__firebaseKey__); + } + }, { + key: 'getByKey', + value: function getByKey(key) { + return this._valueMap.get(key); + } + }, { + key: 'removeByKey', + value: function removeByKey(key) { + var _this2 = this; + + return new Promise(function (resolve, reject) { + _this2._query.ref().child(key).remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(key); + }); + }); + } + }, { + key: 'clear', + value: function clear() { + var _this3 = this; + + return new Promise(function (resolve, reject) { + var query = _this3._query.ref(); + query.remove(function (error) { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + }, { + key: '_listenToQuery', + value: function _listenToQuery(query) { + var _this4 = this; + + query.on('child_added', function (snapshot, previousKey) { + _this4._onItemAdded(snapshot, previousKey); + }); + query.on('child_removed', function (snapshot) { + _this4._onItemRemoved(snapshot); + }); + query.on('child_changed', function (snapshot, previousKey) { + _this4._onItemChanded(snapshot, previousKey); + }); + query.on('child_moved', function (snapshot, previousKey) { + _this4._onItemMoved(snapshot, previousKey); + }); + } + }, { + key: '_stopListeningToQuery', + value: function _stopListeningToQuery(query) { + query.off(); + } + }, { + key: '_onItemAdded', + value: function _onItemAdded(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var index = previousKey !== null ? this.items.indexOf(this._valueMap.get(previousKey)) + 1 : 0; + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(index, 0, value); + } + }, { + key: '_onItemRemoved', + value: function _onItemRemoved(oldSnapshot) { + var key = oldSnapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var index = this.items.indexOf(value); + this._valueMap['delete'](key); + if (index !== -1) { + this.items.splice(index, 1); + } + } + }, { + key: '_onItemChanged', + value: function _onItemChanged(snapshot, previousKey) { + var value = this._valueFromSnapshot(snapshot); + var oldValue = this._valueMap.get(value.__firebaseKey__); + + if (!oldValue) { + return; + } + + this._valueMap['delete'](oldValue.__firebaseKey__); + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(this.items.indexOf(oldValue), 1, value); + } + }, { + key: '_onItemMoved', + value: function _onItemMoved(snapshot, previousKey) { + var key = snapshot.key(); + var value = this._valueMap.get(key); + + if (!value) { + return; + } + + var previousValue = this._valueMap.get(previousKey); + var newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0; + this.items.splice(this.items.indexOf(value), 1); + this.items.splice(newIndex, 0, value); + } + }, { + key: '_valueFromSnapshot', + value: function _valueFromSnapshot(snapshot) { + var value = snapshot.val(); + if (!(value instanceof Object)) { + value = { + value: value, + __firebasePrimitive__: true + }; + } + value.__firebaseKey__ = snapshot.key(); + return value; + } + }], [{ + key: '_getChildLocation', + value: function _getChildLocation(root, path) { + if (!path) { + return root; + } + if (!root.endsWith('/')) { + root = root + '/'; + } + + return root + (Array.isArray(path) ? path.join('/') : path); + } + }]); + + return ReactiveCollection; + })(); + + _export('ReactiveCollection', ReactiveCollection); + } + }; +}); \ No newline at end of file diff --git a/dist/system/configuration.js b/dist/system/configuration.js new file mode 100644 index 0000000..35818e6 --- /dev/null +++ b/dist/system/configuration.js @@ -0,0 +1,85 @@ +System.register([], function (_export) { + 'use strict'; + + var ConfigurationDefaults, Configuration; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + return { + setters: [], + execute: function () { + ConfigurationDefaults = function ConfigurationDefaults() { + _classCallCheck(this, ConfigurationDefaults); + }; + + _export('ConfigurationDefaults', ConfigurationDefaults); + + ConfigurationDefaults._defaults = { + firebaseUrl: null, + monitorAuthChange: false + }; + + ConfigurationDefaults.defaults = function () { + var defaults = {}; + Object.assign(defaults, ConfigurationDefaults._defaults); + return defaults; + }; + + Configuration = (function () { + function Configuration(innerConfig) { + _classCallCheck(this, Configuration); + + this.innerConfig = innerConfig; + this.values = this.innerConfig ? {} : ConfigurationDefaults.defaults(); + } + + _createClass(Configuration, [{ + key: 'getValue', + value: function getValue(identifier) { + if (this.values.hasOwnProperty(identifier) !== null && this.values[identifier] !== undefined) { + return this.values[identifier]; + } + if (this.innerConfig !== null) { + return this.innerConfig.getValue(identifier); + } + throw new Error('Config not found: ' + identifier); + } + }, { + key: 'setValue', + value: function setValue(identifier, value) { + this.values[identifier] = value; + return this; + } + }, { + key: 'getFirebaseUrl', + value: function getFirebaseUrl() { + return this.getValue('firebaseUrl'); + } + }, { + key: 'setFirebaseUrl', + value: function setFirebaseUrl(firebaseUrl) { + return this.setValue('firebaseUrl', firebaseUrl); + } + }, { + key: 'getMonitorAuthChange', + value: function getMonitorAuthChange() { + return this.getValue('monitorAuthChange'); + } + }, { + key: 'setMonitorAuthChange', + value: function setMonitorAuthChange() { + var monitorAuthChange = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + return this.setValue('monitorAuthChange', monitorAuthChange === true); + } + }]); + + return Configuration; + })(); + + _export('Configuration', Configuration); + } + }; +}); \ No newline at end of file diff --git a/dist/system/events.js b/dist/system/events.js new file mode 100644 index 0000000..7810715 --- /dev/null +++ b/dist/system/events.js @@ -0,0 +1,177 @@ +System.register(['aurelia-dependency-injection', 'aurelia-event-aggregator'], function (_export) { + 'use strict'; + + var inject, EventAggregator, FirebaseEvent, UserEvent, UserCreatedEvent, UserSignedInEvent, UserSignedOutEvent, UserEmailChangedEvent, UserPasswordChangedEvent, UserDeletedEvent, UserAuthStateChangedEvent, Publisher; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + return { + setters: [function (_aureliaDependencyInjection) { + inject = _aureliaDependencyInjection.inject; + }, function (_aureliaEventAggregator) { + EventAggregator = _aureliaEventAggregator.EventAggregator; + }], + execute: function () { + FirebaseEvent = function FirebaseEvent() { + _classCallCheck(this, FirebaseEvent); + + this.handled = false; + }; + + UserEvent = (function (_FirebaseEvent) { + _inherits(UserEvent, _FirebaseEvent); + + function UserEvent() { + var uid = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, UserEvent); + + _get(Object.getPrototypeOf(UserEvent.prototype), 'constructor', this).call(this); + this.uid = uid; + } + + return UserEvent; + })(FirebaseEvent); + + UserCreatedEvent = (function (_UserEvent) { + _inherits(UserCreatedEvent, _UserEvent); + + function UserCreatedEvent(data) { + _classCallCheck(this, UserCreatedEvent); + + _get(Object.getPrototypeOf(UserCreatedEvent.prototype), 'constructor', this).call(this, data.uid); + this.email = data.email; + } + + return UserCreatedEvent; + })(UserEvent); + + _export('UserCreatedEvent', UserCreatedEvent); + + UserSignedInEvent = (function (_UserEvent2) { + _inherits(UserSignedInEvent, _UserEvent2); + + function UserSignedInEvent(data) { + _classCallCheck(this, UserSignedInEvent); + + _get(Object.getPrototypeOf(UserSignedInEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider; + this.email = data.email; + } + + return UserSignedInEvent; + })(UserEvent); + + _export('UserSignedInEvent', UserSignedInEvent); + + UserSignedOutEvent = (function (_UserEvent3) { + _inherits(UserSignedOutEvent, _UserEvent3); + + function UserSignedOutEvent(data) { + _classCallCheck(this, UserSignedOutEvent); + + _get(Object.getPrototypeOf(UserSignedOutEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserSignedOutEvent; + })(UserEvent); + + _export('UserSignedOutEvent', UserSignedOutEvent); + + UserEmailChangedEvent = (function (_UserEvent4) { + _inherits(UserEmailChangedEvent, _UserEvent4); + + function UserEmailChangedEvent(data) { + _classCallCheck(this, UserEmailChangedEvent); + + _get(Object.getPrototypeOf(UserEmailChangedEvent.prototype), 'constructor', this).call(this); + this.oldEmail = data.oldEmail; + this.newEmail = data.newEmail; + } + + return UserEmailChangedEvent; + })(UserEvent); + + _export('UserEmailChangedEvent', UserEmailChangedEvent); + + UserPasswordChangedEvent = (function (_UserEvent5) { + _inherits(UserPasswordChangedEvent, _UserEvent5); + + function UserPasswordChangedEvent(data) { + _classCallCheck(this, UserPasswordChangedEvent); + + _get(Object.getPrototypeOf(UserPasswordChangedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserPasswordChangedEvent; + })(UserEvent); + + _export('UserPasswordChangedEvent', UserPasswordChangedEvent); + + UserDeletedEvent = (function (_UserEvent6) { + _inherits(UserDeletedEvent, _UserEvent6); + + function UserDeletedEvent(data) { + _classCallCheck(this, UserDeletedEvent); + + _get(Object.getPrototypeOf(UserDeletedEvent.prototype), 'constructor', this).call(this); + this.email = data.email; + } + + return UserDeletedEvent; + })(UserEvent); + + _export('UserDeletedEvent', UserDeletedEvent); + + UserAuthStateChangedEvent = (function (_UserEvent7) { + _inherits(UserAuthStateChangedEvent, _UserEvent7); + + function UserAuthStateChangedEvent(data) { + _classCallCheck(this, UserAuthStateChangedEvent); + + data = data || {}; + _get(Object.getPrototypeOf(UserAuthStateChangedEvent.prototype), 'constructor', this).call(this, data.uid); + this.provider = data.provider || null; + this.auth = data.auth || null; + this.expires = data.expires || 0; + } + + return UserAuthStateChangedEvent; + })(UserEvent); + + _export('UserAuthStateChangedEvent', UserAuthStateChangedEvent); + + Publisher = (function () { + function Publisher(eventAggregator) { + _classCallCheck(this, _Publisher); + + this._eventAggregator = eventAggregator; + } + + _createClass(Publisher, [{ + key: 'publish', + value: function publish(event) { + if (event.handled) { + return; + } + this._eventAggregator.publish(event); + } + }]); + + var _Publisher = Publisher; + Publisher = inject(EventAggregator)(Publisher) || Publisher; + return Publisher; + })(); + + _export('Publisher', Publisher); + } + }; +}); \ No newline at end of file diff --git a/dist/system/index.js b/dist/system/index.js new file mode 100644 index 0000000..cd9b762 --- /dev/null +++ b/dist/system/index.js @@ -0,0 +1,35 @@ +System.register(['./configuration', './user', './authentication', './collection', './events'], function (_export) { + 'use strict'; + + var Configuration; + + _export('configure', configure); + + function configure(aurelia, configCallback) { + var config = new Configuration(Configuration.defaults); + + if (configCallback !== undefined && typeof configCallback === 'function') { + configCallback(config); + } + aurelia.instance(Configuration, config); + } + + return { + setters: [function (_configuration) { + Configuration = _configuration.Configuration; + + _export('Configuration', _configuration.Configuration); + }, function (_user) { + _export('User', _user.User); + }, function (_authentication) { + _export('AuthenticationManager', _authentication.AuthenticationManager); + }, function (_collection) { + _export('ReactiveCollection', _collection.ReactiveCollection); + }, function (_events) { + for (var _key in _events) { + if (_key !== 'default') _export(_key, _events[_key]); + } + }], + execute: function () {} + }; +}); \ No newline at end of file diff --git a/dist/system/user.js b/dist/system/user.js new file mode 100644 index 0000000..f3f56e2 --- /dev/null +++ b/dist/system/user.js @@ -0,0 +1,66 @@ +System.register([], function (_export) { + "use strict"; + + var User; + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + return { + setters: [], + execute: function () { + User = (function () { + _createClass(User, [{ + key: "isAuthenticated", + get: function get() { + return this.token && this.auth && this.expires > 0 || false; + } + }]); + + function User() { + var userData = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0]; + + _classCallCheck(this, User); + + this.uid = null; + this.provider = null; + this.token = null; + this.auth = null; + this.expires = 0; + this.email = null; + this.isTemporaryPassword = null; + this.profileImageUrl = null; + + this.update(userData); + } + + _createClass(User, [{ + key: "update", + value: function update(userData) { + userData = userData || {}; + this.uid = userData.uid || null; + this.provider = userData.provider || null; + this.token = userData.token || null; + this.auth = userData.auth || null; + this.expires = userData.expires || 0; + + userData.password = userData.password || {}; + this.isTemporaryPassword = userData.password.isTemporaryPassword || false; + this.profileImageUrl = userData.password.profileImageURL || null; + this.email = userData.password.email || null; + } + }, { + key: "reset", + value: function reset() { + this.update({}); + } + }]); + + return User; + })(); + + _export("User", User); + } + }; +}); \ No newline at end of file diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md new file mode 100644 index 0000000..d39987d --- /dev/null +++ b/doc/CHANGELOG.md @@ -0,0 +1,6 @@ +### 0.1.0 (2015-10-17) + +#### Features + +* **all:** initial commit of aurelia-firebase ([5c444129](http://github.com/pulsarblow/aurelia-firebase/commit/5c44412992bb4c4da4d614fdc115e735903a67a7)) + diff --git a/doc/api.json b/doc/api.json new file mode 100644 index 0000000..6de3c0a --- /dev/null +++ b/doc/api.json @@ -0,0 +1 @@ +{"classes":[],"methods":[],"properties":[],"events":[]} \ No newline at end of file diff --git a/doc/intro.md b/doc/intro.md new file mode 100644 index 0000000..1cdc464 --- /dev/null +++ b/doc/intro.md @@ -0,0 +1,118 @@ +[![Join the chat at https://gitter.im/aurelia/discuss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/aurelia/discuss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +# Installation + + +#### Install via JSPM +Go into your project and verify it's already `npm install`'ed and `jspm install`'ed. Now execute following command to install the plugin via JSPM: + +``` +jspm install aurelia-firebase +``` + +this will add the plugin into your `jspm_packages` folder as well as an mapping-line into your `config.js` as: + +``` +"aurelia-firebase": "github:aurelia-firebase@X.X.X", +``` + +If you're feeling experimental or cannot wait for the next release, you could also install the latest version by executing: +``` +jspm install aurelia-firebase=github:pulsarblow/aurelia-firebase@master +``` + + +#### Migrate from aurelia-app to aurelia-app="main" +You'll need to register the plugin when your aurelia app is bootstrapping. If you have an aurelia app because you cloned a sample, there's a good chance that the app is bootstrapping based on default conventions. In that case, open your **index.html** file and look at the *body* tag. +``` html + +``` +Change the *aurelia-app* attribute to *aurelia-app="main"*. +``` html + +``` +The aurelia framework will now bootstrap the application by looking for your **main.js** file and executing the exported *configure* method. Go ahead and add a new **main.js** file with these contents: +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging(); + + aurelia.start().then(a => a.setRoot('app', document.body)); +} + +``` + +#### Load the plugin +During bootstrapping phase, you can now include the validation plugin: + +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging() + .plugin('aurelia-firebase'); //Add this line to load the plugin + + aurelia.start().then(a => a.setRoot('app', document.body)); +} +``` + +# Getting started + +TBD... + +#Configuration +##One config to rule them all +The firebase plugin has one global configuration instance, which is passed to an optional callback function when you first install the plugin: +``` javascript +export function configure(aurelia) { + aurelia.use + .standardConfiguration() + .developmentLogging() + .plugin('aurelia-firebase', (config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/'); }); + + aurelia.start().then(a => a.setRoot('app', document.body)); +} +``` + +>Note: if you want to access the global configuration instance at a later point in time, you can inject it: +``` javascript +import {Configuration} from 'aurelia-firebase'; +import {inject} from 'aurelia-framework'; + +>@inject(Configuration) +export class MyVM{ + constructor(config) + { + +> } +} +``` + +##Possible configuration +>Note: all these can be chained: +``` javascript +(config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/').setMonitorAuthChange(true); } +``` + +###config.setFirebaseUrl(firebaseUrl: string) +``` javascript +(config) => { config.setFirebaseUrl('https://myapp.firebaseio.com/'); } +``` +Sets the Firebase URL where your app answers. +This is required and the plugin will not start if not provided. + +###config.setMonitorAuthChange(monitorAuthChange: boolean) +``` javascript +(config) => { config.setMonitorAuthChange(true); } +``` +When set to true, the authentication manager will monitor authentication changes for the current user +The default value is false. + +#AuthenticationManager + +The authentication manager handles authentication aspects in the plugin. + +#ReactiveCollection + +The ReactiveCollection class handles firebase data synchronization. diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..4b28ec1 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,3 @@ +// all gulp tasks are located in the ./build/tasks directory +// gulp configuration is in files in ./build directory +require('require-dir')('build/tasks'); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..75fec1b --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,87 @@ +// Karma configuration + +var babelOptions = { + sourceMap: 'inline', + modules: 'system', + moduleIds: false, + comments: false, + loose: "all", + optional: [ + "es7.decorators", + "es7.classProperties" + ] +}; + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jspm', 'jasmine-ajax', 'jasmine'], + + jspm: { + // Edit this to your needs + loadFiles: ['src/**/*.js', 'test/**/*.js'], + paths: { + '*': '*.js' + } + }, + + + // list of files / patterns to load in the browser + files: [], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*.js': ['babel'], + 'src/**/*.js': ['babel'] + }, + + 'babelPreprocessor': { + options: babelOptions + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..35cf71b --- /dev/null +++ b/package.json @@ -0,0 +1,92 @@ +{ + "name": "aurelia-firebase", + "version": "0.1.0-beta", + "description": "A Firebase plugin for Aurelia.", + "keywords": [ + "aurelia", + "firebase", + "plugin" + ], + "homepage": "https://github.com/pulsarblow/aurelia-firebase", + "license": "MIT", + "author": "Alain Mereaux ", + "main": "dist/commonjs/index.js", + "scripts": { + "test": "gulp test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pulsarblow/aurelia-firebase#readme" + }, + "bugs": { + "url": "https://github.com/pulsarblow/aurelia-firebase/issues" + }, + "devDependencies": { + "aurelia-bundler": "^0.1.6", + "aurelia-tools": "^0.1.3", + "babel": "^5.8.23", + "babel-eslint": "^4.1.3", + "conventional-changelog": "0.0.11", + "del": "^1.1.0", + "faker": "^3.0.1", + "gulp": "^3.8.10", + "gulp-babel": "^5.1.0", + "gulp-bump": "^0.3.1", + "gulp-changed": "^1.1.0", + "gulp-eslint": "^1.0.0", + "gulp-notify": "^2.2.0", + "gulp-plumber": "^0.6.6", + "gulp-protractor": "^0.0.12", + "gulp-sourcemaps": "^1.3.0", + "gulp-typedoc": "^1.2.1", + "gulp-typedoc-extractor": "0.0.8", + "gulp-yuidoc": "^0.1.2", + "jasmine-core": "^2.1.3", + "jspm": "^0.16.12", + "karma": "^0.13.9", + "karma-babel-preprocessor": "^5.2.2", + "karma-chrome-launcher": "^0.2.0", + "karma-coverage": "^0.3.1", + "karma-jasmine": "^0.3.6", + "karma-jasmine-ajax": "^0.1.13", + "karma-jspm": "^2.0.1", + "object.assign": "^1.0.3", + "phantomjs": "^1.9.18", + "require-dir": "^0.1.0", + "run-sequence": "^1.0.2", + "vinyl": "^1.0.0", + "vinyl-paths": "^1.0.0", + "yargs": "^2.1.1" + }, + "directories": { + "doc": "doc", + "packages": "jspm_packages" + }, + "jspm": { + "main": "index", + "format": "amd", + "directories": { + "lib": "dist/amd" + }, + "dependencies": { + "aurelia-dependency-injection": "github:aurelia/dependency-injection@^0.11.0", + "aurelia-event-aggregator": "github:aurelia/event-aggregator@^0.9.0", + "aurelia-logging": "github:aurelia/logging@^0.8.0", + "bluebird": "npm:bluebird@^2.10.2", + "core-js": "npm:core-js@^0.9.4", + "firebase": "github:firebase/firebase-bower@^2.3.1", + "text": "github:systemjs/plugin-text@^0.0.2" + }, + "devDependencies": { + "babel": "npm:babel-core@^5.8.24", + "babel-runtime": "npm:babel-runtime@^5.8.24", + "core-js": "npm:core-js@^0.9.4", + "faker": "npm:faker@^3.0.1" + }, + "overrides": { + "npm:core-js@0.9.18": { + "main": "client/shim.min" + } + } + } +} diff --git a/src/authentication.js b/src/authentication.js new file mode 100644 index 0000000..69bd028 --- /dev/null +++ b/src/authentication.js @@ -0,0 +1,177 @@ +import Promise from 'bluebird'; +import Firebase from 'firebase'; +import {inject} from 'aurelia-dependency-injection'; + +import * as events from './events'; +import {User} from './user'; +import {Configuration} from './configuration'; + +/** + * Handles Firebase authentication features + */ +@inject(Configuration, events.Publisher) +export class AuthenticationManager { + + _firebase = null; + _publisher = null; + currentUser = null; + + /** + * Initializes a new instance of the AuthenticationManager + * @param {Configuration} configuration - The configuration to use + * @param {Publisher} publisher - The publisher used to broadcast system wide user events + */ + constructor( + configuration: Configuration, + publisher: Publisher) { + this._firebase = new Firebase(configuration.getFirebaseUrl()); + this._publisher = publisher; + this.currentUser = new User(); + + // Register auth state changed event + // This will handle user data update now and in the future. + if (configuration.getMonitorAuthChange() === true) { + this._firebase.onAuth((result) => { + this._onUserAuthStateChanged(result); + }, this); + } + } + + /** + * Creates a new user but does not authenticate him. + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} - Returns a promise which on completion will return the user infos + */ + createUser(email, password) : Promise { + return new Promise((resolve, reject) => { + this._firebase.createUser({email: email, password: password}, (error, result) => { + if (error) { + reject(error); + return; + } + + let user = new User(result); + user.email = user.email || email; // Because firebase result doesn't provide the email + this._publisher.publish(new events.UserCreatedEvent(user)); + resolve(user); + }); + }); + } + + /** + * Sign in a user with a password. + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} Returns a promise which on completion will return user infos + */ + signIn(email, password) : Promise { + return new Promise((resolve, reject) => { + this._firebase.authWithPassword({email: email, password: password}, (error, result) => { + if (error) { + reject(error); + return; + } + + let user = new User(result); + this._publisher.publish(new events.UserSignedInEvent(user)); + resolve(user); + }); + }); + } + + /** + * Creates a user and automatically sign in if creation succeed + * @param {string} email - The user email + * @param {string} password - The user password + * @returns {Promise} - Returns a promise which on completion will return user infos + */ + createUserAndSignIn(email, password) : Promise { + return this.createUser(email, password).then(() => { + return this.signIn(email, password); + }); + } + + /** + * Sign out any authenticated user + * @returns {Promise} - Returns a promise + */ + signOut() : Promise { + return new Promise((resolve) => { + this._firebase.unauth(); + this.currentUser.reset(); + resolve(); + }); + } + + /** + * Changes the user email. + * User will be disconnected upon email change. + * @param {string} oldEmail - The current user email (email to be changed) + * @param {string} password - The current user password + * @param {string} newEmail - The new email + * @returns {Promise} - Returns a promise which on completion will return an object containing the old and new email + */ + changeEmail(oldEmail, password, newEmail) : Promise { + return new Promise((resolve, reject) => { + this._firebase.changeEmail({oldEmail: oldEmail, password: password, newEmail: newEmail}, (error) => { + if (error) { + reject(error); + return; + } + + this.currentUser.email = newEmail; + let result = {oldEmail: oldEmail, newEmail: newEmail}; + this._publisher.publish(new events.UserEmailChangedEvent(result)); + resolve(result); + }); + }); + } + + /** + * Changes the user password + * @param {string} email - The email of the user to change the password + * @param {string} oldPassword - The current password + * @param {string} newPassword - The new password + */ + changePassword(email, oldPassword, newPassword) : Promise { + return new Promise((resolve, reject) => { + this._firebase.changePassword({email: email, oldPassword: oldPassword, newPassword: newPassword}, (error) => { + if (error) { + reject(error); + return; + } + + let result = {email: email}; + this._publisher.publish(new events.UserPasswordChangedEvent(result)); + resolve(result); + }); + }); + } + + /** + * Deletes a user account + * @param {string} email - The users's email + * @param {string} password - The user's password + */ + deleteUser(email: string, password: string) : Promise { + return new Promise((resolve, reject) => { + this._firebase.removeUser({email: email, password: password}, (error) => { + if (error) { + reject(error); + return; + } + + this.currentUser.reset(); + let result = {email: email}; + this._publisher.publish(new events.UserDeletedEvent(result)); + resolve(result); + }); + }); + } + + _onUserAuthStateChanged(authData) { + this.currentUser.update(authData); + this._publisher.publish(new events.UserAuthStateChangedEvent(authData)); + } +} diff --git a/src/collection.js b/src/collection.js new file mode 100644 index 0000000..9f9675c --- /dev/null +++ b/src/collection.js @@ -0,0 +1,164 @@ +import Promise from 'bluebird'; +import Firebase from 'firebase'; +import {Container} from 'aurelia-dependency-injection'; +import {Configuration} from './configuration'; + +export class ReactiveCollection { + + _query = null; + _valueMap = new Map(); + items = []; + + constructor(path: Array) { + if (!Container || !Container.instance) throw Error('Container has not been made global'); + let config = Container.instance.get(Configuration); + if (!config) throw Error('Configuration has not been set'); + + this._query = new Firebase(ReactiveCollection._getChildLocation( + config.getFirebaseUrl(), + path)); + this._listenToQuery(this._query); + } + + add(item:any) : Promise { + return new Promise((resolve, reject) => { + let query = this._query.ref().push(); + query.set(item, (error) => { + if (error) { + reject(error); + return; + } + resolve(item); + }); + }); + } + + remove(item: any): Promise { + if (item === null || item.__firebaseKey__ === null) { + return Promise.reject({message: 'Unknown item'}); + } + return this.removeByKey(item.__firebaseKey__); + } + + getByKey(key): any { + return this._valueMap.get(key); + } + + removeByKey(key) { + return new Promise((resolve, reject) => { + this._query.ref().child(key).remove((error) =>{ + if (error) { + reject(error); + return; + } + resolve(key); + }); + }); + } + + clear() { + //this._stopListeningToQuery(this._query); + return new Promise((resolve, reject) => { + let query = this._query.ref(); + query.remove((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + + _listenToQuery(query) { + query.on('child_added', (snapshot, previousKey) => { + this._onItemAdded(snapshot, previousKey); + }); + query.on('child_removed', (snapshot) => { + this._onItemRemoved(snapshot); + }); + query.on('child_changed', (snapshot, previousKey) => { + this._onItemChanded(snapshot, previousKey); + }); + query.on('child_moved', (snapshot, previousKey) => { + this._onItemMoved(snapshot, previousKey); + }); + } + + _stopListeningToQuery(query) { + query.off(); + } + + _onItemAdded(snapshot, previousKey) { + let value = this._valueFromSnapshot(snapshot); + let index = previousKey !== null ? + this.items.indexOf(this._valueMap.get(previousKey)) + 1 : 0; + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(index, 0, value); + } + + _onItemRemoved(oldSnapshot) { + let key = oldSnapshot.key(); + let value = this._valueMap.get(key); + + if (!value) { + return; + } + + let index = this.items.indexOf(value); + this._valueMap.delete(key); + if (index !== -1) { + this.items.splice(index, 1); + } + } + + _onItemChanged(snapshot, previousKey) { + let value = this._valueFromSnapshot(snapshot); + let oldValue = this._valueMap.get(value.__firebaseKey__); + + if (!oldValue) { + return; + } + + this._valueMap.delete(oldValue.__firebaseKey__); + this._valueMap.set(value.__firebaseKey__, value); + this.items.splice(this.items.indexOf(oldValue), 1, value); + } + + _onItemMoved(snapshot, previousKey) { + let key = snapshot.key(); + let value = this._valueMap.get(key); + + if (!value) { + return; + } + + let previousValue = this._valueMap.get(previousKey); + let newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0; + this.items.splice(this.items.indexOf(value), 1); + this.items.splice(newIndex, 0, value); + } + + _valueFromSnapshot(snapshot) { + let value = snapshot.val(); + if (!(value instanceof Object)) { + value = { + value: value, + __firebasePrimitive__: true + }; + } + value.__firebaseKey__ = snapshot.key(); + return value; + } + + static _getChildLocation(root: string, path: Array) { + if (!path) { + return root; + } + if (!root.endsWith('/')) { + root = root + '/'; + } + + return root + (Array.isArray(path) ? path.join('/') : path); + } +} diff --git a/src/configuration.js b/src/configuration.js new file mode 100644 index 0000000..a7e5931 --- /dev/null +++ b/src/configuration.js @@ -0,0 +1,93 @@ +/** + * The default configuration + */ +export class ConfigurationDefaults { + +} + +ConfigurationDefaults._defaults = { + firebaseUrl: null, + monitorAuthChange: false +}; + +ConfigurationDefaults.defaults = function() { + let defaults = {}; + Object.assign(defaults, ConfigurationDefaults._defaults); + return defaults; +}; + +/** + * Configuration class used by the plugin + */ +export class Configuration { + + /** + * Initializes a new instance of the Configuration class + * @param {Object} innerConfig - The optional initial configuration values. If not provided will initialize using the defaults. + */ + constructor(innerConfig) { + this.innerConfig = innerConfig; + this.values = this.innerConfig ? {} : ConfigurationDefaults.defaults(); + } + + /** + * Gets the value of a configuration option by its identifier + * @param {string} identifier - The configuration option identifier + * @returns {any} - The value of the configuration option + * @throws {Error} - When configuration option is not found + */ + getValue(identifier) { + if (this.values.hasOwnProperty(identifier) !== null && this.values[identifier] !== undefined) { + return this.values[identifier]; + } + if (this.innerConfig !== null) { + return this.innerConfig.getValue(identifier); + } + throw new Error('Config not found: ' + identifier); + } + + /** + * Sets the value of a configuration option + * @param {string} identifier - The key used to store the configuration option value + * @param {any} value - The value of the configuration option + * @returns {Configuration} - The current configuration instance (Fluent API) + */ + setValue(identifier, value) { + this.values[identifier] = value; + return this; // fluent API + } + + /** + * Gets the value of the firebaseUrl configuration option + * @returns {string} - The value of the firebaseUrl configuration option + */ + getFirebaseUrl() { + return this.getValue('firebaseUrl'); + } + + /** + * Sets the value of the firebaseUrl configuration option + * @param {string} firebaseUrl - An URL to a valid Firebase location + * @returns {Configuration} - Returns the configuration instance (fluent API) + */ + setFirebaseUrl(firebaseUrl) { + return this.setValue('firebaseUrl', firebaseUrl); + } + + /** + * Gets the value of the monitorAuthChange configuration option + * @returns {boolean} - The value of the monitorAuthChange configuration option + */ + getMonitorAuthChange() { + return this.getValue('monitorAuthChange'); + } + + /** + * Sets the value of the monitorAuthChange configuration option + * @param monitorAuthChange + * @returns {Configuration} - Returns the configuration instance (fluent API) + */ + setMonitorAuthChange(monitorAuthChange: boolean = true) { + return this.setValue('monitorAuthChange', monitorAuthChange === true); + } +} diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..8ef4e87 --- /dev/null +++ b/src/events.js @@ -0,0 +1,130 @@ +import {inject} from 'aurelia-dependency-injection'; +import {EventAggregator} from 'aurelia-event-aggregator'; + +class FirebaseEvent { + handled = false; + + constructor() { + } +} + +class UserEvent extends FirebaseEvent { + uid: string; + + constructor(uid: string = null) { + super(); + this.uid = uid; + } +} + +/** + * An event triggered when a user is created + */ +export class UserCreatedEvent extends UserEvent { + email:string; + constructor(data: Object) { + super(data.uid); + this.email = data.email; + } +} + +/** + * An event triggered when a user signed in + */ +export class UserSignedInEvent extends UserEvent { + provider: string; + email: string; + profileImageUrl:string; + + constructor(data: Object) { + super(data.uid); + this.provider = data.provider; + this.email = data.email; + } +} + +/** + * An event triggered when a user signed out + */ +export class UserSignedOutEvent extends UserEvent { + email: string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user's email has changed + */ +export class UserEmailChangedEvent extends UserEvent { + oldEmail: string; + newEmail: string; + constructor(data: Object) { + super(); + this.oldEmail = data.oldEmail; + this.newEmail = data.newEmail; + } +} + +/** + * An event triggered when a user's password has changed + */ +export class UserPasswordChangedEvent extends UserEvent { + email:string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user has been deleted + */ +export class UserDeletedEvent extends UserEvent { + email: string; + constructor(data: Object) { + super(); + this.email = data.email; + } +} + +/** + * An event triggered when a user authentication state has changed + */ +export class UserAuthStateChangedEvent extends UserEvent { + provider: string; + auth: any; + expires: number; + + constructor(data: Object) { + data = data || {}; + super(data.uid); + this.provider = data.provider || null; + this.auth = data.auth || null; + this.expires = data.expires || 0; + } +} + +/** + * Handles publishing events in the system + */ +@inject(EventAggregator) +export class Publisher { + _eventAggregator; + + constructor(eventAggregator: EventAggregator) { + this._eventAggregator = eventAggregator; + } + + /** + * Publish an event + * @param {FirebaseEvent} event - The event to publish + */ + publish(event: FirebaseEvent) { + if (event.handled) { + return; + } + this._eventAggregator.publish(event); + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..62e93a1 --- /dev/null +++ b/src/index.js @@ -0,0 +1,16 @@ +import {Configuration} from './configuration'; + +export {Configuration} from './configuration'; +export {User} from './user'; +export {AuthenticationManager} from './authentication'; +export {ReactiveCollection} from './collection'; +export * from './events'; + +export function configure(aurelia: Object, configCallback: Function) { + let config = new Configuration(Configuration.defaults); + + if (configCallback !== undefined && typeof configCallback === 'function') { + configCallback(config); + } + aurelia.instance(Configuration, config); +} diff --git a/src/user.js b/src/user.js new file mode 100644 index 0000000..507cb6e --- /dev/null +++ b/src/user.js @@ -0,0 +1,96 @@ +/** + * Represents a Firebase User + */ +export class User { + + /** + * A unique user ID, intented as the user's unique key accross all providers + * @type {string} + */ + uid = null; + + /** + * The authentication method used + * @type {string} + */ + provider = null; + + /** + * The Firebase authentication token for this session + * @type {string} + */ + token = null; + + /** + * The contents of the authentication token + * @type {Object} + */ + auth = null; + + /** + * A timestamp, in seconds since UNIX epoch, indicated when the authentication token expires + * @type {number} + */ + expires = 0; + + /** + * The user's email address + * @type {string} + */ + email = null; + + /** + * Whether or not the user authenticated using a temporary password, + * as used in password reset flows. + * @type {boolean} + */ + isTemporaryPassword = null; + + /** + * The URL to the user's Gravatar profile image + * @type {string} + */ + profileImageUrl = null; + + /** + * Whether or not the user is authenticated + * @type {boolean} True is the user is authenticated, false otherwise. + */ + get isAuthenticated() { + return (this.token && this.auth && this.expires > 0) || false; + } + + /** + * Initializes a new instance of user + * @param userData {Object} Optional object containing data + * to initialize this user with. + */ + constructor(userData: Object = null) { + this.update(userData); + } + + /** + * Update the current user instance with the provided data + * @param userData {Object} An object containing the data + */ + update(userData: Object) { + userData = userData || {}; + this.uid = userData.uid || null; + this.provider = userData.provider || null; + this.token = userData.token || null; + this.auth = userData.auth || null; + this.expires = userData.expires || 0; + + userData.password = userData.password || {}; + this.isTemporaryPassword = userData.password.isTemporaryPassword || false; + this.profileImageUrl = userData.password.profileImageURL || null; + this.email = userData.password.email || null; + } + + /** + * Reinitializes the current user instance. + */ + reset() { + this.update({}); + } +} diff --git a/test/authentication.spec.js b/test/authentication.spec.js new file mode 100644 index 0000000..9126cfb --- /dev/null +++ b/test/authentication.spec.js @@ -0,0 +1,211 @@ +import faker from 'faker'; +import {EventAggregator} from 'aurelia-event-aggregator'; +import {Configuration} from '../src/configuration'; +import {Publisher} from '../src/events'; +import {AuthenticationManager} from '../src/authentication'; +import {User} from '../src/user'; +import * as helpers from './testHelpers'; + +describe('AuthenticationManager', () => { + + describe('without MonitorAuthStateChange', () => { + let configuration = new Configuration(Configuration.defaults) + .setFirebaseUrl('https://aurelia-firebase.firebaseio.com') + .setMonitorAuthChange(false); + let authManager = createAuthManager(configuration), + email = createRandomTestEmail(), + password = createRandomTestPassword(); + + it('and should initialize properly', () => { + expect(typeof authManager).toBe('object'); + }); + + it('should create a new user', (done) => { + authManager.createUser(email, password) + .then((user)=> { + expect(typeof user).toBe('object'); + expect(user.uid).toBeTruthy(); + expect(user.email).toBe(email); + done(); + }) + .catch((error) => { + helpers.logError('createUser test error', error); + done.fail(); + }); + }); + + it('should sign in the user', (done) => { + authManager.signIn(email, password) + .then((user) => { + expect(typeof user).toBe('object'); + expect(user.provider).toBe('password'); + expect(user.uid).toBeTruthy(); + expect(user.token).toBeTruthy(); + expect(typeof user.auth).toBe('object'); + expect(user.auth.uid).toBe(user.uid); + expect(user.auth.provider).toBe(user.provider); + expect(user.expires).toBeGreaterThan(0); + expect(user.email).toBe(email); + expect(user.isTemporaryPassword).toBe(false); + expect(user.profileImageUrl).toBeTruthy(); + expect(user.isAuthenticated).toBe(true); + done(); + }) + .catch((error) => { + helpers.logError('signIn test error', error); + done.fail(); + }); + }); + + it('should change the user\'s email', (done) => { + let newEmail = createRandomTestEmail(); + authManager.changeEmail(email, password, newEmail) + .then((result) => { + expect(typeof result).toBe('object'); + expect(result.oldEmail).toBe(email); + expect(result.newEmail).toBe(newEmail); + email = newEmail; // used in the next test + done(); + }) + .catch((error) => { + helpers.logError('change email test error', error); + done.fail() + }); + }); + + it('should change the user\'s password', (done) => { + let newPassword = createRandomTestPassword(); + authManager.changePassword(email, password, newPassword) + .then((result) => { + expect(typeof result).toBe('object'); + expect(result.email).toBe(email); + password = newPassword; + done(); + }) + .catch((error) => { + helpers.logError('change password test error', error); + done.fail(); + }); + }); + + it('should sign out a user', (done) => { + authManager.signOut() + .then(() => { + done(); + }) + .catch((error) => { + helpers.logError('signout test error', error); + done.fail(); + }); + }); + + it('should be able to relog after email and password changed', (done) => { + authManager.signIn(email, password) + .then(()=> { + authManager.signOut(); + done(); + }) + .catch((error) => { + helpers.logError('signIn test error', error); + done.fail(); + }); + }); + + it('should delete a user', (done) => { + authManager.deleteUser(email, password) + .then((result) => { + expect(typeof result).toBe('object'); + expect(result.email).toBe(email); + done(); + }) + .catch((error) => { + helpers.logError('delete user test error', error); + done.fail(); + }); + }); + + it('should not be able to sign in after being deleted', (done) => { + authManager.signIn(email, password) + .then(() => { + helpers.logError('signIn test error : user shouldn\'t be signed in'); + done.fail(); + }) + .catch(() => { + done(); + }); + }); + }); + + describe('createUserAndSignin', () => { + + it('should create and sign in a user', (done) => { + + let configuration = new Configuration(Configuration.defaults) + .setFirebaseUrl('https://aurelia-firebase.firebaseio.com') + .setMonitorAuthChange(false); + let authManager = createAuthManager(configuration), + email = createRandomTestEmail(), + password = createRandomTestPassword(); + + authManager.createUserAndSignIn(email, password) + .then((user) => { + expect(typeof user).toBe('object'); + expect(user.isAuthenticated).toBe(true); + return authManager.signOut().then(() => { + return authManager.deleteUser(email, password).then(() => { + done(); + }); + }); + }) + .catch((error) => { + helpers.logError('create user and signIn test error', error); + done.fail(); + }); + }); + }); + + describe('with monitorAuthStateChange', () => { + + it('should monitor auth state changes', (done) => { + + let configuration = new Configuration(Configuration.defaults) + .setFirebaseUrl('https://aurelia-firebase.firebaseio.com') + .setMonitorAuthChange(true); + let authManager = createAuthManager(configuration), + email = createRandomTestEmail(), + password = createRandomTestPassword(); + + authManager.createUserAndSignIn(email, password) + .then((user) => { + expect(typeof user).toBe('object'); + expect(user.isAuthenticated).toBe(true); + expect(authManager.currentUser).toEqual(user); + return authManager.signOut().then(() => { + return authManager.deleteUser(email, password).then(() => { + expect(authManager.currentUser.isAuthenticated).toBe(false); + done(); + }); + }); + }) + .catch((error) => { + helpers.logError('create user and signIn test error', error); + done.fail(); + }); + }); + }); + + function createRandomTestEmail() { + setTimeout(function() { + // Timeout to force timestamp increment + }, 100); + return 'xunittests_' + new Date().getTime() + '_' + faker.internet.email().toLowerCase(); + } + + function createRandomTestPassword() { + return faker.internet.password(); + } + + function createAuthManager(configuration: Configuration) { + return new AuthenticationManager(configuration, new Publisher(new EventAggregator())); + } +}); diff --git a/test/collection.spec.js b/test/collection.spec.js new file mode 100644 index 0000000..9a4aba3 --- /dev/null +++ b/test/collection.spec.js @@ -0,0 +1,102 @@ +import faker from 'faker'; +import {Container} from 'aurelia-dependency-injection'; +import {EventAggregator} from 'aurelia-event-aggregator'; +import {Configuration} from '../src/configuration'; +import {ReactiveCollection} from '../src/collection'; +import {testHelpers} from './testHelpers'; + +describe('A ReactiveCollection', () => { + + let config = new Configuration(Configuration.defaults) + .setFirebaseUrl('https://aurelia-firebase.firebaseio.com') + .setMonitorAuthChange(false); + let container = new Container(); + container.makeGlobal(); + container.registerInstance(Configuration, config); + + let id = faker.random.uuid(), + value = faker.lorem.sentence(), + collection = new ReactiveCollection(['unit_tests', faker.random.uuid()]); + + it('should initialize properly', () => { + expect(typeof collection).toBe('object'); + }); + + it('should add an item properly', (done) => { + collection.add({id: id, value: value}).then((data) => { + expect(data).not.toBeNull(); + expect(data.id).toBe(id); + expect(data.value).toBe(value); + + let item = collection.items[0]; + expect(item).toBeDefined(); + expect(item.id).toBe(data.id); + expect(item.value).toBe(data.value); + expect(item.__firebaseKey__).toBeDefined(); + + done(); + }).catch((error) => { + testHelpers.logWarn('add item test failed', error); + done.fail(); + }); + }); + + it('should remove an item properly', (done) => { + let item = collection.items[0]; + collection.remove(item) + .then((itemKey) => { + expect(itemKey).toBeTruthy(); + item = collection.items[0]; + expect(item).not.toBeDefined(); + done(); + }) + .catch(() => { + done.fail(); + }); + }); + + it('should find an item by key', (done) => { + collection.add({id: id, value: value}).then((data) => { + let actual = collection.items[0]; + let expected = collection.getByKey(actual.__firebaseKey__); + expect(actual).toBe(expected); + done(); + }) + .catch(() => { + done.fail(); + }); + }); + + it('should clear the collection accordingly', (done) => { + collection.clear().then(() => { + expect(collection.items.length).toBe(0); + done(); + }); + }); + + it('should handle a path location provided as a string', () => { + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com')) + .toBe('https://aurelia-firebase.firebaseio.com'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com/')) + .toBe('https://aurelia-firebase.firebaseio.com/'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com', null)) + .toBe('https://aurelia-firebase.firebaseio.com'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com', 'apath')) + .toBe('https://aurelia-firebase.firebaseio.com/apath'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com/', 'apath')) + .toBe('https://aurelia-firebase.firebaseio.com/apath'); + }); + + it('should handle a path location provided as an array', () => { + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com')) + .toBe('https://aurelia-firebase.firebaseio.com'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com/')) + .toBe('https://aurelia-firebase.firebaseio.com/'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com', null)) + .toBe('https://aurelia-firebase.firebaseio.com'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com', ['apath'])) + .toBe('https://aurelia-firebase.firebaseio.com/apath'); + expect(ReactiveCollection._getChildLocation('https://aurelia-firebase.firebaseio.com/', ['one', 'another'])) + .toBe('https://aurelia-firebase.firebaseio.com/one/another'); + }); +}); diff --git a/test/configuration.spec.js b/test/configuration.spec.js new file mode 100644 index 0000000..340e438 --- /dev/null +++ b/test/configuration.spec.js @@ -0,0 +1,57 @@ +import {Configuration} from '../src/configuration'; + +describe('Configuration', () => { + it('should have default values', () => { + let config = new Configuration(); + expect(config.getFirebaseUrl()).toBeNull(); + expect(config.getMonitorAuthChange()).toBe(false); + }); + + it('should be configurable', () => { + let config = new Configuration(); + expect(config.setFirebaseUrl('https://google.com')).toBe(config); // fluent api check + expect(config.getFirebaseUrl()).toBe('https://google.com'); + + config = new Configuration(); + expect(config.setMonitorAuthChange(true)).toBe(config); // fluent api check + expect(config.getMonitorAuthChange()).toBe(true); + }); + + it('should never change the defaults', () => { + let config1 = new Configuration(); + let config2 = new Configuration(); + + expect(config2.getFirebaseUrl()).toBeNull(); + config1.setFirebaseUrl('https://google.com'); + expect(config2.getFirebaseUrl()).toBeNull(); + }); + + it('should get values on the parent when it does not have a value itself', () => { + let parentConfig = new Configuration(); + parentConfig.setValue('test', 'a'); + let config = new Configuration(parentConfig); + expect(config.getValue('test')).toBe('a'); + }); + + it('should not copy the values when using get but instead delegate to the parent', () => { + let parentConfig = new Configuration(); + parentConfig.setValue('test', 'a'); + let config = new Configuration(parentConfig); + expect(config.getValue('test')).toBe('a'); + + parentConfig.setValue('test', 'b'); + expect(config.getValue('test')).toBe('b'); + }); + + it('should allow to override parents with own values', () => { + let parentConfig = new Configuration(); + parentConfig.setValue('test', 'a'); + let config = new Configuration(parentConfig); + expect(config.getValue('test')).toBe('a'); + + config.setValue('test', 'c'); + parentConfig.setValue('test', 'a'); + expect(config.getValue('test')).toBe('c'); + + }); +}); diff --git a/test/events.spec.js b/test/events.spec.js new file mode 100644 index 0000000..77624a6 --- /dev/null +++ b/test/events.spec.js @@ -0,0 +1,155 @@ +import faker from 'faker'; +import {EventAggregator} from 'aurelia-event-aggregator'; +import * as events from '../src/events'; +import * as testHelpers from './testHelpers'; + +describe('A Publisher', () => { + + it('should initialize properly', () => { + let publisher = new events.Publisher(new EventAggregator()); + expect(typeof publisher).toBe('object'); + }); + + describe('publishing events', () => { + let eventAggregator = new EventAggregator(), + publisher = new events.Publisher(eventAggregator); + + it('should publish a UserCreatedEvent', (done) => { + let data = createEventData(); + + eventAggregator.subscribeOnce(events.UserCreatedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBe(data.uid); + expect(eventData.email).toBe(data.email); + done(); + }); + + publisher.publish(new events.UserCreatedEvent(data)); + }); + + it('should publish a UserSignedInEvent', (done) => { + let data = createEventData(); + + eventAggregator.subscribeOnce(events.UserSignedInEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBe(data.uid); + expect(eventData.email).toBe(data.email); + expect(eventData.provider).toBe(data.provider); + done(); + }); + + publisher.publish(new events.UserSignedInEvent(data)); + }); + + it('should publish a UserSignedOutEvent', (done) => { + let data = {email: faker.internet.email()}; + + eventAggregator.subscribeOnce(events.UserSignedOutEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBeNull(); + expect(eventData.email).toBe(data.email); + done(); + }); + + publisher.publish(new events.UserSignedOutEvent(data)); + }); + + it('should publish a UserEmailChangedEvent', (done) => { + let data = { + oldEmail: faker.internet.email(), + newEmail: faker.internet.email() + }; + + eventAggregator.subscribeOnce(events.UserEmailChangedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBeNull(); + expect(eventData.oldEmail).toBe(data.oldEmail); + expect(eventData.newEmail).toBe(data.newEmail); + done(); + }); + + publisher.publish(new events.UserEmailChangedEvent(data)); + }); + + it('should publish a UserPasswordChangedEvent', (done) => { + let data = {email: faker.internet.email()}; + + eventAggregator.subscribeOnce(events.UserPasswordChangedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.email).toBe(data.email); + done(); + }); + + publisher.publish(new events.UserPasswordChangedEvent(data)); + }); + + it('should publish a UserDeletedEvent', (done) => { + let data = { email: faker.internet.email() }; + + eventAggregator.subscribeOnce(events.UserDeletedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.email).toBe(data.email); + done(); + }); + + publisher.publish(new events.UserDeletedEvent(data)); + }); + + it('should publish a UserAuthStateChangedEvent with data', (done) => { + let uid = faker.random.uuid(), + data = { uid: uid, provider: 'password', auth: {uid:uid, provider:'password'}, expires: 111111 }; + + eventAggregator.subscribeOnce(events.UserAuthStateChangedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBe(data.uid); + expect(eventData.provider).toBe(data.provider); + expect(eventData.auth).toBe(data.auth); + expect(eventData.expires).toBe(data.expires); + done(); + }); + + publisher.publish(new events.UserAuthStateChangedEvent(data)); + }); + + it('should publish a UserAuthStateChangedEvent with no data', (done) => { + let data = { }; + + eventAggregator.subscribeOnce(events.UserAuthStateChangedEvent, (eventData) => { + expect(eventData).toBeDefined(); + expect(eventData).not.toBeNull(); + expect(eventData.uid).toBeNull(); + expect(eventData.provider).toBeNull(); + expect(eventData.auth).toBeNull(); + expect(eventData.expires).toBe(0); + done(); + }); + + publisher.publish(new events.UserAuthStateChangedEvent(data)); + }); + + it('should not publish handled event', (done) => { + let event = new events.UserCreatedEvent({}); + event.handled = true; + eventAggregator.subscribeOnce(events.UserCreatedEvent, () => { + done.fail(); + }); + publisher.publish(event); + done(); + }); + }); +}); + +function createEventData() { + return { + uid: faker.random.uuid(), + email: faker.internet.email(), + provider: faker.hacker.noun() + }; +} diff --git a/test/plugin.spec.js b/test/plugin.spec.js new file mode 100644 index 0000000..f6624eb --- /dev/null +++ b/test/plugin.spec.js @@ -0,0 +1,37 @@ +import {Container} from 'aurelia-dependency-injection'; +import {configure} from '../src/index'; + +describe('aurelia-firebase', () => { + + let container = new Container(); + + describe('plugin initialization', () => { + let aurelia = { + globalizeResources: () => { + + }, + singleton: (type: any, implementation: Function) => { + this.container.registerSingleton(type,implementation); + return aurelia; + }, + container: container + }; + + it('should export configure function', () => { + expect(typeof configure).toBe('function'); + }); + + it('should accept a setup callback passing back the configuration instance', (done) => { + let firebaseUrl = 'https://aurelia-firebase.firebaseio.com', + loginRoute = 'a/login/route', + cb = (instance) => { + expect(typeof instance).toBe('object'); + instance + .setFirebaseUrl(firebaseUrl) + .setMonitorAuthChange(true); + done(); + }; + configure(aurelia, cb); + }); + }); +}); diff --git a/test/testHelpers.js b/test/testHelpers.js new file mode 100644 index 0000000..593f08f --- /dev/null +++ b/test/testHelpers.js @@ -0,0 +1,19 @@ +export function logInfo(message, args) { + log('log', message, args); +} + +export function logWarn(message, args) { + log('warn', message, args); +} + +export function logError(message, args) { + log('error', message, args); +} + +function log(method, message, args) { + if(args) { + console[method](message, JSON.stringify(args)); + } else { + console[method](message); + } +} diff --git a/test/user.spec.js b/test/user.spec.js new file mode 100644 index 0000000..5d258b5 --- /dev/null +++ b/test/user.spec.js @@ -0,0 +1,64 @@ +import {User} from '../src/user'; + +describe('User', () => { + it('should initialize properly without initializer', () => { + let user = new User(); + expect(user.uid).toBeNull(); + expect(user.provider).toBeNull(); + expect(user.token).toBeNull(); + expect(user.auth).toBeNull(); + expect(user.expires).toBe(0); + expect(user.isTemporaryPassword).toBe(false); + expect(user.profileImageUrl).toBeNull(); + expect(user.email).toBeNull(); + expect(user.isAuthenticated).toBe(false); + }); + + it('should initialize properly with uncomplete initializer', () => { + let user = new User({uid:'uid'}); + expect(user.uid).toBe('uid'); + expect(user.provider).toBeNull(); + expect(user.token).toBeNull(); + expect(user.auth).toBeNull(); + expect(user.expires).toBe(0); + expect(user.isTemporaryPassword).toBe(false); + expect(user.profileImageUrl).toBeNull(); + expect(user.email).toBeNull(); + expect(user.isAuthenticated).toBe(false); + }); + + it('should returns truthy authenticated state', () => { + let user = new User({auth: {}, token:'token', expires: 1000}); + expect(user.isAuthenticated).toBe(true); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: {}, token:'token', expires: 0}); + expect(user.isAuthenticated).toBe(false); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: {}, token:'token', expires: '45'}); + expect(user.isAuthenticated).toBe(true); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: {}, token:'token', expires: 'abs'}); + expect(user.isAuthenticated).toBe(false); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: {}, token:null, expires: 100}); + expect(user.isAuthenticated).toBe(false); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: null, token:'token', expires: 1000}); + expect(user.isAuthenticated).toBe(false); + }); + + it('should returns false authenticated state', () => { + let user = new User({auth: undefined, token:''}); + expect(user.isAuthenticated).toBe(false); + }); +});