Wildcard subdomain info sharing between node server and Nuxt/Vue client - node.js

We are building a multi_tenant solution with NodeJS/Express for the back end and VueJS/Nuxt for the front-end. Each tenant will get their own subdomain like x.mysite.com, y.mysite.com, etc.
How can we make both our back end and front-end read the subdomain name and share with each other?
I have some understanding that in the Vue client, we can read suvdomain using window.location. But I think that's too late. Is there a better way? And what about the node /express setup? How do we get the suvidhaon info there?
Note that Node/Express server is primarily an API to interface with database and for authentication.
Any help or insight to put us on the right path is appreciated.

I'm doing something similar in my app. My solution looks something like this...
Front End: In router.vue, I check the subdomain to see what routes to return using window.location.host. There is 3 options
no subdomain loads the original routes (mysite.com)
portal subdomain loads the portal routes (portal.mysite.com)
any other subdomain loads the routes for the custom client subdomain, which can be anything and is dynamic
My routes for situation #3 looks like this:
import HostedSiteHomePage from 'pages/hostedsite/hosted-site-home'
export const hostedSiteRoutes = [
{ path: '*', component: HostedSiteHomePage }
]
The asterisk means that any unmatched route will fallback to it.
In your fallback page (or any page), you will want this (beforeMount is the important part here):
beforeMount: function () {
var host = window.location.host
this.subdomain = host.split('.')[0]
if (this.subdomain === 'www') subdomain = host.split('.')[1]
this.fetchSiteContent()
},
methods: {
fetchSiteContent() {
if (!this.subdomain || this.subdomain === 'www') {
this.siteContentLoaded = true
this.errorLoadingSite = true
return
}
// send subdomain to the server and get back configuration object
http.get('/Site/LoadSite', { params: { site: this.subdomain } }).then((result) => {
if (result && result.data && result.data.success == true) {
this.siteContent = result.data.content
} else {
this.errorLoadingSite = true
}
this.siteContentLoaded = true
}).catch((err) => {
console.log("Error loading " + this.subdomain + "'s site", err)
this.errorLoadingSite = true
this.siteContentLoaded = false
})
},
}
I store a configuration object in json in the database for the subdomain, and return that to the client side for a matching subdomain then update the site to match the information/options in the config object.
Here is my router.vue
These domain names are supported:
mysite.com (loads main/home routes)
portal.mysite.com (loads routes specific to the portal)
x.mysite.com (loads routes that support dynamic subdomain, fetches config from server)
y.mysite.com (loads routes that support dynamic subdomain, fetches config from server)
localhost:5000 (loads main/home routes)
portal.localhost:5000 (loads routes specific to the portal)
x.localhost:5000 (loads routes that support dynamic subdomain, fetches config from server)
y.localhost:5000 (loads routes that support dynamic subdomain, fetches config from server)
import Vue from 'vue'
import VueRouter from 'vue-router'
// 3 different routes objects in routes.vue
import { portalRoutes, homeRoutes, hostedSiteRoutes } from './routes'
Vue.use(VueRouter);
function getRoutes() {
let routes;
var host = window.location.host
var subdomain = host.split('.')[0]
if (subdomain === 'www') subdomain = host.split('.')[1]
console.log("Subdomain: ", subdomain)
// check for localhost to work in dev environment
// another viable alternative is to override /etc/hosts
if (subdomain === 'mysite' || subdomain.includes('localhost')) {
routes = homeRoutes
} else if (subdomain === 'portal') {
routes = portalRoutes
} else {
routes = hostedSiteRoutes
}
return routes;
}
let router = new VueRouter({
mode: 'history',
routes: getRoutes()
})
export default router
As you can see I have 3 different set of routes, one of which is a set of routes that supports dynamic subdomains. I send a GET request to the server once i load the dynamic subdomain page and fetch a configuration object that tells the front end what that site should look like.

Related

Serve multiple protected static folders using Express

I'm trying to write a very simple Express app, which is supposed to serve multiple static folders.
I have a root folder "stories" which contains multiple folders (story-1, story2, etc...). Each story folder contains static assets (scripts, CSS stylesheets, subpages...).
My users can unlock each of those stories, so each story folder must be protected. (If anyone tries to access http://backend/stories/story-1, it should give a 401 Forbidden).
My initial thought was to generate a one-time JWT upfront (like a signed url; not a bearer), add it to query params like http://backend/stories/story-1?jwt=the-jwt-token, then do some backend logic to test if the user has access to this content before serving it.
I tried fiddling with a basic express configuration + a custom authorization middleware :
Project structure :
...
-- /src
-- /stories ⬅️ custom public folder
-- /story-1 ⬅️ public but protected
- index.html
- /subpages
-page2.html
-page3.html
- /styles
- /scripts
-- /story-2 ⬅️ public but protected
- index.html
- /subpages
-page2.html
-page3.html
- /styles
- /scripts
-- /story-3 ⬅️ public but protected
- index.html
- /subpages
-page2.html
-page3.html
- /styles
- /scripts
etc...
index.js :
const express = require("express");
const { authorized } = require("./middlewares/authorized");
const app = express();
const port = 3000;
app.use("/stories/:story", authorized);
app.use("/stories", express.static(__dirname + "/stories"));
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
authorized.js :
exports.authorized = (req, res, next) => {
const jwt = req.query.jwt;
if (!jwt) return res.sendStatus(401);
// todo : custom logic to test if the user has access to this content, if yes we do next(), if no we return a 401.
return next();
};
This simple example works partially, when I try to go to http://localhost:3000/stories/first-story (without JWT), I get a 401 (that's ok).
But when I add the jwt :
http://localhost:3000/stories/first-story/?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The middleware runs for every assets that are linked in the index.html, but those assets urls don't have the JWT query params, which leads to a 401.
I guess it's totally normal because that's how middlewares are intended to work. My guess is that i'm configuring express router wrong :
app.use("/stories/:story", authorized);
app.use("/stories", express.static(__dirname + "/stories"));
I would like to run the middleware only once, when any of the /:story subfolders inside /stories are asked to be served.
You write:
I would like to run the middleware only once, when any of the /:story subfolders inside /stories are asked to be served.
But if every .html (sub-)page is served by a separate HTTP request, each of these requests must be protected, assuming that the HTML contains material that is worthy of protection. (The styles and scripts may not need this extra protection.)
Therefore it is OK that the authorized middleware runs for each such request. And if the JWT was in a cookie (as suggested by Kaneki21), it would be present automatically in each request.
I would separate out the access control logic from the identity logic. You can use your jwt to verify that the user is who they say the are, and then use your existing knowledge of who that user is to grant them access.
I put a simple example using cookie-backed sessions below, note that you can add sequential middleware a, b, and c all in one function via app.use('/foobar',a,b,c).
// other setup
...
const session = require('express-session'),
fs = require('fs'),
{ Router } = require('express');
const secret = (() => {
let secretFile = '/path/to/my/secret.txt';
try {
// try reading a saved secret
return fs.readFileSync(secretFile,'utf8');
}
catch(err) {
// otherwise generate secret and save it
let random = require('crypto').randomBytes(128).toString('base64');
fs.writeFileSync(secretFile,random);
return random;
}
})();
// Add the session middleware to the app
app.use(session(
{ secret,
name: 'stories-and-whatnot',
cookie: { sameSite: true } }
));
// Create a router for stories and add it to the app
let storyRouter = Router();
app.use('/stories', storyRouter);
// add identity middleware to storyRouter
storyRouter.use( authorized);
let storyMax = 10;
for(let i=0; i<storyMax; i++) {
// set up the individual story routers
storyRouter.use(
`/story-${i}`,
function(req,res,next) {
if(!req.session.storyAccess || !req.session.storyAccess[i]) {
// If the user's session doesn't show it has access, reject with 401
res.status(401).end(`You do not have access to story ${i}`);
}
else {
// Otherwise let them proceed to the static router
next();
}
},
express.static(require('path').join(__dirname,`stories/story-${i}`)
);
}
...
// And somewhere else you have something like this
app.get('/access-granted', authorized, function(req,res,next) {
let { id } = req.query;
if(!req.session.storyAccess)
req.session.storyAccess = {};
req.session.storyAccess[id] = true;
res.end(`Access granted to story ${id}`);
});
You might consider, not using a middleware at all for the serving of content. But rather, to setup the user's set of "approved" paths.
That way a user, failing authentication, would have no valid paths, other then perhaps a preset collection of "base" paths.
This way, later after your authentication middleware the "routing" can be constrained to just that users set of "granted" paths.
Essentially model the access using sessions, which are established on first request, and then updated and maintained as things progress.
One solution is that you check if the user has the right to view the page on the client side. You'll need some JavaScript on the client side to do this.
You can store the token in LocalStorage after login. Then, at the beginning of the protected HTML file, you include your JS code to retrieve the token, and send a request to the server to check if the user is authenticated or not. Then, based on the response of the server you show the content or hide it.
To be honest, I rarely see the JWT in the URL. People talk about it here, here, here... You should revise your current approach carefully.

How to Connect Reactivesearch to an external Elasticsearch cluster?

I am trying to connect my reacetivesearch application to external elasticsearch provider( not AWS). They dont allow making changes to the elasticsearch cluster and also using nginx in front of the cluster .
As per the reacetivesearch documentation I have cloned the proxy code and only made changes to the target and the authentication setting(as per the code below ) .
https://github.com/appbaseio-apps/reactivesearch-proxy-server/blob/master/index.js
Proxy is successfully starting and able to connect the remote cluster . However when I connect reacetivesearch app through proxy I get the following error.
Access to XMLHttpRequest at 'http://localhost:7777/testing/_msearch?' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource
I repeated the same steps with my local elasticsearch cluster using the same proxy code and getting the same error .
Just was wondering do we need to make any extra changes to make sure the proxy sending the right request to the elasticsearch cluster ? I am using the below code for the proxy.
const express = require('express');
const proxy = require('http-proxy-middleware');
const btoa = require('btoa');
const app = express();
const bodyParser = require('body-parser')
/* This is where we specify options for the http-proxy-middleware
* We set the target to appbase.io backend here. You can also
* add your own backend url here */
const options = {
target: 'http://my_elasticsearch_cluster_adddress:9200/',
changeOrigin: true,
onProxyReq: (proxyReq, req) => {
proxyReq.setHeader(
'Authorization',
`Basic ${btoa('username:password')}`
);
/* transform the req body back from text */
const { body } = req;
if (body) {
if (typeof body === 'object') {
proxyReq.write(JSON.stringify(body));
} else {
proxyReq.write(body);
}
}
}
}
/* Parse the ndjson as text */
app.use(bodyParser.text({ type: 'application/x-ndjson' }));
/* This is how we can extend this logic to do extra stuff before
* sending requests to our backend for example doing verification
* of access tokens or performing some other task */
app.use((req, res, next) => {
const { body } = req;
console.log('Verifying requests ✔', body);
/* After this we call next to tell express to proceed
* to the next middleware function which happens to be our
* proxy middleware */
next();
})
/* Here we proxy all the requests from reactivesearch to our backend */
app.use('*', proxy(options));
app.listen(7777, () => console.log('Server running at http://localhost:7777 🚀'));
Regards
Yep you need to apply CORS settings to your local elasticsearch.yaml as well as your ES service provider.
Are you using Elastic Cloud by any chance? They do allow you to modify Elasticsearch settings.
If so:
Login to your Elastic Cloud control panel
Navigate to the Deployment Edit page for your cluster
Scroll to your '[Elasticsearch] Data' deployment configuration
Click the User setting overrides text at the bottom of the box to expand the settings editor.
There's some example ES CORS settings about halfway down the reactivebase page that provide a great starting point.
https://opensource.appbase.io/reactive-manual/getting-started/reactivebase.html
You'll need to update the provided http.cors.allow-origin: setting based on your needs.

How to handle large number of redirects in Node/Vue app?

I am working on migrating an existing app to a new tech stack that uses Node and MongoDB on the backend and Vue on the frontend. I have a fairly large number of pages that will need to be redirected to new URLs (over 50). I know I can do something like this in the frontend:
const appRouter = new Router({
mode: 'history',
routes: [
{ path: '/a', redirect: '/a2' },
{ path: '/b', redirect: '/b2' },
{ path: '/c', redirect: '/c2' },
]
});
However it doesn't strike me as particularly elegant. I could see keeping the redirects in another file and importing them to keep my router file neater, but that seems like just a formatting benefit.
I'm wondering how other people handle a large number of redirects in Vue? Would this be better to do at the server-level with Node?
If boilerplate is the problem, you can use something like:
const router = new VueRouter({
routes: [
{ path: '/([abc])', redirect: to => {
returect to.path + '2'; // to.path will be like '/a'
}}
]
})
Notice that the part inside () is a regex that can be customized.
I have a fairly large number of pages that will need to be redirected to new URLs
When we talk about redirecting a Uniform Resource Locator (URL) in the context of a Single Page Application (SPA) like Vue with Vue Router, hosted by a web server like Node.js, we might mean one of two things:
we've changed the route of a view within our Vue SPA
we've changed the location of our SPA (the resource) from one location to another.
To determine which kind of redirect you need to do, we can examine how the URL will change. URLs are made up of these components:
scheme:[//[user[:password]#]host[:port]][/path][?query][#fragment]
By default, Vue Router uses the #fragment (hash) portion of the URL to change views, so if this changes then we should redirect using Alias or Redirect.
If any other portion of the URL changes, we should have Node.js return an HTTP status code for redirect, like 301 Moved Permanently or 302 Moved Temporarily.
Normally the solution from #acdcjunior is good enough, but sometimes you may prefer hooking beforeRouteUpdate to implement the redirect.
You can check vue-router: dynamic Routing for more details.
Below is one simple sample is from the official document:
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
if ( to.match(new RegExp('your_regex_expression'))) {
next('redirect_url')
} else {
// default
next()
}
}
}
Or in main.js by using global guards:
import router from './router'
router.beforeEach((to, from, next) => {
if ( to.match(new RegExp('your_regex_expression'))) {
next('redirect_url')
} else {
// default
next()
}
})

Subdomain routing in loopback to multiple static files (angular apps)

I've got two angular 2 client apps that I'd like to serve from my loopback backend. I've been able to serve them successfully from subdirectories (www.mysite.com/subdirectory), but I'm trying to serve them from subdomains (www.subdomain.mysite.com).
Loopback doesn't seem to have a built in way to handle subdomain routing through the middleware or anywhere else that I can see. So I'm trying to accomplish the routing by matching the hostname in a url-not-found-handler that is set for final in the middleware.json file as such:
if (req.hostname.match(/subdomain1\./g)) {
console.log('requesting subdomain1 site')
res.sendFile(path.resolve() + '/dist/subdomain1/index.html')
} else if (req.hostname.match(/subdomain2\./g)) {
console.log('requesting subdomain2 site')
res.sendFile(path.resolve() + '/dist/subdomain/index.html')
} else {
next();
}
I've also got the static files in the middleware.json set up as such:
"files": {
"loopback#static": [
{
"name": "subdomain1",
"params": "dist/subdomain1"
},
{
"name": "subdomain2",
"params": "dist/subdomain2"
}
]
}
This seems to work in that it properly matches and sends the correct index.html file. I know it's the right index.html by inspecting in the browser.
But for some reason the actual angular app that gets served ALWAYS is whatever is first in the loopback#static array. If I have subdomain2 first, that will be shown for both subdomain1.mysite.com as well as subdomain2.mysite.com.
How can I fix this issue and serve a different apps based on the subdomain?
So I figured out a solution. Don't think loopback has a built in way of handling this, so got it to work with the following:
Cleared the files section from middleware.json
"files": {}
Used a combination of vhost and serve-static to deliver based on the subdomain
var vhost = require('vhost');
var serveStatic = require('serve-static');
var serveSubdomain1 = serveStatic('dist/subdomain1', {'index': ['index.html']})
var serveSubdomain2 = serveStatic('dist/subdomain2', {'index': ['index.html']})
app.use(vhost('subdomain1.mysite', serveSubdomain1));
app.use(vhost('subdomain2.mysite', serveSubdomain2));

How to redirect multiple subdomains to the same running express app

I'm building a SaaS app in NodeJS and using the Express framework. The individual members of the website has a url with a custom subdomain to login.
For example, a company called ABC Corp may login at abc.example.com and another company called Sony may login at sony.example.com
Any idea how I can redirect/route multiple subdomains to the same app instance?
You can use the express-subdomain package. Assuming you have a routes folder, containing abc.js and sony.js files that respectively export login routes for the abc and sony subdomains, you could have the following in index.js or whatever file from which your express server is listening.
const subdomain = require('express-subdomain');
const express = require('express');
const app = express();
const abcRoutes = require('./routes/abc');
const sonyRoutes = require('./routes/sony');
const port = process.env.PORT || 3000;
// use the subdomain middleware
app.use(subdomain('abc', abcRoutes));
app.use(subdomain('sony', sonyRoutes));
// a simple get route on the top-level domain
app.get('/', (req, res) => {
res.send('Welcome to the Home Page!');
});
// add any other needed routes
module.exports = app.listen(port, () => {
console.log('Server listening on port ' + port);
});
Your server will then be live and working as expected
http://example.com/ --> Welcome to the Home Page!
http://abc.example.com/login --> (Your login page for abc)
http://sony.example.com/login --> (Your login page for sony)
To tests subdomains locally you need to add them to your /etc/hosts file. (it requires sudo permissions)
127.0.0.1 localhost
127.0.0.1 example.com
127.0.0.1 abc.example.com
127.0.0.1 sony.example.com
The equivalent for /etc/hosts file on windows is at %systemroot%\system32\drivers\etc
For better details on setting up localhost domains locally check here
You can do more with the subdomain package. It accepts wildcards and you can use it to check API keys if you need such a feature.
Checkout the documentation for the express-subdomain package at https://npmjs.com/package/express-subdomain
You can actually handle that particular route or a wide range then go for Reg Exp (which allows you to do this app.get(new RegExp('(your|string)\/here'), function…) which you want to redirect and then follow the redirecting action something like below code is doing:
response.writeHead(302, {
'Location': 'yourpath/page.html'
//add other headers here...
});
response.end();
Update 1 : [as per the comments and other updates]
Then you try to handle all requests with the following app:
express()
.use(express.vhost('abc.example.com', require('/path/to/loginApp').app))
.use(express.vhost('sony.example.com', require('/path/to/loginApp').app))
.listen(80)
where /path/to/loginApp can be absolute paths or relative paths.
I hope this solves your problem.
Update 2:
Actually when a request comes in the request event is raised on a HTTP Server. So basically it is handled by express, express.vhost is a middle-ware function which raises the request event on another instance of a HTTP Server, that is how it works.
Below is the code:
function vhost(req, res, next){
if (!req.headers.host) return next();
var host = req.headers.host.split(':')[0];
if (req.subdomains = regexp.exec(host)) {
req.subdomains = req.subdomains[0].split('.').slice(0, -1);
server.emit('request', req, res);
} else {
next();
}
};

Resources