AngularJS html5Mode fallback in IE. Express server needs to know route - angularjs

I'm using html5Mode=true with AngularJS routing. Works fine. When I access the site with IE, Angular routing falls back to Hashbang URI's like http://example.com/#!/route-name. That's all fine. Except in Express I need to know the route, because it tells me which html file to serve from Express. The # part of the url is not sent to the server.
My question is, how can I let my server know which AngularJS route is being requested with hashbang routing in IE? I was thinking of configuring Angular somehow to send the route as an http header that I can read in express, but I don't know how to do that. Any ideas?
Update: Based on feedback I got, let me tell you that the site has templates. One for the homepage and one for all the other pages. They are both pretty different. Based on the route, the server needs to know when to serve the file for the homepage and when to serve the file for the other pages.

Just to be clear: this won't send the hashbang on the main page request. Based on the HTTP specifications there is no way of doing that.
You can send it on subsequent AJAX requests, but this won't really help you with solving this problem. There is no solution other than not supporting browsers that don't support html5mode.
Browsers won't send the hashbang to the server automatically, so yes you will need to send it manually. Using a header is a good option.
Take a look at $httpProvider.interceptors in the docs.
You can register a request interceptor for $http (AngularJS uses it internally for any AJAX request and so should you) that attaches a custom header. You can then set that header value to be the actual hashbang using $location.path():
var app = angular.module('MyApp',[]);
app.factory('httpInterceptor', function($q, $location) {
return {
'request': function(config) {
if(config.url.indexOf('product')) {
config.headers.Hashbang = $location.path();
}
return config;
}
};
});
app.config(function($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
});
app.controller('Ctrl', function($scope, $http) {
$http({method: 'GET', url: '/echo/json/'}).
success(function(data, status, headers, config) {
console.log(data);
});
});
Here's the fiddle. You can test it like so. Check out the sent headers in developer tools.
Notice that I'm not testing with a true hashbang (#!) but just a hash because the fiddle does not allow it.

I had a similar problem where I needed the parameters of the URL before they were removed by the redirect. As the URL is redirected in angular from the original one the previous URL is in the referer header.
So I used this express 3.x middleware to rewrite the original parameters from the referer to the query on requests to the root URL:
var _ = require('lodash');
var url = require('url');
var qs = require('querystring');
app.use(function(req, res, next) {
if (req.url === '/' && req.headers && req.headers.referer) {
var referer = url.parse(req.headers.referer);
// rewrite original parameters
_.forEach(qs.parse(referer.query), function(value, key) {
req.query[key] = value;
});
// do something with pathname
console.log(referer.pathname);
}
});
You could do the same in your case for the path, that is in referer.pathname.

There seems no client-side answer as per the answer of Sergiu Paraschiv. So I have investigated a server-side solution that requires only one thing on the client. Make sure you always links as you would do in html5Mode which is linking to /xxxxx and not /#!/xxxxx.
Then In html5Mode and IE9 and lower what happens is that AngularJS redirects everything /xxxxx to /#!/xxxxx. In Express this makes the url / and the referer /xxxxx. This is something that you can check for quite easily.
If you want to cater for IE8 and lower to, it's unfortunate that Angular uses window.location.href in this fall back scenario redirecting to /#!/xxxxx. See github / angular.js / src / ng / browser.js line 169.
Using window.location.href causes IE8 and lower to not send the referer.
Here's a server-side (Express 3.x) solution that resolves the Angular hashbang route from the referer value or a previousUrl session variable for IE8 and lower.
app.use(function(req, res, next) {
if (req.url.match(/\/api/))
return next(); // /api/something should proceed to next in middleware
console.log(req.url, req.headers)
//IE does not support html5Mode so /xxxxx is redirected to /#!/xxxxx
//effectively making the url / and the referer /xxxxx
//I check for that here and if xxxxx is not home I present page.html
if (req.headers['user-agent'] &&
req.headers['user-agent'].match(/MSIE ([7-8]{1}[.0-9]*)/) &&
req.url.match(/^\/[\w-]+$/)
) {
//However IE8 does not report the referer when doing a redirect using window.locarion.href
//so I store the initially requested url /xxxxx in session...
req.session.previousUrl = req.url;
//console.log('IE8 saving the url here', req.session.previousUrl);
}
if (req.headers['user-agent'] &&
req.headers['user-agent'].match(/MSIE ([7-8]{1}[.0-9]*)/) &&
!req.headers['referer'] &&
req.url === '/' &&
req.session.previousUrl &&
req.session.previousUrl !== '/home'
) {
//continuation on the IE* referer story (aka the next redirected request)...
//... and checked for the url stored in session here ;)
return res.sendfile('public\\page.html');
}
if (req.headers['user-agent'] &&
req.headers['user-agent'].match(/MSIE ([7-9]{1}[.0-9]*)/) &&
req.url === '/' &&
req.headers['referer'] &&
req.headers['referer'].match(/^https?:\/\/(?:www)?\.example\.com\/(?!home$)([\w-]+)$/)
) {
return res.sendfile('public\\page.html');
}
if (req.url.match(/\/home|\/$/))
return res.sendfile('public\\home.html'); //for the home pages we're going to use home.html
res.sendfile('public\\page.html'); //for everything else we're going to use page.html
});
I'm sure that there are scenario's where this fails, but it worked for me in my tests and if it fails, that will only fail for IE9- browsers (as per the regexp's).

Related

File serving using ngRoute in Angular with NodeJS server

I am trying to run AngularJS, using Angular Router, with a NodeJS server. I do not plan on serving the various views in Node, but instead I want to use the angular router. This first page is served correctly with no errors but when I try to click on another link, the browser displays the following
error code. Below is the relevant code from the server script, the routing script, and where the link in the HTML is.
HTML Link
<li>Add Workout</li>
Server.js
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname + '/public/app/views/home.html'));
});
app.use(express.static('public'));
App.js
var app = angular.module("fitness2Uapp", ["ngRoute"]);
app.config(function($routeProvider) {
$routeProvider
.when("/", {
templateUrl : "./app/views/home.html"
})
.when("/browse", {
templateUrl : "./app/views/browse.html"
})
.when("/add", {
templateUrl : "./add.html"
})
.when("/workout", {
templateUrl : "./app/views/workout.html"
});
});
Try putting in a "catch all" route that just redirects back to the main page. This will in turn allow the angular routing mechanism to kick in. Right now the problem is that it's trying to find an endpoint path of '/add' on the node server, but nothing is found. This should be the very last route established on your server.
I personally use AngularJS and had to do this, and everything works great. Not sure if this will also perform the same way as Angular2+
// Catch all if all other routes fail to match.
app.get('*', (req, res) => {
res.sendFile(path.resolve(`${__dirname}/path/to/home.html`));
});

How fix or disable proxy in phonegap?

When my app do POST request its work. look like
http://192.168.0.165:3000/proxy/http%3A%2F%2Fhomestead.app%2Fapi%2Fv1%2Fuser%2F
But when I do next request with GET that don't work.
I think it because phonegap lost what I send in my params. How I can fix it or disable proxy in phonegap?
Add this in your index.html file
(function() {
var xhr = {};
xhr.open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
console.log(url);
if(url.indexOf('/proxy/') == 0){
url = window.decodeURIComponent(url.substr(7));
}
xhr.open.apply(this, arguments);
};
})(window);
If you begin to face the "Access-Control-Allow-Origin" problem use this extension for chrome (modheader) and add a response like this:
Access-Control-Allow-Origin:http://yourip:3000
If you are making multiple requests and wish the session be shared between them then add another response like this:
Access-Control-Allow-Credentials: true

How to force login in angular full stack yeoman?

Is there a way to force the user to log in first in an app generated by the angular full stack yeoman ?
I tried to add the following code in the run part of app.js but was not successful
Auth.isLoggedIn(function(loggedIn) {
console.log(loggedIn);
if (!loggedIn) {
console.log("redirecting");
// event.preventDefault();
$state.go('login');
}
});
I found authentication controls in api index.js files but none for the / landing page ...
Thx
I did not use google as i should have !
To force authentication for a state, just add
authenticate: true
in the state (or all states in my case)
Without more code or information on which router you are using (generator-angular-fullstack supports both the default NgRouter and UIRouter) it is tough to give a complete answer. By your answer to your question I am assuming you have UI Router and have figured out how to do client side authentication within the generated fullstack code. However, you will also need to implement something similar to what they have done in their 'users' api to protect your api end points on the server side and return a 401/403 error.
'use strict';
var express = require('express');
var controller = require('./user.controller');
var config = require('../../config/environment');
var auth = require('../../auth/auth.service');
var router = express.Router();
router.get('/', auth.hasRole('admin'), controller.index);
router.delete('/:id', auth.hasRole('admin'), controller.destroy);
router.get('/me', auth.isAuthenticated(), controller.me);
router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);
router.get('/:id', auth.isAuthenticated(), controller.show);
router.post('/', controller.create);
module.exports = router;
In the above code (which can be found by navigating to the server folder, then the api folder, then the user folder and looking at index.js) you will see that they are calling a couple of functions.
They are calling auth.hasRole('admin') and auth.isAuthenticated().
Those are functions which can be found in the server side auth/role service under the folder auth and in the auth.service.js file.
function hasRole(roleRequired) {
if (!roleRequired) throw new Error('Required role needs to be set');
return compose()
.use(isAuthenticated())
.use(function meetsRequirements(req, res, next) {
if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
next();
}
else {
res.status(403).send('Forbidden');
}
});
}
I think it is important to understand how this is working on the server side also. So, if you navigate to localhost:9000/admin and open console you will see that there is a 401 or 403 error depending on whether or not you are logged in and/or logged in as an admin user.
Just Paste authenticate:true on main.js
angular.module('testcesarApp')
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/main/main.html',
controller: 'MainCtrl',
authenticate:true
});
});

NavBar address loading angular template but not root shell

I am using Node.JS with Express, Angular.JS and the node module connect-roles for ACL. I want to allow a user with user.status of "Platinum" to access "Platinum" but not "Gold" and vice versa.
I have the ACL part working, if I enter /Platinum into the navigation bar I can't access /Gold, but when I try to access /Platinum I only get the template but not the root shell, so what comes up is this:
You made it!
You have the {{status}} status!
If I click on a link in angular to /Platinum, everything works as it should. If I enter any neutral address in the navigation bar, everything works as it should.
This should be an easy fix, but I've not figured it out.
Here is the code that sets up authorizations, I'm pretty sure everything here is okay.
ConnectRoles = require('connect-roles')
var user = new ConnectRoles({
failureHandler: function(req, res, action){
var accept = req.headers.accept || '';
res.status(403);
if(accept.indexOf('html')) {
res.render('access-denied', {action: action});
} else {
res.send('Access Denied - You don\'t have permission to: ' + action);
}
}
});
var app = express();
app.use(user.middleware());
// Setting up user authorizations,
// i.e. if req.user.status = "Platinum", they are given Platinum status
user.use('Platinum', function(req) {
if (req.user.status == 'Platinum') {
return true;
}
});
user.use('Gold', function(req) {
if (req.user.status == 'Gold') {
return true;
}
});
user.use('Admin', function(req) {
if (req.user.status == 'Admin') {
return true;
}
});
That sets up authorizations, now the problem lies below with the routing.
app.post('/login', passport.authenticate('local',
{ successRedirect: '/', failureRedirect: '/login' }));
app.get('/Platinum', user.is('Platinum'), function(req, res) {
//Obviously the code below is wrong.
res.render('templates/support/Platinum');
});
app.get('/Gold', user.is('Gold'), function(req, res) {
res.render('templates/support/Gold');
});
The way you are configuring your routes on server side (using express) is not correct. For a single page app like AngularJS, you need to do all of the routing for pages on the client (i.e. in Angular). The server still defines routes for API requests (e.g. getting and posting data) and static resources (index.html, partial HTML files, images, javascript, fonts, etc), though.
Thus the following code is wrong in your server side JS:
app.get('/Platinum', user.is('Platinum'), function(req, res) {
//Obviously the code below is wrong.
res.render('templates/support/Platinum');
});
app.get('/Gold', user.is('Gold'), function(req, res) {
res.render('templates/support/Gold');
});
Just remove those lines.
Instead, you need to define the routes that the server will handle, such as your /login post one first, and how to get static files (I suggest prefixing them all with /pub in the URL). Then you need to do something like the technique in this answer to return your index.html page if no routes are matched.
That way, when a user types http://localhost:port/Gold, express will see there is no route defined for /Gold, so it will return index.html, which will load AngularJS, run your Angular app, which will then look at the URL and see if that matches any of the routes your AngularJS app has configured, and if so, fetch the partial for that page and insert it into your ng-view (if using the core router).

Restricting access to all routes but one

I'm starting to experiment with ExpressJS + AngularJS and I've run into a stressful situation.
My goal is to have one login page and one dashboard page, and using Passport + MongoDB I'll authenticate a user in and, if credentials are correct, redirect him to the dashboard page.
I've started with the angular-express-seed and modified it so I ended up with the previously mentioned views:
$routeProvider.
when('/login', {
templateUrl: 'partials/login',
controller: 'LoginCtrl'
}).
when('/dashboard', {
templateUrl: 'partials/dashboard',
controller: 'DashboardCtrl'
}).
otherwise({
redirectTo: '/login'
});
I'm serving each partial view with:
app.get('/partials/:name', routes.partials);
exports.partials = function (req, res) {
var name = req.params.name;
res.render('partials/' + name);
};
The login system is working correctly except for the part that I can access the dashboard directly (without loging in at all).
So I guess I need to figure out if the user if authenticated, before granting him access to the dashboard (or any other area for that matter).
I found this that does the trick:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) { return next(); }
res.redirect('/login');
}
and I would call it on every GET to a partial
app.get('/partials/:name', ensureAuthenticated, routes.partials);
The big problem here is that the login view is also a partial, and I don't want ensureAuthenticated to run when that's the view being requested.
Currently calling ensureAuthenticated on every partial request crashes any browser, no errors whatsoever.
What I've tried so far (and did not work):
function ensureAuthenticated(req, res, next) {
if ( req.route.params.name !== 'login' && req.isAuthenticated() ) { return next(); }
res.redirect('/login');
}
Also tried calling the login view with its own app.GET and not using ensureAuthenticated but somehow Angular flips off and does not even load.
Any ideas on how to resolve this ?
Thanks in advance.
It's unclear to me if the first (unmodified) version of ensureAuthenticated crashes your browsers, or the last version (in which you check for /partials/login), although no matter what, it shouldn't happen (if by 'crashing' you mean 'stalls', that's a tell-tale sign of Express not sending back a response)
But as an alternative, try this:
app.get('/partials/login', routes.partials);
app.get('/partials/:name', ensureAuthenticated, routes.partials);
Your routes.partials does have to check explicitly if it's called for the login route, because in that case the name parameter won't exist in req.params.
Just to make sure: do you have a server-side handler for /login as well? Otherwise the res.redirect('/login') isn't going to work.
I don't think that server side redirecting on XHR request is a good practice. XHR request should return 401 if the user is not authorized and the client side (Angular) should take the action as displaying notification message and redirecting user to the login partial. Look at this project for handling such requests. There are demo and blog post.

Resources