AngularJs - doesn't skip request when waiting for new token - javascript

I have implemented authentication system and after upgrading from angular 1.0.8 to 1.2.x,
system doesn't work as it used to. When user logs in it gets a token. When token is expired,
a refresh function for new token is called. New token is successfully created on a server and it is
stored to database. But client doesn't get this new token, so it requests a new token again,
and again and again until it logs out. Server side (MVC Web Api) is working fine, so problem must
be on client side. The problem must be on a retry queue. Below I pasted relevant code and
a console trace for both versions of applications (1.0.8 and 1.2.x).
I am struggling with this for days now and I can't figure it out.
In the link below, there are 5 relevant code blocks:
interceptor.js (for intercepting requests, both versions)
retryQueue.js (manages queue of retry requests)
security.js (manages handler for retry queue item and gets a new token from api)
httpHeaders.js (sets headers)
tokenHandler.js (handles tokens in a cookies)
Code: http://pastebin.com/Jy2mzLgj
Console traces for app in angular 1.0.8: http://pastebin.com/aL0VkwdN
and angular 1.2.x: http://pastebin.com/WFEuC6WB
interceptor.js (angular 1.2.x version)
angular.module('security.interceptor', ['security.retryQueue'])
.factory('securityInterceptor', ['$injector', 'securityRetryQueue', '$q',
function ($injector, queue, $q) {
return {
response: function(originalResponse) {
return originalResponse;
},
responseError: function (originalResponse) {
var exception;
if (originalResponse.headers){
exception = originalResponse.headers('x-eva-api-exception');
}
if (originalResponse.status === 401 &&
(exception === 'token_not_found' ||
exception === 'token_expired')){
queue.pushRetryFn(exception, function retryRequest() {
return $injector.get('$http')(originalResponse.config);
});
}
return $q.reject(originalResponse);
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('securityInterceptor');
}]);
retryQueue.js
angular.module('security.retryQueue', [])
.factory('securityRetryQueue', ['$q', '$log', function($q, $log) {
var retryQueue = [];
var service = {
onItemAddedCallbacks: [],
hasMore: function(){
return retryQueue.length > 0;
},
push: function(retryItem){
retryQueue.push(retryItem);
angular.forEach(service.onItemAddedCallbacks, function(cb) {
try {
cb(retryItem);
}
catch(e){
$log.error('callback threw an error' + e);
}
});
},
pushRetryFn: function(reason, retryFn){
if ( arguments.length === 1) {
retryFn = reason;
reason = undefined;
}
var deferred = $q.defer();
var retryItem = {
reason: reason,
retry: function() {
$q.when(retryFn()).then(function(value) {
deferred.resolve(value);
}, function(value){
deferred.reject(value);
});
},
cancel: function() {
deferred.reject();
}
};
service.push(retryItem);
return deferred.promise;
},
retryAll: function() {
while(service.hasMore()) {
retryQueue.shift().retry();
}
}
};
return service;
}]);
security.js
angular.module('security.service', [
'session.service',
'security.signin',
'security.retryQueue',
'security.tokens',
'ngCookies'
])
.factory('security', ['$location', 'securityRetryQueue', '$q', /* etc. */ function(){
var skipRequests = false;
queue.onItemAddedCallbacks.push(function(retryItem) {
if (queue.hasMore()) {
if(skipRequests) {return;}
skipRequests = true;
if(retryItem.reason === 'token_expired') {
service.refreshToken().then(function(result) {
if(result) { queue.retryAll(); }
else {service.signout(); }
skipRequests = false;
});
} else {
skipRequests = false;
service.signout();
}
}
});
var service = {
showSignin: function() {
queue.cancelAll();
redirect('/signin');
},
signout: function() {
if(service.isAuthenticated()){
service.currentUser = null;
TokenHandler.clear();
$cookieStore.remove('current-user');
service.showSignin();
}
},
refreshToken: function() {
var d = $q.defer();
var token = TokenHandler.getRefreshToken();
if(!token) { d.resolve(false); }
var session = new Session({ refreshToken: token });
session.tokenRefresh(function(result){
if(result) {
d.resolve(true);
TokenHandler.set(result);
} else {
d.resolve(false);
}
});
return d.promise;
}
};
return service;
}]);
session.service.js
angular.module('session.service', ['ngResource'])
.factory('Session', ['$resource', '$rootScope', function($resource, $rootScope) {
var Session = $resource('../api/tokens', {}, {
create: {method: 'POST'}
});
Session.prototype.passwordSignIn = function(ob) {
return Session.create(angular.extend({
grantType: 'password',
clientId: $rootScope.clientId
}, this), ob);
};
Session.prototype.tokenRefresh = function(ob) {
return Session.create(angular.extend({
grantType: 'refresh_token',
clientId: $rootScope.clientId
}, this), ob);
};
return Session;
}]);
Thanks to #Zerot for suggestions and code samples, I had to change part of the interceptor like this:
if (originalResponse.status === 401 &&
(exception === 'token_not_found' || exception === 'token_expired')){
var defer = $q.defer();
queue.pushRetryFn(exception, function retryRequest() {
var activeToken = $cookieStore.get('authorization-token').accessToken;
var config = originalResponse.config;
config.headers.Authorization = 'Bearer ' + activeToken;
return $injector.get('$http')(config)
.then(function(res) {
defer.resolve(res);
}, function(err)
{
defer.reject(err);
});
});
return defer.promise;
}
Many thanks,
Jani

Have you tried to fix the error you have in the 1.2 log?
Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: project in client.projects, Duplicate key: string:e
That error is at the exact point where you would need to see the $httpHeaders set line. It looks like your session.tokenrefresh is not working(and that code is also missing from the pastebin so I can't check.)

Interceptors should always return a promise.
So in responseError, you should better return $q.reject(originalResponse); instead of just return originalResponse.
Hope this helps

I think that your interceptor returns wrong result in errorResponse method.
I faced some "undebuggable" issue for the same reason.
With this code you could fall in some infinite loop flow...
Hope this helps.

Related

Managing multiple SignalR connections in a single page

I'm experiencing intermittent signalr connection problems
sometimes it fails, sometimes it doesn't...
Here is the setup...
I have a list of orders, each order has a unique signalr connection. Currently there are 230 orders on a single page. The purpose of having a signalr connection is so users can see any real time updates on each order (who is viewing, editing, etc). I've decided to have a separate connection for each order so that I don't have to manage the order that is currently being viewed, edited, etc. So far, for the orders that have successfully connected, the updates are correct and smooth.
Here is my list with a sample of another user viewing an order (a photo of that user is being shown)
Here is my code that connects to the signalr hubs
crimeassure.factory('hubProxy', ['$rootScope', function ($rootScope) {
function hubProxyFactory(hubName) {
var _hubConnection = $.hubConnection();
_hubConnection.logging = true;
var _hubProxy = _hubConnection.createHubProxy(hubName);
return {
on: function (eventName, callback, failCallback) {
_hubProxy.on(eventName, function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
})
},
invoke: function (methodName, data) {
_hubProxy.invoke(methodName, data)
.done(function (result) {
//$rootScope.$apply(function () {
// if (callback) {
// callback(result);
// }
//});
});
},
start: function (successCallback, failCallback) {
_hubConnection.start({ transport: 'webSockets' }).done(successCallback).fail(failCallback);
},
hubConnection: _hubConnection,
};
};
return hubProxyFactory;
}]);
crimeassure.directive('componentLiveUpdates', function () {
return {
restrict: 'E',
scope: {
componentId: '=',
},
templateUrl: '/scripts/templates/directive-templates/component-live-updates.html',
controllerAs: 'vm',
bindToController: true,
controller: ["$scope", "$rootScope", "appData", "hubProxy",
function componentLiveUpdates($scope, $rootScope, appData, hubProxy) {
var vm = (this);
var user = appData.getCurrentUser();
vm.componentActivity = [];
var reQueueHub = hubProxy('researcherExpressQueueHub');
var componentActivityChanged = function (component) {
if (component.ActivityValue === 'ComponentModalClose') {
var idx = vm.componentActivity.indexOf(component);
vm.componentActivity.splice(idx, 1);
}
if (component.ActivityValue === 'ComponentModalOpen') {
vm.componentActivity.push(component);
}
}
var successCallback = function () {
console.log('connected to signalR, connection ID =' + reQueueHub.hubConnection.id + '--' + vm.componentId);
reQueueHub.invoke('joinGroup', vm.componentId);
$rootScope.reQueueHubs.push({
ComponentId: vm.componentId,
Hub: reQueueHub
});
}
var failCallback = function (e) {
console.log('Error connecting to signalR = ' + vm.componentId);
console.log(e);
//startHubConnection();
}
var startHubConnection = function () {
reQueueHub.start(successCallback, failCallback);
}
var initialize = function () {
reQueueHub.on('updateComponentActivity', componentActivityChanged);
startHubConnection();
}
initialize();
}],
}
});
and here is my hub class
public class ResearcherExpressQueueHub : Hub
{
public void UpdateComponentActivity(ComponentItem item)
{
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public void ComponentModalOpen(ComponentItem item)
{
item.Activity = ComponentActivity.ComponentModalOpen;
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public void ComponentModalClose(ComponentItem item)
{
item.Activity = ComponentActivity.ComponentModalClose;
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public Task JoinGroup(string componentId)
{
return Groups.Add(Context.ConnectionId, componentId);
}
public Task LeaveGroup(string componentId)
{
return Groups.Remove(Context.ConnectionId, componentId);
}
}
so my questions are,
Why am i experiencing a disconnect "WebSocket is closed before the connection is established"
Is my approach the best way to approach this type of requirement?
Use grouping mechanisme of signalr and NOT create multiple connections for your usecase!
There are limitations from IIS and also from browsers. Some browser have a limit of 4 or 5 paralell connections. You can test it by yourself by opening multiple different browsers.
Details about grouping:
Working with groups in signalr is really simple. Details you will find here: https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/working-with-groups

promise resolution with a mocked service within the tested service

My question: In Karma, I am mocking an injected service while testing the actual service. The mocked service gets some data, and sends back a promise. I can't figure out what I am doing wrong.
I know there are a number of issues with the actual "Login" mechanism, but I am using it to illustrate this Karma question. I do not plan to use it as production code. (However, any suggestions for a better illustration of the problem are welcome!)
First, I wrote this in a generic .js file, and said "node testcode.js"
testcode.js:
function Login(name,password,callback){
setTimeout(function(){
var response;
var promise = getByUserName();
promise.then(successCb);
function successCb(userObj){
if ( userObj != null && userObj.password === password ) {
response = { success : true };
} else {
response = { success: false, message: 'Username or password is incorrect' };
}
callback(response);
};
},200);
}
function getByUserName(){
return Promise.resolve(user);
}
var user = {
username : 'test',
id : 'testId',
password : 'test'
};
var test = undefined;
Login(user.username,user.password,testCb);
function testCb(response){
test = response;
console.log("Final: " + JSON.stringify(test));
}
This gives me my expected result:
Final: {"success":true}
Now, I try to repeat this in Karma...
TestService:
(function(){
"use strict";
angular.module('TestModule').service('TestService',TestService);
TestService.$inject = ['$http','$cookieStore','$rootScope','$timeout','RealService'];
})();
function TestService($http,$cookieStore,$rootScope,$timeout,RealService){
var service = {};
service.Login = Login;
/* more stuff */
return service;
function Login(username,password,callback){
$timeout(function () {
var response;
var promise = UserService.GetByUsername(username);
promise.then(successCb);
function successCb(user){
if (user !== null && user.password === password) {
response = { success: true };
} else {
response = { success: false, message: 'Username or password is incorrect' };
}
callback(response);
};
}, 1000);
}
}
My Karma-Jasmine Test:
describe('TestModule',function(){
beforeEach(module('TestModule'));
describe('TestService',function(){
var service,$rootScope,
user = {
username : 'test',
id : 'testId',
password : 'test'
};
function MockService() {
return {
GetByUsername : function() {
return Promise.resolve(user);
}
};
};
beforeEach(function(){
module(function($provide){
$provide.service('RealService',MockService);
});
inject(['$rootScope','TestService',
function($rs,ts){
$rootScope = $rs;
service = ts;
}]);
});
/**
* ERROR: To the best of my knowledge, this should not pass
*/
it('should Login',function(){
expect(service).toBeDefined();
var answer = {"success":true};
service.Login(user.username,user.password,testCb);
//$rootScope.$apply(); <-- DID NOTHING -->
//$rootScope.$digest(); <-- DID NOTHING -->
function testCb(response){
console.log("I'm never called");
expect(response.success).toBe(false);
expect(true).toBe(false);
};
});
});
});
Why is the promise not being resolved? I have tried messing with $rootScope.$digest() based on similar questions I have read on SO, but nothing seems to get testCb to be called.
Somebody let me know if I can give 'estus' the credit for this answer.
The magic is in the $timeout angular mock service:
https://docs.angularjs.org/api/ngMock/service/$timeout
and the $q service:
https://docs.angularjs.org/api/ng/service/$q
I updated two things. First, my MockUserService is using $q.defer instead of native promise. As 'estus' stated, $q promises are synchronous, which matters in a karma-jasmine test.
function MockUserService(){
return {
GetByUsername : function(unusedVariable){
//The infamous Deferred antipattern:
//var defer = $q.defer();
//defer.resolve(user);
//return defer.promise;
return $q.resolve(user);
}
};
};
The next update I made is with the $timeout service:
it('should Login',function(){
expect(service).toBeDefined();
service.Login(user.username,user.password,testCb);
$timeout.flush(); <-- NEW, forces the $timeout in TestService to execute -->
$timeout.verifyNoPendingTasks(); <-- NEW -->
function testCb(response) {
expect(response.success).toBe(true);
};
});
Finally, because I'm using $q and $timeout in my test, I had to update my inject method in my beforeEach:
inject([
'$q','$rootScope','$timeout','TestService',
function(_$q_,$rs,$to,ts) {
$q = _$q_;
$rootScope = $rs;
$timeout = $to;
service = ts;
}
]);

Angular js redirecting from log in page to Dashboard along with json data

I am trying to build a simple app using Angular JS. Here I have two html files (Login.html & Dashboard.html). when I run the Login.html it works well And on the successful log in I need to show the user dashboard with the json data populated(from server) at Login time.
here is my code: (main.js)
var app = angular.module('NovaSchedular', []);
app.factory('MyService', function()
{
var savedData = {}
function set(data)
{
savedData = data;
}
function get()
{
return savedData;
}
return {
set: set,
get: get
}
});
function LoginController($scope,$http,MyService,$location)
{
$scope.login = function(str) {
console.log(".......... login called......");
var validEmail=validateEmail(email.value);
if(validEmail && password.value != ""){
$http.post('./nova/TaskManager/public/user/login?email='+email.value+'&password='+password.value).success(function(data, status)
{
console.log(data);
var result=data.response;
console.log(result);
if (result=="success")
{
$scope.userId=data.user_id;
$scope.email=data.email;
$scope.Name=data.name;
$scope.password=data.password;
$scope.Type=data.type;
console.log("........"+$scope.userId);
console.log("........"+$scope.email);
console.log("........"+$scope.Name);
console.log("........"+$scope.password);
console.log("........"+$scope.Type);
MyService.set(data);
console.log(data);
alert(data.message);
window.location.href='./Dashboard.html';
//$location.path('./Dashboard.html', data);
}
else
alert(data.message);
});
}
}
}
function DashboardController($scope,$http,MyService)
{
$scope.userInfo = MyService.get();
}
here after LOGIN successfully, I am getting the server response (json data) under the LoginController well. Now, further I need these data to be available on the Dashboard page so that dashboard would populate with the respective user data.
I am trying this by using:
window.location.href='./Dashboard.html';
//$location.path('./Dashboard.html', data);
but it didn't work for me. It's redirecting to the Dashboard.html well, but doesn't containing the data what I need to pass from the LoginController to the DashboardController. so, that it would available for the Dashboard.html page.
while seeing at the console for the Dashboard.html, it's empty json is showing there.
Don't know what's missing. why it's not passing the data.
Any suggestion would be appreciated.
try this controller code :
app.controller('LoginController' function($scope,$http,MyService,$location)
{
$scope.login = function(credential) {
console.log(".......... login called......");
var validEmail=validateEmail(credential.email);
if(validEmail && credential.password!= ""){
MyService.login(credential);
}
}
});
and this:
app.controller('DashboardController',function($scope,$http,MyService)
{
$scope.userInfo = MyService.getDashboardData();
});
MyService.js:
var app = angular.module('NovaSchedular', []);
app.factory('MyService', function()
{
var dashboardData;
var obj={
login:function(credential){
$http.post('./nova/TaskManager/public/user/login?email='+credential.email+'&password='+credential.password).success(function(data, status, headers) {
dashboardData=data;
window.location.href='./Dashboard.html';
})
.error(function(data, status, headers, config) {
console.error('error in loading');
});
},
getDashboardData:function(){
return dashboardData;
}
}
return {
login:obj.login,
getDashboardData:obj.getDashboardData
}
});

Error: No pending request to flush - when unit testing AngularJs service

I'm newbie to AngularJs, and I'm in the process of writing my first unit test; to test the service I wrote a test that simply returns a single Json object. However, everytime I run the test I get the error stated in the title. I don't know what exactly is causing this! I tried reading on $apply and $digest and not sure if that's needed in my case, and if yes how; a simple plunker demo would be appreciated.
here is my code
service
var allBookss = [];
var filteredBooks = [];
/*Here we define our book model and REST api.*/
var Report = $resource('api/books/:id', {
id: '#id'
}, {
query: {
method: 'GET',
isArray: false
}
});
/*Retrive the requested book from the internal book list.*/
var getBook = function(bookId) {
var deferred = $q.defer();
if (bookId === undefined) {
deferred.reject('Error');
} else {
var books= $filter('filter')(allBooks, function(book) {
return (book.id == bookId);
});
if (books.length > 0) {
deferred.resolve(books[0]);//returns a single book object
} else {
deferred.reject('Error');
};
};
return deferred.promise;
};
test
describe('unit:bookService', function(){
beforeEach(module('myApp'));
var service, $httpBackend;
beforeEach(inject(function (_bookService_, _$httpBackend_) {
service = _bookService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/api/books/1").respond(200, {
"book": {
"id": "1",
"author": "James Spencer",
"edition": "2",
.....
}
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should return metadata for single report', function() {
service.getBook('1').then(function(response) {
expect(response.length).toEqual(1);
});
$httpBackend.flush();// error is in this line
});
});
error
Error: No pending request to flush !
at c:/myapp/bower_components/angular-mocks/angular-mocks.js:1439
at c:/myapptest/tests/bookTest.js:34
libs version
AngularJS v1.2.21
AngularJS-mock v1.2.21
I don't see where you're actually issuing a Report.query(). The getBook function just returns an unresolved promise that will never be resolved because nothing in the function is async.
You need to call Report.query via the book function with the promise resolved in the .then() (in the book function). After that, flush the http backend in the service.getBook().then() and do the expect.

Instagram OAuth login

I would like to build an app for Instagram login. The problem is that i don't know how to initialize connection with instagram client id. I had done this with OAuth.initialize(), but it doesn't work. I am recieving 'OAuthException'.
My code so far:
return {
initialize: function() {
//initialize OAuth.io with public key of the application
OAuth.initialize('e6u0TKccWPGCnAqheXQYg76Vf2M', {cache:true});
authorizationResult = OAuth.create('instagram');
console.log(authorizationResult);
},
isReady: function() {
return (authorizationResult);
},
connectInstagram: function() {
var deferred = $q.defer();
OAuth.popup('instagram', {cache:true}, function(error, result) {
if (!error) {
authorizationResult = result;
deferred.resolve();
console.log('case');
} else {
console.log('case2');
}
});
return deferred.promise;
},
clearCache: function() {
OAuth.clearCache('instagram');
authorizationResult = false;
}
For starters, the only acceptable argument for OAuth.initialize is "public key".
Remove the second parameter.

Categories

Resources