so I have a login form that accepts a username and password. When a username/password is entered and submit is clicked, the first step is to check if the account exists and is enabled. I've accomplished that using the code below. The problem is, the server-side method that does the checking, is_user_enabled, can be accessed by the client via the browser console. Usually I can prevent this by doing:
my_method : function(doc) {
if (is_admin()) {
// Only admins can run this method.
}
}
But in the case of is_user_enabled, the user is not logged in yet. So, my question is, what is the correct way to handle this situation?
My code:
client/login.html
{{#autoForm schema=get_login_form_schema id="login_form"}}
{{> flashMessages}}
<fieldset>
<!-- <legend>Create User</legend> -->
{{> afQuickField name="username" placeholder="schemaLabel" label=false}}
{{> afQuickField name="password" placeholder="schemaLabel" type="password" label=false}}
<div>
<button type="submit" class="btn btn-primary">Login</button>
</div>
</fieldset>
{{/autoForm}}
client/lib/helpers.js
AutoForm.hooks({
login_form: {
onSubmit: function (insert_doc, update_doc, current_doc) {
Meteor.call("is_user_enabled", insert_doc, function(error, result) {
if (result) {
// Try to log user in via Meteor.loginWithPassword()
}
});
}
}
});
server/lib/methods.js
Meteor.methods({
is_user_enabled : function(doc) {
// Used by the login form. Returns true if user exists and account is enabled.
check(doc, schemas.login);
var user = Meteor.users.findOne({username: doc.username}, {fields: {status: 1}});
if (user.status === "enabled") {
return true;
}
}
});
Final Solution:
client/lib/helpers.js
AutoForm.hooks({
login_form: {
onSubmit: function (insert_doc, update_doc, current_doc) {
Meteor.loginWithPassword(insert_doc.username, insert_doc.password, function(error) {
// Called with no arguments on success
// or with a single Error argument on failure.
if (error) {
FlashMessages.sendError(error);
this.done();
} else {
// Successful login. Redirect to /.
this.done();
Router.go('/');
}
});
return false; // Prevent browser submit event.
},
}
server/lib/permissions.js
Accounts.validateLoginAttempt(function (info) {
if (info.user && info.user.status === "enabled") {
return true;
} else {
throw new Meteor.Error("Invalid credentials.");
}
});
More info about [Accounts.validateLoginAttempt][1]
You can't prevent the client from calling a server method. Your checks for is_user_enabled and is_admin need to happen inside your server methods as well as on the client. You can of course have private functions inside your methods.js file that only methods on the server can access. For more tips see http://0rocketscience.blogspot.com/2015/07/meteor-security-no-1-meteorcall.html
Yes you can prevent Meteor methods from being executed from the client side. this.connection will only be set inside a method when it is called from the client. When called from the server it will be null. The enables you to do something like this:
serverOnlyMethod: function () {
if(this.connection) throw(new Meteor.Error(403, 'Forbidden.'));
}
Related
I am having a bit of difficulty getting the email of the current user in Meteor.
publish.js
Meteor.publish('allUsers', function(){
if(Roles.userIsInRole(this.userId, 'admin')) {
return Meteor.users.find({});
}
});
Meteor.publish('myMail', function(){ {
return Meteor.user().emails[0].address;
}
});
profile.html
<template name="Profile">
<h1> My Profile </h1>
{{#if currentUser}}
<p>{{currentUser.profile.firstName}}</p> <p>{{currentUser.roles}}</p>
<p>{{currentUser.userEmail}}</p>
{{/if}}
</template>
profile.js
Template.Profile.helpers({
users: function() {
return Meteor.users.find();
},
userEmail: function() {
return Meteor.user().emails[0].address;
}
});
Firstname and ._id display fine, emailaddress unfortunately does not. Does anyone have a tip? thanks!
Your 'myMail publication is both redundant and incorrect. You should either return a cursor (or an array of cursors), or observe a cursor and send handle the publication lifecycle yourself (a fairly advanced feature, irrelevant to your question). You are using it a-la Meteor.methods, and you should not really user Meteor.user() in a publication anyway.
It's redundant because Meteor's accounts package publishes the current user's emails field automatically.
In your template, you are treating userEmail as an attribute of the current user, instead of calling it as a helper.
I would advise to use a guard and make sure that the user actually has an email address, something in the lines of:
JS:
Template.Profile.helpers({
users: function() {
return Meteor.users.find();
},
userEmail: function(user) {
if (user.emails && user.emails.length > 0) {
return user.emails[0].address;
}
return 'no email';
}
});
HTML:
<template name="Profile">
<h1> My Profile </h1>
{{#if currentUser}}
<p>{{currentUser.profile.firstName}}</p> <p>{{currentUser.roles}}</p>
<p>{{userEmail currentUser}}</p>
{{/if}}
</template>
I would also strongly advise against publishing all of the fields in the 'allUsers' publication, as it will expose sensitive data that should not leave the server under almost any circumstances (e.g, password data).
I'm trying to create a helper like this:
this.helpers({
data() {
return Customers.findOne({ user: Meteor.user().username });
}
});
but an error occurs, It seems that the user is logging in when the helper is executing, How can I execute the helper after the user is logged in ?
Don't know if is the best solution but I created a deferred promise that wait for the user to login and resolve the $state.
resolve: {
currentUser: ($q) => {
var deferred = $q.defer();
Meteor.autorun(function() {
if(!Meteor.loggingIn()) {
if(!Meteor.user()) {
deferred.reject('PERMISSION_REQUIRED');
} else {
deferred.resolve();
}
}
});
I hope that it can be useful for someone else.
Try this:
data() {
if(Meteor.user()){
return Customers.findOne({ user: Meteor.user().username });
}
}
Try builtin currentUser users helper, which check whether the user is logged in. Like that:
{{#if currentUser}}
{{data}}
{{/if}}
This question already has answers here:
How to use Meteor methods inside of a template helper
(6 answers)
Closed 6 years ago.
I'm trying to access (another) user's details on the client side in meteor. I have a server side method called 'userDetails' that I'm calling from a template helper called 'acc'.
Server method:
'userDetails': function(userId) {
check(userId, String);
return Meteor.users.findOne({_id: userId},
{fields: {
"services.facebook.first_name": 1,
"profile.birthday": 1,
"services.facebook.gender": 1,
"profile.location.name": 1
}});
}
Template helper:
acc: function(_id) {
Meteor.call('userDetails', _id, function(err, res) {
if(err) throw error;
return res;
});
}
When I try to access acc.profile.birthday in the template I don't get anything. What could cause this?
Meteor calls are asynchronous calls, that is why your helper is not returning any data.
Best option here is to either use Session or ReactiveVar or ReactiveDict
I'll use Session option here
acc: function(_id) {
Meteor.call('userDetails', _id, function(err, res) {
if(err){
}else{
Session.set('userDetails', res)
}
});
return Session.get('userDetails')
}
In your html you can use this helper like this
{{#if acc}}
{{name}}
...
{{else}}
<p>Information not found</p>
{{/if}}
You have to wrap the return in a else statement.
if(error) {
}
else {
return res;
}
The call to you method in asynchronous. This means that the callback function will be executed when your server method completes.
If you want to display the result on a template, you have two possibilities:
1/ Use a session.
acc: function(_id) {
Meteor.call('userDetails', _id, function(err, res) {
if(err){
}else{
Session.set('data', res)
}
});
return Session.get('data')
}
2/ Use Template subscriptions (better solution):
On the server, you publish the data:
Meteor.publish("data", function(){
return Meteor.users.findOne(...)
});
On the client, you subscribe:
Template.mytemplate.onCreated(function () {
Template.instance().subscribe("data");
});
Then directly on the client you will be able to create a helper and call the findOne.
In the html:
{{#if Template.subscriptionsReady}}
{{#each myHelper}}
{{acc.profile.birthday}}
{{/each}}
{{else}}
<p>Loading...</p>
{{/if}}
Important Notice about users:
Users profile are editable by default. Please read this: https://dweldon.silvrback.com/common-mistakes
For example, in this controller parties object autoupdates when another user changes Paries collection, but how do we catch this update and run some controller logic?
this.helpers({ parties: () => Parties.find({}) });
SPECIFIED QUESTION:
Recieved answers do not solve my problem as it's not server logic or any database manipulations that I need to perform upon update. Actually it's controller logic that I need to run.
In the following example I disable submit button if form is not changed. isFormChange function compares party with originalParty. I need to redefine originalParty value when party changes from server side. So how can I do this?
<form ng-submit="vm.updateParty()">
<input type="text" ng-model="vm.party.name">
<input type="submit" ng-disabled="!vm.isFormChanged()" value="Submit">
</form>
directive controller:
function Ctrl($scope, $reactive) {
let vm = this;
$reactive(vm).attach($scope);
vm.helpers({ party: () => Parties.findOne({_id: vm.partyId}) });
let originalParty = angular.copy(vm.party);
vm.isFormChanged = isFormChanged;
vm.updateParty = updateParty;
function isFormChanged() {
return !angular.equals(vm.party, originalParty);
}
function updateParty() {
Meteor.call('updateParty', vm.party._id, vm.party.name);
}
}
Use the collection-hooks package to run code before or after updates, upserts, inserts, and removes.
myCollection.after.update(userId, doc, fieldNames, modifier, options){
...your code
}
You can run your logic on the server, if you have your database functionality in Meteor methods.
Example:
Parties = new Mongo.Collection("parties");
if (Meteor.isClient) {
// This code only runs on the client
Meteor.subscribe("parties");
/* a helper or event can run the Meteor.call() */
var partyData = {}; // get partyData
Meteor.call("insertParty", partyData,
// callback function
function (error, result) {
if (error) { console.log(error); };
});
}
if (Meteor.isServer) {
// This code only runs on the server
Meteor.publish("parties", function () {
return Parties.find({});
});
Meteor.methods({
insertParty: function (partyData) {
// insert logic to run on partyData here
Parties.insert(partyData);
}
})
}
I am trying to make HTTP Auth Interceptor Module work together with $resource.
for now, i have a basic app working with services like this :
angular.module('myApp.services', ['ngResource']).
factory('User', function($resource){
return $resource('path/to/json/', {}, { 'login': { method: 'GET' } });
});
and then controllers like this one :
angular.module('myApp.controllers', []).
controller('User', ['$scope', 'User', function($scope, List)
{ $scope.user = User.query();
]);
and the app :
angular.module('myApp', ['myApp.services', 'myApp.directives', 'myApp.controllers']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/dashboard', {templateUrl: 'partials/dashboard.html', controller: 'Dashboard'});
$routeProvider.when('/user', {templateUrl: 'partials/user.html', controller: 'TrackingCtrl'});
$routeProvider.otherwise({redirectTo: '/dashboard'});
}]);
Until now, everything is working like expected. Then, i put an http auth on the server, so the json files http status are 401 and the browser display a login popup. I would like to use the HTTP Auth Interceptor Module to replace the browser login popup and handle the login. it is the purpose of this script, right ?
To do so, i am trying to understand the demo of the script and make that work with my app.
first, i injected 'http-auth-interceptor' to the app.
then, this was added to index.html
<body ng-controller="UserCtrl" class="auth-demo-application waiting-for-angular">
and the login form above ng-view:
<div id="login-holder">
<div id="loginbox">
<div id="login-inner" ng:controller="LoginController">
<form ng-submit="submit()">
Username: <input type="text" class="login-inp" ng-model="username"/><br/>
Password: <input type="password" class="login-inp" ng-model="password"/><br/>
<input type="submit" class="submit-login"/>
</form>
</div>
</div>
</div>
and the script at the bottom of the page:
<script src="lib/directives/http-auth-interceptor.js"></script>
in controller.js :
controller('LoginController', ['$scope', 'authService', 'User', function ($scope, authService, User) {
$scope.submit = function() {
User.login(
function(user, response) { // success
authService.loginConfirmed();
console.log('login confirmed');
}
);
}
}])
and this directive too :
directive('authDemoApplication', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
//once Angular is started, remove class:
elem.removeClass('waiting-for-angular');
var login = elem.find('#login-holder');
var main = elem.find('#content');
login.hide();
scope.$on('event:auth-loginRequired', function() {
login.slideDown('slow', function() {
main.hide();
});
});
scope.$on('event:auth-loginConfirmed', function() {
main.show();
login.slideUp();
});
}
}
})
Ok… that's it. but it's not working at all: the browser loggin form is still there. and that's the only way to login.
any idea on what i should do to make this work ?
From their webpage:
Typical use case:
somewhere the: $http(...).then(function(response) { do-something-with-response }) is invoked,
the response of that requests is a HTTP 401,
'http-auth-interceptor' captures the initial request and broadcasts 'event:auth-loginRequired',
your application intercepts this to e.g. show a login dialog (or whatever else),
once your application figures out the authentication is OK, you are to call: authService.loginConfirmed(),
your initial failed request will now be retried and finally, the do-something-with-response will fire.
So the only thing you need to do to interact with this is to have a parent scope to listen to even:auth-loginRequired. This can be done in a directive, in a controller, it doesn't matter. I haven't used the software, but I'm imagining something like this:
angular.module('myApp', [])
.service('api', function(httpAutenticationService, $rootScope, $location) {
$rootScope.$on('event:auth-loginRequired', function() {
// For the sake of this example, let's say we redirect the user to a login page
$location.path('/login')
})
$http.get(API_URL).then( ... ); // If the user is not authenticated, this will be intercepted (1)
})
.controller('LoginController', function(httpAutenticationService) {
// Let's imagine you have a route with the 'controller' option to this controller
$http.get(LOGIN_URL).then(function() {
httpAutenticationService.loginConfirmed(); // The success callback from (1) will be executed now
}, function(){
alert("Invalid credentials")
})
})