I'm currently experimenting a little bit with my first app with angular-js, nodejs and a mongodb backend. This app and its URLs are managed by ngRoute from angular. In this app there is a little admin area with authentication (based on nodejs' client-sessions). This works so far pretty well. The authentication works with URLs managed by nodejs, but not with the ones of the routeProvider. The provider looks like this:
app.config(['$locationProvider', '$routeProvider', function ($locationProvider, $routeProvider) {
$routeProvider.when("/main",
{
templateUrl: "app/main/landing.html",
controller: "MainController"
})
.when("/admin",
{
templateUrl: "restricted",
controller: "AdminController"
})
.when("/login",
{
templateUrl: "app/login/login.html"
})
.otherwise({redirectTo: '/main'});
}]);
The restricted area (/admin) calls a get to the server and sends the view.html if user is valid:
app.get('/restricted', function (req, res) {
if (!req.session_state.user) {
res.res.status(302).redirect("/login");//Does not really work, should redirect to login, but endless loop
} else {
res.sendFile(path.resolve(__dirname + "/../views/restricted/admin.html"));
}
});
The server's restricted folder is outside of the static area provided by nodejs.
The problem is that deep links (when calling directly '#!/admin') there is an endless loop. How can this be solved? I already tried with differnt URLs and status codes. Is this a correct approach to realize authentication with routeProvider or is there a better one?
Related
I make a single page application with Sails and AngularJS. And now i'm trying to make an authentication.
I have this angular code on client side:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/', {
templateUrl:'pages/postList.html',
controller:'PostListController'
}).
when('/createPost', {
templateUrl:'pages/postCreation.html',
controller:'PostCreationController'
})
}])
Here all .html files are stored at assets/ folder, which is public. But i don't want to have a public access to postCreation.html, i want to permit access to this file for some users using policies.
I think i can put all .html files in views folder, create a controller methods for each file and than use policies. But i'm not sure this is a good solution.
So, how to use policies in Sails + Angular SPA?
You cannot use sails policies for this. Instead, you can use the resolve parameter in your $routeProvider.
From $routeProvider docs:
An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired.
I have something along the likes of following configured in an app of mine, which is working great (using $stateProvider, but the concept should work just as well for $routeProvider):
.state('stateOnlyCertainUsersCanSee', {
url: "/stateOnlyCertainUsersCanSee",
templateUrl: "myTemplate.html",
controller: 'myController',
resolve: {
validate: function($q, $sails, $state) {
var defer = $q.defer();
$sails.get("/me") // gets user info
.then(
function(response) {
if (response.user.canAccessThisPage) { // Condition on which to pass or fail an user
defer.resolve();
}
else {
defer.reject("No access to page");
$state.go("home"); // Redirect wherever you want
}
}
);
return defer.promise;
}
}
})
So I've been trying to find a solution for my problem during the last 7 days or so. I have almost given up on this so this is my last attempt at solving this.
I'm trying to build a recipe site which fetches the recipes from my Laravel API Backend (i.e. api/recipes returns all recipes in the MySQL-database). The data is requested from the AngularJS frontend via the $http-service, so far so good.
Single page applications like this isn't a problem since I've defined the routes in Laravel like this. All HTTP reqs who isn't sent to the RESTful API is redirect to my index-view where I want AngularJS to take over the routing from there on.
Route::get('/', function()
{
return View::make('index');
});
Route::group(array('prefix' => 'api'), function() {
Route::resource('recipes', 'RecipeController',
array('except' => array('create', 'edit', 'update')));
Route::resource('ingredients', 'IngredientController',
array('except' => array('create', 'edit', 'update')));
Route::resource('nutrients', 'NutrientController',
array('except' => array('create', 'edit', 'update')));
Route::resource('IngredientsByRecipe', 'IngredientsByRecipeController');
});
App::missing(function($exception)
{
return View::make('index');
});
I want the user to be able to edit existing recipes, create new ones etc. Therefore I've created these routes in Angular:
var recipeApp = angular.module('recipeApp', [
'ngRoute',
]);
recipeApp.config(['$routeProvider',
function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'list.html',
controller: 'MainCtrl'
})
.when('/edit/:recipeId', {
templateUrl: 'detail.html',
controller: 'EditCtrl'
})
.when('/new', {
templateUrl: 'detail.html',
controller: 'CreateCtrl'
})
.otherwise({
redirectTo: '/'
});
}
]);
Unfortunately I can't seem to get this to work even with this routing in Angular. I've seen similar problems being solved by decoupling the app and stuff like that, and I've tried something like that by running my Angular frontend at port 8888 and the Laravel backend at port 8000 but then I got a problem with CORS.
I'm eager to get this to work but can't seem to figure out how to get it to work. It seems like the browser ignores the Angular routing and only uses the Laravel routing, which means that I can only access the index view or the API. How should I solve this?
Building hybrid apps like this is something I would not recommend. You should separate your Laravel API backend from your AngularJS frontend. You can then set up services in AngularJS to call your API. API driven development is the way to go.
If you have problems with CORS, you can modify the headers in your Laravel responses to fix this. To fix the problem with every Laravel route, you can add the following somewhere at the top of your routes.php file:
header('Access-Control-Allow-Origin: *');
Or (better solution if you want it for all routes), add this to your after filter in filters.php:
$response->headers->set('Access-Control-Allow-Origin', '*');
Or you can set up a separate filter:
Route::filter('allowOrigin', function($route, $request, $response) {
$response->header('Access-Control-Allow-Origin', '*');
});
Now, to answer your question ...
In the head of your index file (for Angular), add <base href="/">, and also add $locationProvider.html5Mode(true); inside your Angular config; you can just place it after your $routeProvider.when('/', { ... }); function.
I'm following a tutorial on how to set up authentication with nodejs and passport. (http://scotch.io/tutorials/javascript/easy-node-authentication-setup-and-local)
The tutorial has me rendering templates with ejs and passing in flash data.
Instead of this, I'd like to use angularjs. The part I'm having trouble with is getting the flash data. I know how to use templates and send variables, but what in angular replaces the "req.flash('signupMessage')" in the below code?
This is the code the tutorial shows:
app.get('/signup', function(req, res) {
// render the page and pass in any flash data if it exists
res.render('signup.ejs', { message: req.flash('signupMessage') });
});
This is the code where I set up my route
// public/js/appRoutes.js
angular.module('appRoutes', []).config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
$routeProvider
// show signup form
.when('/signup', {
templateUrl: 'views/signup.html',
controller: 'SignupController'
});
$locationProvider.html5Mode(true);
}]);
Here is the controller:
// public/js/controllers/SetupCtrl.js
angular.module('SignupCtrl', []).controller('SignupController', function($scope) {
$scope.tagline = 'TEST';
});
A similar question was answered here: What is the proper way to log in users using Angular & Express?
TLDR: the answer posted was to the following link, where the author describes that you need to keep all the passport stuff on the server side, and then allow the client side (angular stuff) to request information about the session.
http://vickev.com/#!/article/authentication-in-single-page-applications-node-js-passportjs-angularjs
I have a MEAN stack app generated with the Yeoman Generator Angular Fullstack generator. You should only have access to the site by logging in.
The repo for ensureLoggedIn is here
While logged out, if I try to navigate to '/products' I'm redirected to '/login' just fine. However, I'm having an issue redirecting users who aren't logged in when the url is '/' or even 'localhost:9000' without the slash.
If I'm not logged in, and at the login screen, and I modify '/login' to just '/' or '' I'm sent to "main" in AngularJS and treated as logged in(I'm assuming because it recognizes the session?) and able to click links through to '/products' or '/users'.
My current routes.js looks like this:
/**
* Main application routes
*/
'use strict';
var errors = require('./components/errors');
var auth = require('./controllers/auth');
var passport = require('passport');
var ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn;
module.exports = function(app) {
// Insert routes below
// All undefined asset or api routes should return a 404
app.route('/:url(api|auth|components|app|bower_components|assets)/*').get(errors[404]);
app.route('/login').get(auth.login).post(auth.loginUser);
app.route('/logout').get(auth.logout);
// All other routes should redirect to the index.html
app.all('*', ensureLoggedIn('/login'));
app.route('/*').get(function(req, res) {
res.sendfile(app.get('appPath') + '/index.html');
});
};
I've also tried this with the routes:
app.route('/*').get(function(req, res) {
res.sendfile(app.get('appPath') + '/index.html');
});
Which seems to have the same behavior as placing ensureLoggedIn in app.all.
Here's a snippet of my routing on the Angular side, which uses ui-router:
.config ($stateProvider, $urlRouterProvider, $locationProvider) ->
$httpProvider.interceptors.push('httpInterceptor')
$urlRouterProvider
.otherwise '/'
$stateProvider
.state 'main',
url: '/'
templateUrl: 'app/views/main/main.html'
controller: 'MainCtrl'
.state 'users',
url: '/users'
templateUrl: 'app/views/users/index.html'
controller: 'UsersController'
As I said, the redirect works fine on '/users'. I'm not sure if this is a routing issue or auth issue. Auth should be fine, since clicking logout does take you to login screen and restricts access, but doesn't restrict access to the '/' route.
For the views, the login.jade is actually on the server side and the form is processed there. Except for a 404.jade, all other views are on the client-side and served using ui-router.
I feel like I'm overlooking something basic. Or just don't fully understand how this is working together. Any help would be great.
EDIT:
One thing I tried was changing the routing before app.route('login'):
app.route('/')
.get(function(req, res) {
res.render('login');
});
And changing ui-router url for main from '/' to '/main'.
This still grabbed index.html from angular and logged me in, so it didn't work. I also tried res.redirect to login in routes.js and it didn't redirect.
This is the code I use for handling authentication. It is a hack but I didn't find a better way when I needed to code it. Also the routes defined in the system varied user to user so I couldn't define them in the normal config stage. This may help with your issue though.
$routeProvider
.when("/login", { templateUrl: "/view/account/login.html", controller: Login })
.when("/forgottenpassword", { templateUrl: "/view/account/forgottenpassword.html", controller: ForgottenPassword })
.otherwise({ redirectTo: "login" });
This basically only allows access to 2 views. Once someone authenticates successfully I rebuild the routing table with the new valid views. Any invalid navigation goes to the login view.
I do this through a hack though so it might not be the best implementation angularjs wise. I do this by keeping a reference to $routeProvider on the window object then use $routeProvider as normal when you have a successful logon.
The original $routeProvider provided in angular also needs a public method to clear the existing routes before adding new ones.
After
var routes = {};
Add
this.ClearRoutes = function ()
{
routes = {};
}
Example usage after successful logon
$routeProvider.ClearRoutes();
$routeProvider
.when("/home", { templateUrl: "/view/home.html", controller: Home })
.when("/logoff", { templateUrl: "/view/account/logoff.html", controller: Logoff })
.otherwise({ redirectTo: "home" });
This might seem like a silly question.. but how do I pass req.user.username (for example) to all pages / globally after the user signs in with passport. This question can apply to any data I would like accessible for all pages...
On the server side, I have the below which sends allows routeProvider to handle all client side routing.
app.get('*',
function(req, res) {
res.sendfile('./public/index.html')
// load the single view file (angular will handle the page changes on the front-end)
})
I'm not sure if the solution is specific to passport... express or involves both...
The client side routing is handled by something like:
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/', {templateUrl: 'views/home.html'})
.when('/login', {templateUrl: 'views/login.html'})
.when('/users', {templateUrl: 'views/users.html', controller: 'UserController'})
...
You have two options. Either
BAD - include that data in your compiled view that is initially served (./public/index.html) or
GOOD/COMMON - fetch the data you need inside something like an Angular controller which is in your view; eg.
$routeProvider.when('/', {
templateUrl: 'views/home.html',
controller: function($scope, myThings) {
//myThings is a service that async fetches some data from api
myThings().then(function(user) {
$scope.user = user;
});
}
});
This obviously means you are exposing data on an api endpoint, but how else would angular be fetching the bits it needs since you said this is a single-page app?
Services are the way to go if you want to share global data among controllers, directives and other services.
Depending upon the type of data, you can expose services that load data from the server or services which do not need to make remote call to load data (like some custom view settings).
For example if in case you want to get the current logged in user.
The first thing is to create a method on the server that return the current logged in user data in json format.
Then create something like a UserService or SessionService that call this server method to load currently loggedin user data.
Something like
angular.module('myApp').factory('SessionService',['$http',function($http) {
var service={};
service.getCurrentUser=function() {
return $http('/user');
};
return service;
}]);
Inject this service into your controllers to get the current user. You can optimize it to cache the user data.
If you want to use the data in routeProvider use the resolve property
.when('/users', {templateUrl: 'views/users.html', controller: 'UserController',
resolve: {
currentUser:function(SessionService) {
return SessionService.getCurrentUser();
}
}}})