Skip to content

Commit

Permalink
Merge pull request #1310 from dvoytenko/access6
Browse files Browse the repository at this point in the history
Login flow and dialog implementation.
  • Loading branch information
dvoytenko committed Jan 6, 2016
2 parents 0a7679c + 9c25412 commit 6055ccc
Show file tree
Hide file tree
Showing 6 changed files with 715 additions and 2 deletions.
43 changes: 42 additions & 1 deletion extensions/amp-access/0.1/amp-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {isExperimentOn} from '../../../src/experiments';
import {listenOnce} from '../../../src/event-helper';
import {log} from '../../../src/log';
import {onDocumentReady} from '../../../src/document-state';
import {openLoginDialog} from './login-dialog';
import {parseQueryString} from '../../../src/url';
import {timer} from '../../../src/timer';
import {urlReplacementsFor} from '../../../src/url-replacements';
import {viewerFor} from '../../../src/viewer';
Expand Down Expand Up @@ -110,11 +112,17 @@ export class AccessService {
/** @private @const {!Viewport} */
this.viewport_ = viewportFor(this.win);

/** @private @const {function(string):Promise<string>} */
this.openLoginDialog_ = openLoginDialog.bind(null, this.win);

/** @private {?Promise<string>} */
this.readerIdPromise_ = null;

/** @private {?Promise} */
this.reportViewPromise_ = null;

/** @private {?Promise} */
this.loginPromise_ = null;
}

/**
Expand Down Expand Up @@ -365,7 +373,40 @@ export class AccessService {
* @private
*/
handleAction_(invocation) {
log.fine(TAG, 'Invocation: ', invocation);
if (invocation.method == 'login') {
this.login();
}
}

/**
* Runs the Login flow. Returns a promise that is resolved if login succeeds
* or is rejected if login fails. Login flow is performed as an external
* 1st party Web dialog. It's goal is to authenticate the reader.
* @return {!Promise}
*/
login() {
if (this.loginPromise_) {
return this.loginPromise_;
}

log.fine(TAG, 'Start login');
const urlPromise = this.buildUrl_(this.config_.login);
this.loginPromise_ = this.openLoginDialog_(urlPromise).then(result => {
log.fine(TAG, 'Login dialog completed: ', result);
this.loginPromise_ = null;
const query = parseQueryString(result);
const s = query['success'];
const success = (s == 'true' || s == 'yes' || s == '1');
if (success) {
// Repeat the authorization flow.
return this.runAuthorization_();
}
}).catch(reason => {
log.fine(TAG, 'Login dialog failed: ', reason);
this.loginPromise_ = null;
throw reason;
});
return this.loginPromise_;
}
}

Expand Down
234 changes: 234 additions & 0 deletions extensions/amp-access/0.1/login-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {getMode} from '../../../src/mode';
import {listen} from '../../../src/event-helper';
import {log} from '../../../src/log';
import {parseUrl} from '../../../src/url';

/** @const */
const TAG = 'AmpAccessLogin';

/** @const {!Function} */
const assert = AMP.assert;


/**
* Opens the login dialog for the specified URL. If the login dialog succeeds,
* the returned promised is resolved with the dialog's response. Otherwise, the
* returned promise is rejected.
* @param {!Window} win
* @param {string|!Promise<string>} urlOrPromise
* @return {!Promise<string>}
*/
export function openLoginDialog(win, urlOrPromise) {
return new LoginDialog(win, urlOrPromise).open();
}


class LoginDialog {
/**
* @param {!Window} win
* @param {string|!Promise<string>} urlOrPromise
*/
constructor(win, urlOrPromise) {
/** @const {!Window} */
this.win = win;

/** @const {string} */
this.urlOrPromise = urlOrPromise;

/** @private {?function(string)} */
this.resolve_ = null;

/** @private {?function(*)} */
this.reject_ = null;

/** @private {?Window} */
this.dialog_ = null;

/** @private {?number} */
this.heartbeatInterval_ = null;

/** @private {?Unlisten} */
this.messageUnlisten_ = null;
}

/**
* Opens the dialog. Returns the promise that will yield with the dialog's
* result or will be rejected if dialog fails.
* @return {!Promise<string>}
*/
open() {
assert(!this.resolve_, 'Dialog already opened');
return new Promise((resolve, reject) => {
this.resolve_ = resolve;
this.reject_ = reject;
// Must always be called synchronously.
this.openInternal_();
}).then(result => {
this.cleanup_();
return result;
}, error => {
this.cleanup_();
throw error;
});
}

/** @private */
cleanup_() {
this.resolve_ = null;
this.reject_ = null;

if (this.dialog_) {
try {
this.dialog_.close();
} catch (e) {
// Ignore.
}
this.dialog_ = null;
}

if (this.heartbeatInterval_) {
this.win.clearInterval(this.heartbeatInterval_);
this.heartbeatInterval_ = null;
}

if (this.messageUnlisten_) {
this.messageUnlisten_();
this.messageUnlisten_ = null;
}
}

/** @private */
openInternal_() {
const screen = this.win.screen;
const w = Math.floor(Math.min(700, screen.width * 0.9));
const h = Math.floor(Math.min(450, screen.height * 0.9));
const x = Math.floor((screen.width - w) / 2);
const y = Math.floor((screen.height - h) / 2);
const options = `height=${h},width=${w},left=${x},top=${y}`;
const returnUrl = this.getReturnUrl_();

let dialogReadyPromise = null;
if (typeof this.urlOrPromise == 'string') {
const loginUrl = this.buildLoginUrl_(this.urlOrPromise, returnUrl);
log.fine(TAG, 'Open dialog: ', loginUrl, returnUrl, w, h, x, y);
this.dialog_ = this.win.open(loginUrl, '_blank', options);
if (this.dialog_) {
dialogReadyPromise = Promise.resolve();
}
} else {
log.fine(TAG, 'Open dialog: ', 'about:blank', returnUrl, w, h, x, y);
this.dialog_ = this.win.open('', '_blank', options);
if (this.dialog_) {
dialogReadyPromise = this.urlOrPromise.then(url => {
const loginUrl = this.buildLoginUrl_(url, returnUrl);
log.fine(TAG, 'Set dialog url: ', loginUrl);
this.dialog_.location.replace(loginUrl);
}, error => {
throw new Error('failed to resolve url: ' + error);
});
}
}

if (dialogReadyPromise) {
dialogReadyPromise.then(() => {
this.setupDialog_(returnUrl);
}, error => {
this.loginDone_(/* result */ null, error);
});
} else {
this.loginDone_(/* result */ null, new Error('failed to open dialog'));
}
}

/**
* @param {string} returnUrl
* @private
*/
setupDialog_(returnUrl) {
const returnOrigin = parseUrl(returnUrl).origin;

this.heartbeatInterval_ = this.win.setInterval(() => {
if (this.dialog_.closed) {
this.win.clearInterval(this.heartbeatInterval_);
this.heartbeatInterval_ = null;
// Give a chance for the result to arrive, but otherwise consider the
// responce to be empty.
this.win.setTimeout(() => {
this.loginDone_('');
}, 3000);
}
}, 500);

this.messageUnlisten_ = listen(this.win, 'message', e => {
log.fine(TAG, 'MESSAGE:', e);
if (e.origin != returnOrigin) {
return;
}
if (!e.data || e.data.sentinel != 'amp') {
return;
}
log.fine(TAG, 'Received message from dialog: ', e.data);
if (e.data.type == 'result') {
this.loginDone_(e.data.result);
}
});
}

/**
* @param {?string} result
* @param {*=} opt_error
* @private
*/
loginDone_(result, opt_error) {
if (!this.resolve_) {
return;
}
log.fine(TAG, 'Login done: ', result, opt_error);
if (opt_error) {
this.reject_(opt_error);
} else {
this.resolve_(result);
}
this.cleanup_();
}

/**
* @param {string} url
* @param {string} returnUrl
* @return {string}
* @private
*/
buildLoginUrl_(url, returnUrl) {
return url +
(url.indexOf('?') == -1 ? '?' : '&') +
'return=' + encodeURIComponent(returnUrl);
}

/**
* @return {string}
* @private
*/
getReturnUrl_() {
if (getMode().localDev) {
const loc = this.win.location;
return loc.protocol + '//' + loc.host + '/dist/v0/amp-login-done.html';
}
return 'https://cdn.ampproject.org/v0/amp-login-done.html';
}
}
Loading

0 comments on commit 6055ccc

Please sign in to comment.