Enable pushState without hash using backbone and express-handlebars - backbone.js

I am using https://github.com/shorttompkins/benm and I want to implement pushState without using a hash. I have set:
Backbone.history.start({pushState: true});
Here's my server.js:
var express = require('express'),
http = require('http'),
path = require('path'),
routes = require('./app/routes'),
exphbs = require('express-handlebars'),
mongoose = require('mongoose'),
seeder = require('./app/seeder'),
app = express(),
bodyParser = require('body-parser'),
morgan = require('morgan'),
methodOverride = require('method-override'),
errorHandler = require('errorhandler');
app.set('port', process.env.PORT || 3300);
app.set('views', __dirname + '/views');
app.engine('handlebars', exphbs({
defaultLayout: 'main',
layoutsDir: app.get('views') + '/layouts'
}));
app.set('view engine', 'handlebars');
app.use(morgan('dev'));
app.use(bodyParser());
app.use(methodOverride());
routes.initialize(app, new express.Router());
// static asset routes
app.use('/', express.static(path.join(__dirname, 'public')));
app.use('/bower_components', express.static(path.join(__dirname, 'bower_components')));
// development only
if ('development' === app.get('env')) {
app.use(errorHandler());
}
//connect to the db server:
mongoose.connect('mongodb://localhost/MyApp');
mongoose.connection.on('open', function() {
console.log('Connected to Mongoose.');
// check if the db is empty, if so seed it with some contacts:
seeder.check();
});
//routes list:
routes.initialize(app);
//finally boot up the server:
http.createServer(app).listen(app.get('port'), function() {
console.log('Server up: http://localhost:' + app.get('port'));
});
And my routes.js:
var home = require('../controllers/home'),
contacts = require('../controllers/contacts');
module.exports.initialize = function(app) {
app.get('/', home.index);
app.get('/api/contacts', contacts.index);
app.get('/api/contacts/:id', contacts.getById);
app.post('/api/contacts', contacts.add);
app.delete('/api/contacts/:id', contacts.delete);
};
This works fine until I try to refresh a page with a backbone route eg. http://localhost:3300/add then the route is not found and I get an error 'Cannot GET /add'.
From what I've read I need to add a catch-all route similar to:
app.get('*', function(req, res){
// serve home page
});
But I can't work how how to do this with express-handlebars.
Everything I've tried seems to stop the static content being served even though I've got the express.static routes in server.js
Anyone help would be much appreciated.

When using pushstate the browser won't request the URL added through pushState() but might do when reloading the page or restarting the useragent at that location.
From the Mozilla Developer Network:
URL — The new history entry's URL is given by this parameter. Note that the browser won't attempt to load this URL after a call to pushState(), but it might attempt to load the URL later, for instance after the user restarts the browser. [...]
https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method
This applies to all user agents.
The idea of pushstate is, to store state objects for an URL while not requesting that document. However, when the user agent is requested to load that URL after the previous browsing session was destroyed, the server needs to be able to serve that URL. Note that a user might also bookmark or send the URL via e-mail to another person.
Your server and client should be able to initialize the application state accoring to that URL.
Since you said, you
prefer to have standard URLs and reserve the hash for drilling into view content likes tabs, ids, etc.
your best option seems to be to implement routes in your backend. All routes "generated" in your frontend should be served. Your single page application document needs to be served for any of those URLs.
If your application should initialize/start at the correct point, you need to either
pass the current state information from your backend to the client side application
recover the state from the URL at which the application was started on the client side
Given the current route /contacts/24552,
For option 1: you could, for example, handle the route, gather the contact data for 24552 and serve the single page application document with that contact data inlined.
For option 2: you could serve the single page application document as is for any route (except the API routes), examine the URL clientside and do the request client side. For example, you could parse location.pathname, extract relevant information (e.g. "contacts" and the ID) and either directly call the route handler or navigate to an unhandled route and back to the route /contacts/24552 to trigger the route handler.
Note: you may also use hash based URLs when using anchors in your content. You may generate those anchor names (IDs) and prefix them with the current application state route. Your Backbone route handlers have to ignore those anchor names.

Related

Getting images from S3 with a cookie to display on react

I'm building an application with express backend and react frontend. The user can upload images and they're stored in a S3 bucket with a privacy policy and served through Cloudfront.
Currently, I'm using signed urls as a two step way to retrieve the images:
React requests the Express server for a signed url for the image
Express responds with the url
React then loads the <img src={signedUrl} /> tag.
This works, but I rather eliminate the signed url step and use something like cookies so the client can directly ask cloudfront for the image: <img src={cfUrl + cookies?} /> I've tested this using postman and I can retrieve the image directly as long as I pass the cookies. So i know my cookies are setup correctly and the content is being served.
However, I'm not sure how i can do it on react with axios... The application is currently on localhost but It will be deployed to heroku.
Server
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['http://localhost:3000'],
credentials: true,
}));
app.post('/cookies', async (req, res) => {
// Function to generate the signed cookies
const cookies = await awsS3.generateCookies();
Object.entries(cookies).forEach(([key, value]) => {
console.log(key, value);
res.cookie(key, value);
});
res.json([]);
});
const port = process.env.PORT || config.port;
app.listen(port, () => {
console.log(`Express server is running on port: ${port}...`);
});
What i want to do in react:
<img src={image_url} />
However, looking at the network call for the image request, the cookies are not included by default. I don't know if i need to do something special or specific to include the cookies on that image request.
Here is an overview of how CloudFront signed cookies work:
A user signs in to your website and either pays for content or meets some other requirement for access.
Your application returns the Set-Cookie headers in the response, and the viewer stores the name-value pairs.
The user requests a file e.g. an image in your case.
The user's browser or other viewer gets the name-value pairs from step 3 and adds them to the request in a Cookie header. This is the signed cookie.
CloudFront uses the public key to validate the signature in the signed cookie and to confirm that the cookie hasn't been tampered with. If the signature is invalid, the request is rejected.
More details can be found here.
UPDATE
Setting cookies for different domains is obviously not allowed. However, one can use a top level apex domain in a signed cookie, for example, .example.com. If properly set this cookie will always be sent to any request made to example.com, but also to any of its subdomains, e.g., cloudfront.example.com. So we can take advantage of this and have our backend service, which is running on backend.example.com create and set a signed cookie for .example.com. Later when the frontend requests a file form cloudfront.example.com it will automatically include the signed cookie with the request.
This approach requires having a custom domain for a Cloud Front distribution. Here docs how to do that
Best, Stefan

Not sure what to put in callback function for routing with express and react front end

I am new to react and I am trying to build an app where users can log in and out. I am using express for back-end purposes, when writing my routes for the register and login page I'm not sure what to put in the callback function of my get routes for register and login.
Example
app.get("/register", (req, res)=>{
// what do i put here?
});
Usually i would just render an ejs template but im not sure what to do since react takes care of the rendering
Express passes (req, res, next) to every next argument after path in get or post.
You can use it anyhow you want.
Generally, req is what you get from the client, and with res.send(xxx) you respond to the client.
I would recommend, try to build very simple server with
console.log(req, res)
res.send(200)
})
And with some REST API tool (e.g. Advanced REST client for Chrome) try to send some requests and see what happens.
Then I strongly recommend to use www.passportjs.org, which will help you build your authentication and authorization part of your app.

How to transfer data between server and client using Node and Angular?

I have an application built with Node.js, Express, Firebase 3.4.0, and Angular.js. On the server side in server.js, I initialize the app and then get the db reference to users:
var express = require('express');
var bodyParser = require('body-parser');
var path = require('path');
var firebase = require("firebase");
var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.use(express.static(path.resolve(__dirname, 'views')));
// Firebase setup; this is where it messes up...
firebase.initializeApp({
apiKey: "apiKey",
authDomain: "projectId.firebaseapp.com",
databaseURL: "https://databaseName.firebaseio.com",
storageBucket: "bucket.appspot.com"
});
var db = firebase.database().ref();
var users = db.child('users');
app.listen(process.env.PORT, process.env.IP);
app.get('/', function (req, res) {
res.sendfile('/views/index.html');
// missing a statement to pass users to the view
});
Now, this users reference needs to be passed to index.html, preferably as an array, so that data will be manipulated and rendered in the view.
Is it possible to accomplish this given that I will be using Angular?
I understand that a templating language would be handy, but since both the server and the client sides speak the same language, isn't there an easy and elegant way for them to just communicate data?
What's a standard approach to such a problem? The connection to the database must be opened on the server side, so there should be a way, in fact many ways, to transfer that data to the client side for further manipulation/decomposition
Thanks!
At first, the index.html is delivered with no business data, just instanciating your AngularJS app on the client when the DOM is ready.
Then a dedicated Angular service for dealing with Users triggers.
This service asks the server for a JSON payload through the use of $http GET request.
On the server side you handle this request with a new route :
app.get('/users', function (res, req) {
// send the users as JSON
})
When Angular receives a valid response, it then has the users and can choose to render them to HTML in a controller
This round trip can seems wasteful but it has the advantage to be autonomous, so you can reuse this dialog many times during the life of your application.
If you prefer to prepopulate the users on the / route. You need to use a server side templating engine so you can inject a <script></script> in your index.html containing some kind of global var with the initial state of your client app:
<script>window.__INITIAL_STATE__ = { users: … };</script>
This naming is arbitrary, it's just a convention of your choice, so Angular will know to check this first when it boots.

server sends raw json and can't be processed by angular

I'm trying to access the data from server to $scope.resp but only raw json is getting displayed on the page.
here is the routes/addr.js
router.get('/', function(req, res) {
var pers = [{add: "abc"},{add: "pqr"}]
res.json(pers)
});
})
aj.js:
var app = angular.module('addbook', [])
app.controller('listcontroller', function($scope, $http){
$http.get('/list').then(function(response) {
$scope.resp = response
})
})
There is no error on chrome console. I notice that when $http.get request is commented out it still displays raw json. I saw a similar question where they asked to do do stringify but it is not working.
After our discussion in chat, we discovered that the issue was with your Express routes.
Angular is a client framework, which does not perform full page reloads. The user does not navigate directly to the URL on the server that is serving the data, they navigate to a URL that doesn't exist on the server, which is directed to the index.html file, where Angular is able to perform an AJAX request to retrieve the data from the Server URL and dynamically redraw the page.
If your user navigates to the server route that is serving the RAW JSON, the browser will receive the JSON and display it without any templating, styling, or other page elements. Therefore, your server URLs and your client URLs need to be different, and your server should use URLs that are exclusively identified as API urls. many people prepend /api/ to these URLs to avoid confusion, or secure them so that clients can't request them directly.
if you get a pure Json try to convert like this
$scope.resp = JSON.parse(response.data);

Node.js and Angular routing - How to make REST API inaccessible by browser URL

I use Angulars $http service to call for data on the backend. Let's say JSON data. An example URL to do so would be something like:
/get/data
Doing this from within Angular nicely returns the requested data. No problem.
But even though I catch all other Angular routes using Angular UI Router with $urlRouterProvider.otherwise('/');, I can still go to my browser and type in the mydomain.com/get/data URL, which provides me with a page of JSON code.
How to I restrict back-end server calls to come just from Angular, NOT from my browser URL without user authentication?
N.B.
Using Express 4.X on Node, I also provided my app with a 'catch-all' route to default back to my front-end index.html page, like so:
router.get('*', function(req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
Thanks!
My God! Spent whole frikin day fighting this problem, finally fixed it!
The dog is burried in headers - you have to specify one on Angular http request, then read it in node.
First of - routing setup is the same as in this guide: https://scotch.io/tutorials/setting-up-a-mean-stack-single-page-application
On frontend in Angular http request I specify one of the accepted header types to be json:
$http.get('/blog/article', {
headers: {
'Accept': 'application/json;'
}
}).success(function(data) {
console.log(data);
})
.error(function(data) {
console.log('Error: ' + data);
});
On backend in Node I check if header includes json type. If it does, I serve back json data, so angular can receive the content. If it doesn't, I force node to load index.html, from which the controller runs the before mentioned http request with the header, ensuring you get your data then.
app.get('/blog/article', function(req, res) {
if(/application\/json;/.test(req.get('accept'))) {
//respond json
//console.log("serving json data...");
blogItemModel.find({ "_id" : req.query.id }, 'title full_text publish_date', function(err, blog_item){
// if there is an error retrieving, send the error. nothing after res.send(err) will execute
if (err) res.send(err);
res.json(blog_item);
});
} else {
//respond in html
//console.log('Request made from browser adress bar, not through Angular, serving index page...');
res.sendfile('./public/views/index.html');
}
});
Agree with #HankScorpio
Angular UI routing for Angular application paths and server application accessing URL paths are two different things.
Angular UI router allows you to navigate within a single page application as if you have a multi page application. This is in no way similar to accessing the actual server application endpoint.
All restrictions should be done on the web server and server web application end. Hence you will have to implement some authentication/authorisation strategy.
This isn't really an angular issue. When a user enters mydomain.com/get/data they never actually load up the angular app, so your solution must be done elsewhere.
For example, you could add this to your website's .htaccess file. It will redirect all traffic to the root of your domain.
Check out this answer here:
.htaccess Redirect based on HTTP_REFERER
You can't.
Your angular code is running on their machine, in their browser.
As such, one can spoof the environment, capture the data as the browser requests it, edit the JS of your app while it is in their browser, or various other methods.
Why do you want such a restriction anyway?

Resources