Meteor JS (Iron Router) - Restricting access to server routes - javascript

I have a download route in my MeteorJs app which i want to restrict access to. The route code is as follows
Router.route("/download-data", function() {
var data = Meteor.users.find({ "profile.user_type": "employee" }).fetch();
var fields = [...fields];
var title = "Employee - Users";
var file = Excel.export(title, fields, data);
var headers = {
"Content-type": "application/vnd.openxmlformats",
"Content-Disposition": "attachment; filename=" + title + ".xlsx"
};
this.response.writeHead(200, headers);
this.response.end(file, "binary");
},
{ where: "server" }
);
The route automatically downloads a file. This is currently working but I want to restrict access to the route. I only want admins to be able to download it.
I have created an onBeforeAction Hook as below
Router.onBeforeAction(
function() {
//using alanning:roles
if(Roles.userIsInRole(this.userId, "admin"){
console.log('message') //testing
}
},
{
only: ["downloadData"]
}
);
and renamed my route as below
//code above
this.response.writeHead(200, headers);
this.response.end(file, "binary");
},
{ where: "server", name: "downloadData" }
);
The onBeforeAcion hook does not take any effect
Also I noticed neither this.userId nor Meteor.userId works on the route

For the server side hook, I am pretty sure you need the onBeforeAction to have the { where: "server" } part as you do for your route.
Also, I don't think iron:router ever implemented server side user auth on their routing. You may want to check into a package around server routing with larger features such as mhagmajer:server-router that has access to authenticated routes.
https://github.com/mhagmajer/server-router

Related

Why all users in Strapi have access to update all users profile?

I added a new filed called config (type: json) to the User model. I use built-in swagger of Strapi local document. The problem is that I can update another user config (data) with put method.
First, I authorized by POST /auth/local and get my token and my user id (in this cast it's 5)
I add my token to swagger Authorize button.
Then, I use PUT /user/{id} in this case id is 5.
Calling api http://localhost:1337/api/users/4 returns 200!
I expect that I get 403 error! Because I should not able to change other user profiles!!!
Is it normal? If yes, tell me a solution to fix this.
This is because Strapi has only two default roles:
Public
Authenticated
So by default, when you setup permissions, whatever authentication state currently the user has access to all the content accordingly (e.g. Public to only public, Authenticated to authenticated)
To work with this, and to limit the user actions in the auth scope you have to use middleware or policy, so since this is in user-permissions scope let's add policy to user-permissions:
Strapi 4.5.3
yarn strapi generate
? Strapi Generatos
>policy
? Policy name
isOwner
? Where do you want to add this policy?
> Add policy to root of project
Next step is in your /src/extensions folder you have to create folder users-permissions, and in this folder file strapi-server.js with following content:
/src/extensions/users-permissions/strapi-server.js
module.exports = (plugin) => {
for (let i = 0; i < plugin.routes["content-api"].routes.length; i++) {
const route = plugin.routes["content-api"].routes[i];
if (
route.method === "GET" &&
route.path === "/users/:id" &&
route.handler === "user.findOne"
) {
console.log(route);
plugin.routes["content-api"].routes[i] = {
...route,
config: {
...route.config,
policies: route.config.policies
? [...route.config.policies, "global::isOwner"] // tests if policies were defined
: ["global::isOwner"],
},
};
}
}
return plugin;
};
if you did the step correct in your strapi server console you have to see:
info: In isOwner policy. if you send get request to /api/users/:id
Next step is we are going to modify policy file like so:
/src/policies/isOwner.js
"use strict";
/**
* `isOwner` policy
*/
module.exports = async (policyContext, config, { strapi }) => {
strapi.log.info("In isOwner policy.");
const { user, auth } = policyContext.state;
const { params } = policyContext;
// this case the userId is the same as the id we are requesting
// other cases would need more extensive validation...
const canDoSomething = user.id == params.id;
if (canDoSomething) {
return true;
}
return false;
};
and whoala:
{
"data": null,
"error": {
"status": 403,
"name": "PolicyError",
"message": "Policy Failed",
"details": {}
}
}
if we try to get other user profile

Meteor session is undefined after page redirect

I am making a game that requires a lobby of players, but no accounts. Kind of like the game, Spyfall. I am using Meteor Sessions to know which player joined the lobby so that I can return the proper data for that specific player. I have a join.js component where the user enters in the lobby access code and the user's name. This component also redirects the user to the lobby. Join.js is at the route, /join, and the lobbies are at the route, /:lobby. Here is the join.js handleSubmit method which takes the user input and puts it in the players collection:
handleSubmit(event) {
event.preventDefault();
var party = Players.findOne({code: this.refs.code.value});
if(typeof party !== 'undefined') {
Meteor.call('players.insert', this.refs.code.value, this.refs.name.value);
var playerId = Players.findOne({"name": this.refs.name.value})._id;
Meteor.call('players.current', playerId);
location.href = "/" + this.refs.code.value;
} else {
document.getElementById("error").innerHTML = 'Please enter a valid party code';
}
I am using Sessions in the Meteor.methods in the players.js collection to get the current user.
import { Mongo } from 'meteor/mongo';
import { Session } from 'meteor/session';
Meteor.methods({
'players.insert': function(code, name) {
console.log('adding player: ', name , code);
Players.insert({code: code, name: name});
},
'players.updateAll': function(ids, characters, banners, countries, ancestors) {
for (var i = 0; i < characters.length; i++){
Players.update({_id: ids[i]}, {$set: {character: characters[i], banner: banners[i], country: countries[i], ancestor: ancestors[i]},});
}
},
'players.current': function(playerId) {
Session.set("currentPlayer", playerId);
console.log(Session.get("currentPlayer"));
},
'players.getCurrent': function() {
return Session.get("currentPlayer");
}
});
export const Players = new Mongo.Collection('players');
The console.log in the 'players.current' method returns the proper player id, but once the page redirects to /:lobby, the players.getCurrent returns undefined. I want players.getCurrent to return the same value that the console.log returns. How do I fix this issue? This is the function to get the current player id in the lobby.js:
getCurrentPlayerId() {
return Meteor.call('players.getCurrent');
}
Per the Meteor API, Meteor methods are meant to be the way you define server side behavior that you call from the client. They are really intended to be defined on the server.
Methods are remote functions that Meteor clients can invoke with Meteor.call.
A Meteor method defined on the client simply acts as a stub.
Calling methods on the client defines stub functions associated with server methods of the same name
Based on your code it looks like you are doing everything client side. In fact, session is part of the Meteor client API (can't use on the server).
Session provides a global object on the client that you can use to store an arbitrary set of key-value pairs.
Therefore, If I were you, I would just implement all this logic in some sort of util file that you can then import into the Templates where you need it. You are effectively doing the same thing, you just need to use regular functions instead of Meteor methods.
Here is an example util file (be sure to update the Players import based upon your project's file structure).
import { Players } from './players.js';
import { Session } from 'meteor/session';
export const players = {
insert: function(code, name) {
console.log('adding player: ', name , code);
return Players.insert({code: code, name: name});
},
updateAll: function(ids, characters, banners, countries, ancestors) {
for (var i = 0; i < characters.length; i++) {
Players.update({_id: ids[i]}, {$set: {character: characters[i], banner: banners[i], country: countries[i], ancestor: ancestors[i]},});
}
},
setCurrent: function(playerId) {
Session.set("currentPlayer", playerId);
console.log(Session.get("currentPlayer"));
},
getCurrent: function(unixTimestamp) {
return Session.get("currentPlayer");
},
};
Then, you can import this into whatever template you have that has defined the event handler you included in your question.
import { Template } from 'meteor/templating';
import { players } from './utils.js';
Template.template_name.events({
'click .class': handleSubmit (event, instance) {
event.preventDefault();
var party = Players.findOne({code: this.refs.code.value});
if (typeof party !== 'undefined') {
var playerId = players.insert(this.refs.code.value, this.refs.name.value);
players.setCurrent(playerId);
location.href = "/" + this.refs.code.value;
} else {
document.getElementById("error").innerHTML = 'Please enter a valid party code';
}
},
});
Of course you will need to modify the above code to use your correct template name and location of the utils file.
I think the issue is that you are using
location.href = "/" + this.refs.code.value;
instead of using
Router.go("/"+this.refs.code.value);
if using Iron Router. Doing this is as if you are refreshing the page. And here's a package to maintain Session variables across page refreshes.

Kibana Customized Visualization with ES and Angular Doesn't Work

First, I try to make a custom visualization in Kibana with learning here.
Then, I want my custom visualization to display like the clock how many hits my elasticsearch index has dynamically .
So, I changed some codes in above tutorial but they don't work.
Chrome Devtools tells says Error: The elasticsearch npm module is not designed for use in the browser. Please use elasticsearch-browser
I know I had better use elasticsearch-browser perhaps.
However, I want to understand what is wrong or why.
public/myclock.js
define(function(require) {
require('plugins/<my-plugin>/mycss.css');
var module = require('ui/modules').get('<my-plugin>');
module.controller('MyController', function($scope, $timeout) {
var setTime = function() {
$scope.time = Date.now();
$timeout(setTime, 1000);
};
setTime();
var es = function(){
var elasticsearch = require('elasticsearch');
var client = new elasticsearch.Client({
host: 'localhost:9200',
log: 'trace'
});
client.search({
index: 'myindex',
}).then(function (resp) {
$scope.tot = resp.hits.total;
}, function (err) {
console.trace(err.message);
});
};
es();
});
function MyProvider(Private) {
...
}
require('ui/registry/vis_types').register(MyProvider);
return MyProvider;
});
public/clock.html
<div class="clockVis" ng-controller="MyController">
{{ time | date:vis.params.format }}
{{tot}}
</div>
Thank you for reading.
Looks like the controller in angularjs treats the elasticsearch javascript client as if it was accessing from the browser.
To elude this, one choice will be by building Server API in index.js and then make kibana access to elasticsearch by executing http request.
Example
index.js
// Server API (init func) will call search api of javascript
export default function (kibana) {
return new kibana.Plugin({
require: ['elasticsearch'],
uiExports: {
visTypes: ['plugins/sample/plugin']
},
init( server, options ) {
// API for executing search query to elasticsearch
server.route({
path: '/api/es/search/{index}/{body}',
method: 'GET',
handler(req, reply) {
// Below is the handler which talks to elasticsearch
server.plugins.elasticsearch.callWithRequest(req, 'search', {
index: req.params.index,
body: req.params.body
}).then(function (error, response) {
reply(response);
});
}
});
}
});
}
controller.js
In the controller, you will need to call GET request for above example.
$http.get( url ).then(function(response) {
$scope.data = response.data;
}, function (response){
$scope.err = "request failed";
});
In my case, I used url instead of absolute or relative path since path of dashboard app was deep.
http://[serverip]:5601/iza/app/kibana#/dashboard/[Dashboard Name]
*
Your here
http://[serverip]:5601/iza/[api path]
*
api path will start here
I used this reference as an example.

How to redirect to external URL and collect client data in Meteor

My goal is to create a link which redirects to an external URL and track client data like OS, Browser etc.
To achieve this, I created two separate routes. One client route which redirects to a template that tracks client data and redirects to the server route.
Here are my routes:
Router.route('/:_id', {name: 'url', controller: 'UrlController'});
Router.route('/redirect/:_id', {name: 'urlRedirect', where: 'server'}).get(function () {
var url = Urls.findOne(this.params._id);
if (url) {
this.response.writeHead(302, {
'Location': url.url
});
this.response.end();
} else {
this.redirect('/');
}
});
UrlController = RouteController.extend({
action: function () {
this.render('redirect');
},
data: function () {
return Urls.findOne(this.params._id);
}
});
And here is the template:
Template.redirect.onRendered(function () {
var client = new ClientJS(); // Create A New Client Object
var userAgent = client.getUserAgent(); // Get User Agent String
console.log(userAgent);
Router.go('urlRedirect', {_id: this.data._id});
});
Now my question is, is it possible to collect client data from the server route (do I need the internal redirection from :_id to /redirect/:_id)?
If a user has been redirected and he clicks on the browser's 'back' button, he will see the empty redirect template. How can I fix this?
You can get header information from the nodejs request object in your route. For example, how to get the user-agent string:
Router.route('/asd', {where: 'server'}).get(function () {
var request = this.request;
var response = this.response;
var headers = request.headers;
var userAgent = headers["user-agent"];
console.log(userAgent);
response.end();
});

Getting Facebook Avatar in Meteor when Autopublish is removed

Currently when auto publish is removed, only {{currentUser.profile.name}} works.I'm trying to get {{currentUser.profile.first_name}} and the avatar from Facebook but have not been able to do so. Here is my code...
On the Server side:
Meteor.publish('userData', function() {
if(!this.userId) return null;
return Meteor.users.find(this.userId, {fields: {
'services.facebook': 1
}});
});
On Iron Router:
Router.configure({
waitOn: function() {
return Meteor.subscribe('userData');
}
});
From my understanding, I see that Meteor is publishing all the userData and then subscribing to it via Iron Router. What I don't understand is why this is not working -- as I think {{currentUser.profile.first_name}} should work but isn't.
Like Richard suggests, when a user is created, you can copy the services document to the profile doc.
Accounts.onCreateUser(function(options, user) {
// We still want the default hook's 'profile' behavior.
if (options.profile) {
user.profile = options.profile;
user.profile.memberSince = new Date();
// Copy data from Facebook to user object
user.profile.facebookId = user.services.facebook.id;
user.profile.firstName = user.services.facebook.first_name;
user.profile.email = user.services.facebook.email;
user.profile.link = user.services.facebook.link;
}
return user;
});
Your publication to get their first name and Facebook ID would look like this...
/* ============== Single User Data =============== */
Meteor.publish('singleUser', function(id) {
check(id, String);
return Meteor.users.find(id,
{fields: {'profile.facebookId': 1, 'profile.name': 1, 'profile.firstName': 1, 'profile.link': 1}});
});
You can access a user's Facebook avatar with a template helper function...
Template.profileView.helpers({
userPicHelper: function() {
if (this.profile) {
var id = this.profile.facebookId;
var img = 'http://graph.facebook.com/' + id + '/picture?type=square&height=160&width=160';
return img;
}
}
});
In your template, you can then use the following helper (provided you are wrapping this in a block that contains user data):
<img src="{{userPicHelper}}" alt="" />
I believe you're trying to access the first_name field from the services subdocument. It should be {{currentUser.services.facebook.first_name}}
If you want to transfer first_name to the profile subdocument, you can have the following event handler:
Accounts.onCreateUser(function(options, user) {
// ... some checks here to detect Facebook login
user.profile.firstName = user.services.facebook.first_name;
user.profile.lastName = user.services.facebook.last_name;
});

Categories

Resources