I have a web app which talks to my backend node.js+sails app through a socket.
Default routes for sockets use id. As example
io.socket.get('/chatroom/5')
My app doesn't authenticate users and as result I want id's to be random, so nobody can guess it. However, id's are generated by mongoDB and aren't that random.
As result, I want to use some other field (a.e. "randomId") and update routing for this model to use this field instead of id.
What's the best way to do it?
P.S. It looks like I have to use policies, but still struggling to figure out what should I do exactly.
You aren't forced to use the default blueprint routes in your app; you can always override them with custom controller methods or turn them off entirely.
The GET /chatroom/:id method automatically routes to the find action of your ChatroomController.js file. If you don't have a custom action, the blueprint action is used. So in your case, you could define something like the following in ChatroomController.js:
find: function (req, res) {
// Get the id parameter from the route
var id = req.param('id');
// Use it to look up a different field
Chatroom.find({randomId: id}).exec(function(err, chatrooms) {
if (err) {return res.serverError(err);}
// Subscribe to these rooms (optional)
Chatroom.subscribe(req, chatrooms);
// Return the room records
return res.json(chatrooms);
});
}
If you don't like the name find or the param id, you can set your own route in config/routes.js:
"GET /chatroom/:randomid": "ChatroomController.myFindActionName"
Also, re:
Default routes for sockets use id.
those routes aren't just for sockets--they respond to regular HTTP requests as well!
I created a policy. This policy converts randomId (which is passed as :id) to real id and saves it in req.options.id (which Sails will pick up).
module.exports = function(req, res, next) {
var Model = req._sails.models[req.options.model];
var randomId = req.params.all()['id'];
Model.findOne().where({ randomId: randomId }).exec(function(err, record) {
req.options.id = record.id;
return next();
});
};
And I apply this policy to findOne and update actions of my controller:
ChatRoomController: {
findOne : 'useRandomId',
update : 'useRandomId'
}
Related
I would like to know the best (and most consistent) way to add redundant bits to my restful uris so that they are more readable while remaining unchanging when some things such as username change.
I read of the concept at this excellent blog post and it is something like what stack overflow does /users/3836923/inkalimeva where the last segment of the URI is redundant and may change but makes the URI more readable and SEO friendly.
Currently I am using Laravel's Route::resource() but that creates routes with only the id segment.
You can use eloquent-sluggable to create slugs for your users. That way the slug will change when they update their username. You can also simply call their username in the url method, though this will result in uglier urls.
This method still requires that you drop Route::resource() and write your routes explicitly.
Here is the code, tested and working:
ROUTES.PHP (don't mind the route details)
Route::get('route-name/{id}/{slugOrUsernameAsYouPlease}', [
'as' => 'admin-confirm-detach-admin',
'uses' => 'AdminController#confirmDetachAdmin'
]);
IN YOUR VIEW
Click me!
OR
Click me!
URL RESULT (My users name here is Fnup. Just for testing)
With Username: http://website.local/route-name/8/Fnup
With Slug: http://website.local/route-name/8/fnup
A quick final note
I just changed fnup's username to fnupper and here is the result:
http://website.local/route-name/8/Fnupper
However the slug didn't change automatically. You have to add that code yourself to the user update method. Otherwise the slug stays as what it was the first time the resource was made. Here is my code when using eloquent-sluggable
public function update(UpdateUserRequest $request)
{
$user = \Auth::user();
$user->name = $request->name;
$user->email = $request->email;
$user->resluggify();
$user->save();
session()->flash('message', 'Din profil er opdateret!');
return redirect()->route('user-show');
}
Which result in: http://website.local/route-name/8/fnupper
New edit per request: Controller method example
Here is my confirmDetachAdmin() method in AdminController.php. Just to clarify, the methods job is to show a "confirm" view before modifying a users status. Just like edit/update & create/store, I made up confirm to accompany destroy (since I'd like a javascript free confirmation option should javascript be disabled).
public function confirmAttachAdmin($id)
{
$user = User::findOrFail($id);
/* Prevent error if user already has role */
if ( $user->hasRole('admin')) {
return redirect()->back();
}
return view('admin.confirmAttachAdmin', compact('user'));
}
You can add your slug/username as a second parameter if you want to, but I don't see a reason, as you can access it from $user when you find them by id.
As opposed to #MartinJH's answer, I don't think you should store your slugs in database if you don't rely only on them in your URIs. A simple link() method on your model, and an explicit route is enough.
App\User
class User extends \Illuminate\Database\Eloquent\Model {
public function link()
{
return route('user-profile', [ $this->id, Str::slug($this->username) ]);
}
}
routes.php
Route::get('{id}/{username}', [ 'as' => 'user-profile', 'uses' => 'UserController#profile' ])
->where('id', '\d+')
->where('username', '[a-zA-Z0-9\-\_]+');
App\Http\Controllers\UserController
...
public function profile($id, $username)
{
$user = \App\User::findOrFail($id);
return view('profile')->with('user', $user);
}
...
I'm using Sails.js 0.10.x. I'm referring to the functionality documented as "Create new record while associating w/ another new record (REST)" in combination with a multi-tenant system. All models have got a related_account as part of the primary key. And I need to find a bullet proof way to have it always filled with the current users related account.
In a parent-child relationship it is easy to fill the related_account on the parent by overriding the blueprint and simply setting the related_account property before the records are created. But as the javascript object is passed to Model.create(parent_and_children) it would mean that I would have to loop over all the children and set the related_account manually before creation. This work is tedious and error prone because there are a lot of situations where I need that. Furthermore this is a possible cause of critical defects as we work in a team and someone could forget to add it. In some cases the standard blueprints might be used, in other cases actions could be implemented manually.
There must be a better way than always setting it manually. Any idea is appreciated.
By the way: The value for the related_account is available in the request variable "user".
Maybe this can help. I am doing the same where I need the companyId of the logged in users on every record (found in session.user.company.id). So this was my solution. It is a policy that checks if the model has any associations and then loops through those associations adding the companyId in the req.body object.
module.exports = function(req, res, next) {
req.options.where = _.assign({companyId : req.session.user.company.id}, req.options.where);
var attachCompanyIdToBody = function(){
if(_.isArray(req.body)){
} else if (_.isObject(req.body)){
req.body.companyId = req.session.user.company.id;
attachCompanyIdToAssociations();
}
};
var attachCompanyIdToAssociations = function(){
// Get assocations with current model
var Model = actionUtil.parseModel(req);
if(Model.associations){
_.forEach(Model.associations, function(item,index,object){
// if has company attached, remove company
if(req.body[item.alias] && item.alias.toLowerCase() !== 'company'){
if(_.isArray(req.body[item.alias])){
// ToDo if assocation is a collection then loop through collection adding companyId
} else if(_.isObject(req.body[item.alias])){
// if assocation is object then add companyId to object
req.body[item.alias].companyId = req.session.user.company.id;
}
}
});
}
};
if(req.body){
attachCompanyIdToBody();
}
return next();
};
When exposing querystring parameters using GET I have the following base URL:
https://school.service.com/api/students
This will return the first 25 students.
What if I want to return a list of students based on ONE of the following criteria:
* have accepted a job
* have received a job offer
* have no job offers
The three above choices are essentially an enum.
Therefore, the query request for students who have no job offers I assume would look like:
https://school.service.com/api/students?jobOfferStatus=3
However, I'm wondering if jobOfferStatus=3 is the proper way to handle this. If so, how would I publish/provide to the clients a list of available options for that jobOfferStatus query parameter? What about other possible query parameters and their valid options? We'll have many possible query parameters like this.
I'd love to see an example of how this should be done properly. What are the best practices?
There are two main options: documenting it, or making it discoverable. A lot of APIs have documentation where they list all of the resources and parameters for reference. Otherwise, the client won't know.
You could also make it discoverable in some way by including the options in a response. For conventions on this, search for HATEOAS if you haven't already. (I'm not really knowledgeable enough about HATEOAS myself to make a suggestion.)
I will mention that "3" is not a very meaningful value for jobOfferStatus, and there's no need for the client to know that number. You can make it anything you want -- jobOfferStatus=none or even jobOffer=none. Your controller can do the work of matching that value to your enumeration. Try to design your interface to be intuitive for developers (and, of course, write good documentation).
To handle multiple query parameters, you can use optional parameters in your function:
public HttpResponseMessage GetStudents(string jobOffer = "",
string other1 = "",
string other2 = "")
{
if (jobOffer == "accepted" && other2 == "whatever") {
// return a response
}
else {
// return a different response
}
}
When the client uses parameters by those names, you can tailor your response appropriately.
You have some options to do this, let's try to help:
1) Configure a generic route to asp.net web api knows how to solve another action's name different from Get to a get method, on the App_Start\WebConfigApi.cs class, try to add this:
config.Routes.MapHttpRoute("DefaultApiWithActionAndId",
"api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
Using it, you can have diferent methods on the api controller:
// request: get
// url: api/Students/GetStudents
public HttpResponseMessage GetStudents()
{
return Request.CreateResponse(...);
}
// request: get
// url: api/Students/GetStudentsWithJobOffer
public HttpResponseMessage GetStudentsWithJobOffer()
{
return Request.CreateResponse(...);
}
// request: get
// url: api/Students/GetStudentsAcceptedJob
public HttpResponseMessage GetStudentsAcceptedJob()
{
return Request.CreateResponse(...);
}
2) Use a simple parameter on the Get method:
// request: get
// url: api/Students?jobOfferStatus=1
public HttpResponseMessage GetStudents(int jobOfferStatus)
{
// use jobOfferStatus parameter to fill some list
return Request.CreateResponse(...);
}
3) Use a simple method with a parameter named id, to get a default friendly url by asp.net mvc web api.
// request: get
// url: api/Students/1
public HttpResponseMessage GetStudents(int id)
{
// use the id parameter to fill some list
return Request.CreateResponse(...);
}
I am somewhat confused of the way to achieve the two-way data binding when posting to my server.
I defined my resource like this:
angular.module('todoServices', ['ngResource']).
factory('Todo', function($resource){
return $resource('api/v1-0/todos/:todoId', {}, {
query: {method: 'GET', params:{todoId:''}, isArray: true},
save: {method: 'POST', isArray: true}
});
})
and I pass the Todo resource to my controller as a dependency.
Then in my controller I have a method to add a new Todo item to my list:
$scope.addTodo = function() {
var savedModel = new Todo();
savedModel.title = $scope.title;
savedModel.description = $scope.description,
//...
savedModel.$save();
$scope.todos.push(savedModel);
}
This works as far as my todo appears in the list, the call to the server works and the item is added in my database.
However, since when I push it to my list, it does not have an ID, yet. The ID is generated by an auto-increment ID in my MySQL database.
My server returns the object in JSON format, so I assume, I have to specify some sort of callback function to get the data-binding to work?
What exactly do I need to do, so my todo which has been added is updated with the correct ID once my server returns the data?
Simply assign the returned object to the savedModel object. Since calls to resources are asynchronous and return a promise, you should use the success function this way:
savedModel.$save(
function success(savedModel) {
$scope.todos.push(savedModel);
});
By the way, check the isArray property of the save method, normally should be false.
In my authentication service, I would like to call methods (query or invoke) on my User service to validate credentials. So, for example:
protected override AuthUser ValidateCredentials(string name, string password,
string customData, out string userData)
{
AuthUser user = null;
userData = null;
using (UserService svc = new UserService())
{
if (the result of a call on UserService shows a valid username/password)
{
//Create the user object
user = new AuthUser()
{
Name = name,
UserId = // that user's UserId
};
}
if (user != null)
{
//Set custom data fields for HTTP session
userData = user.UserId.ToString();
}
}
return user;
}
The results I'm finding when searching for things like "call ria service from another ria service" and similar are unrelated to actually calling one from another. Am I doing something wrong from a paradigm point of view? If not, how the heck do you do this? :)
Aggregating DomainServices when all you want to do is Query is pretty easy. Something like
new MyDomainService().GetUser(userName)
should work just fine. However, when you're trying to Submit or Invoke it become trickier because you'll need to initialize and dispose the DomainService. It's been a while since I did this, but I think you can override Initialize and Dispose in your parent DS to call through to the methods in your child DS. For submitting, you won't be able to call the methods directly. Instead you'll need to create a ChangeSet and call the DS.Submit method.
Also, for your scenario, it might be worth checking out the custom authentication sample here. It's a slightly different approach for what you're trying to do.