I'm trying to setup routes for my Symfony2 single page app and I'm not sure how to go about it correctly without it feeling super hacky.
Here is what I need it to do and how I've attempted to set it up:
When Authenticated
Any route requesting application/jsonshould hit the routes they have been setup for as usual.
Any route that is entered not requesting application/json should load a controller that renders a twig file containing all the JS for my single page app.
Any static resource that doesn't exist and ends looking for a symfony route eg [.js, .css, .jpeg, etc] should return 404.
When NOT Authenticated
Anything requesting application/json should return 403
Anything NOT requesting application/json should return to the login page
Here is what i've attempted so far:
Setup routes with the FOSRestBundle for each service
Setup a listener that returns the base controller html if the request isn't application/json
if (!in_array('application/json', $request->getAcceptableContentTypes())) {
$fakeRequest = $event->getRequest()->duplicate(
null,
null,
array('_controller' => 'HvHDashboardBundle:Dashboard:index')
);
$controller = $this->resolver->getController($fakeRequest);
$event->setController($controller);
}
Setup a bunch of 'catch all' routes to fake a 404 if the static resource doesn't exist.
# routing.yml
# Catch any files that are meant to be their own static resource and return 404
catch_all_fail:
pattern: /{uri}.{_format}
defaults: { _controller: MyBundle:Dashboard:return404 }
requirements:
_format: js|hbs|css|jpg|gif|jpeg|png
Issues
This approach feels like a massive hack, and is not how the Symfony routing system is intended to work
The base controller page is returned even if you aren't authenticated because the type listener is being hit before the security context and forcing that controller to render.
Question:
How do other solve this issue with routing and single page apps with Symfony where they initially need to render HTML with twig, then JS takes over and requests JSON?
Only make an API, no static pages at all. Trust me, I recently did a moderate sized API with Symfony and that's the way to go. It will simplify your backend security a lot, if you do not mix the API with static pages. Ofcourse you can still have static pages, if you want to have some sort of Landing page or something. but try not to mix them with the main app.
Even for login, do not make a static page, but instead have an api route that will validate username/password and return the user the auth token in response. One user can have multiple tokens for example (can be logged in at multiple locations), and the token is sent in the request headers everytime.
If the token is validated ok, symfony will know which user it belongs, so you will know the 'User'. If no token is present, it should return "Not Authenticated", and if token is invalid also something like that or 'Bad request'.
One thing that I had to do when doing with APIs is that I had to write a request listener to accept JSON content and transform it into request object, so I could access data with $request->request.
If you have any questions, let me know in comment and I can help.
As far as routing is concerned, follow the REST rules and you will be good to go.
Related
The problem
I have a web-server that generates an index page that runs Angular, which uses API calls to display data.
I have meta tags which are being set by Angular API calls too.
Now if I will post my website on Facebook, the tags will not be initialized because Facebook scrawler does not support Javascript.
Solution
Detect Facebook user-agent server-sided, and dedicate specific routes that will use a controller action that returns an index page already initialized with that meta data that facebook needs, using the API calls server sided.
I have tried using the condition option that I read here in Symfony router but I am getting the following error:
Resources/config/routing.yml"
contains unsupported keys for "bot_facebook_post": "condition"
And that's how my route looks in yml:
bot_facebook_post:
pattern: /locations/{location}
condition: "request.headers.get('User-Agent') matches 'facebookexternalhit/1.1'"
defaults: {
_controller: Main:bot, slug: ""
}
I have a function that calls our API called executeGetRequest (with variants for other HTTP methods). It is static and located in a base class. It is called from Controllers. If the API returns a 401 HTTP status code, executeGetRequest should redirect the user to the logout page on the "frontend" Yii2 project in order to clear the out of date session data.
My coworker and I have tried multiple different ways to redirect. $this->redirect() will not work because there is no $this object from a static context.
return Yii::$app->getResponse()->redirect(Url::to('login/user-logout'));
does not work.
Yii::$app->getResponse()->redirect(Url::to('login/user-logout'))->send();
return;
does not work. We tried these with and without Url::to().
I was able to get a Yii::trace() in the conditional that checked for the 401 response. It works fine. So the problem is not the detection of 401 status codes, but the redirect.
This should work
Yii::$app->response->redirect(['login/user-logout'])->send(); return;
Possible causes why it's not working in your case:
This was ajax/pjax request.
You have used redundant Url::to() with wrong route.
You are expecting POST request which is often the case with logout actions.
401 means it's probably #3. Try the same redirection mechanism but pointing to other route and/or remove verb behavior for user-logout POST action to verify it.
I am re-writing the authentication process for my application running under ZF2.
I need to have more options to authenticate the user depending on the service called.
I.E. for web application accessed from browser I will authenticate via Ldap and for API services I will check the user credentials in the header.
I created an abstract controller checking if the user is authenticated; if not it will be redirected to login page.
All the controllers in the modules needing the same authentication process will extend this class.
I need to save the original request to redirect it back to it after successful login.
My questions are:
1. Is the abstract controller -> onDispatch() method the right
place to place it?
Every solution I found around was always doing it in the Module.php. To distinguish the auth method they need to check if the requested controller match, since Module.php is called always. Isn't it 'cleaner' to set it in the controller?
2. Should I use redirect or forward to pass from original controller
to login controller and then back?
I don't mind the url changing in the browser bar, just looking for the best and fastest solution keeping also original request.
3. Is it correct to store the uri in the session class ( from the
auth module)? Is there any way to conserve the whole request (including maybe the POST data in case needed)?
Here is the abstract controller:
abstract class AbstractAuthActionController extends AbstractActionController {
public function onDispatch(MvcEvent $e) {
$serviceManager = $e->getApplication ()->getServiceManager ();
$auth = $serviceManager->get ( 'LdapAuth\Client\Ldap' );
if (! $auth->hasIdentity ()) {
$uri = $e->getRequest()->getRequestUri();
$callBackFunction = $this->getLdap ()->getCallBackFunction (); // = new SessionData();
$callBackFunction::setOriginalUri($uri); // function to store temporarly the uri
return $this->redirect ()->toRoute ( 'ldap-login-route' );
} else {
return parent::onDispatch ( $e );
}
}
}
A lot of people do that because they want to take care of checking authentication before the controller dispatch event. Authentication can be checked much earlier in the process, for example on route event, or at least before the controller has been instantiated (dispatch with higher priority then controller).
In case the user is unauthenticated you want to respond with a 401 (unauthorized) or 403 (forbidden) or a 302 (moved temporarily) response (read some more background on this status code here on Wiki) as early as possible to prevent all the overhead which only keeps your server (unnecessarily) occupied and thus slows down your application and delays the unauthenticated response.
module.php is NOT the best place to add all the authentication related code. Better would be to create an authentication listener (and inject a authentication service in the listener) and only connect the listener in your module.php.
Read on the difference between redirect and forward here in this answer. If want to redirect the client that it is not properly authenticated in a response with a 302 status code you will need to send a redirect response including this status code. I also see people using forward in such cases, but in my opinion it is not correct, because the client won't get notified about any redirection. You could also check authentication modules like ZfcUser to see how they handle this.
You don't need to store this url on the server, you can send the url you want to go to after logging in (the original url) inside the redirect response. For example you redirect to login.php from a request targeted at profile.php, then your redirect url could look like this:
http://www.example.com/login.php?redirect=profile.php
You can now set the redirect inside the login process/controller so that after a successful login you return the client to to profile.php.
I have a single route defined like this:
Route::resource('problem', 'ProblemController');
The moment I POST to /problem, a ProblemController#store method is fired.
Now what I want is to return a JSON response if it's an API call or a view (or maybe redirect) if I'm on the "web-side" of my application. How can I approach this problem?
Should I create separate controllers? Should I (in every method/controller) detect the type of the request and respond accordingly? Should I use middlewares? Route groups? Separate application?
The main goal is to have multiple application types (API + versioning + web) in one package but share the business logic, models and most of the code (to avoid repeating).
I am using Laravel 5.2.
Thank you!
Request object offers a method wantsJson() that checks Accept header of the request and returns TRUE if JSON was requested.
In your controller you can do the following:
if( request()->wantsJson() )
{
return ['foo' => 'bar'];
}
return view('foo.bar');
You can read more about content negotiation in Laravel here: http://fideloper.com/laravel-content-negotiation
You can create a route group like this:
Route::group(['prefix'=>'api'], function(){
//All routes in this route become domain.com/api/route
});
This makes the most sense to me because a route that returns a view and an API route are two separate things. You should have a controller for the pages and views you want to show in your app, and another one for the api routes that update and change your data, returning JSON.
Im creating a MySQL database driven PHP (W)CMS application which follows the MVC pattern. First take a look at the framework:
The MVC framework handles the request and decides what to load/call based on the URL, like: http://domain.com/user/details/121 will load and instantiate a User controller object, and calls its details(121) method with the userid passed as a parameter, and then instantiate a User_Model and "ask" it for the detailed data of the user with the 121 userid, and at last display the result with a View. This is the basic concept of an MVC architecture. Nothing particular, everything is clear at this point.
Whereas this will be a CMS, I want to handle a Page model. A user with the nesessary permissions (mostly admin and/or root) can perform basic CRUD operations and other stuff on a page, for example:
I can create a page with the:
tile = 'About us' (this will be displayed as a headline of the page or the title of the browser tab like eg.: HTML title and h1 tags)
URL denomination = '*about_us*' (this will be the URI endpoint, like: http://domain.com/about_us)
reference name = 'Who we are' (This is the text displayed in the menubar)
page content = 'lorem ipsum...' (The actual content of the page...by a WYSIWYG html text editor)
and much more options like structuring the pages, to assign sub pages under a parent page, or making a page startpage (which means if I set 'About us' as a start page, then http://domain.com will automaticall load that page content)...
Or I can modify these properties, even I can delete a page...etc.
The MVC framework makes no difference between handling a frontend and a backend call.
For example we have some requests:
http://domain.com/user/details/121
http://domain.com/about_us
http://domain.com/our_products/1255
The first will load a backend controller as I detailed before,
but the others will load a frontend content.
When the Bootstrap loads the appropriate controller/action we look for the actual controller file, in the example above :
/controllers/Users.php
/controllers/About_us.php
/controllers/Our_products.php
The first can be loaded because that is a 'static' controller written before, but the About_us and Our_products are not existing controllers so If it is impossible to load the controller, the bootstrap searches the database if is there a page with the same URL denimination (like: about_us, our_products). If there is, we load a common FrontEndController and display the requested page data, if there isn't, display a 404 error.
I do this because I want the bootstrap to handle all requests the same way, but I dont want to every frontend URL compulsorily contain the FrontEndController (e.g.:http://domain.com/FrontEndController/our_products/1255). So this is how I hide it from the user, so the URL can remain more user friendly. My question is: Is this a good practice? Or are there any other proper ways to do this?
The MVC framework handles the request and decides what to load/call based on the URL
What you would normally is have is some sort of Router and Dispatcher class. The router would accept the the user/details/121, parse it and return a Route.
$route = $router->route( $request->getUri() );
The router could hold config values like the allowed space character in URI's, default allowed characters etc.
You can also add custom routes to the router
$router->addRoutes($routes);
The custom routes can be a simple associative array
$routes['requested-uri'] = 'custom-route'
In the example above you said when they visit the root of the website you want them to actually see the About Us page so that could be done like this:
$router->addRoutes([
'' => 'about-us
]);
Meaning when the URI is ''(blank) then go to the 'about-us' route. It shouldn't do a redirect, just transparently load up a different route while keeping the URI in the clients web browser the same.
Routing can obviously be more complex, using route objects added to a route collection for more advanced custom routing with more control. Some frameworks use annotations and all sort of different ways to achieve flexible routing.
The dispatcher could then accept the route returned from the router and dispatch it. That means verifying if the requested route actually exists i.e does the controller file exist and the requested method in the controller exist.
$view = $dispatcher->dispatch($route);
Inside the Dispatcher::Dispatch() method:
// Check if the controller file exists.
// Instantiate the controller file, preferably using a controller factory.
// Check if the controller method exists.
// Call the controller method
call_user_func_array([$controller, $route->getMethod()], $route->getParams());
$view = $controller->getView();
$action = $route->getAction();
// Call the view method.
if( method_exists($view, $action) ) {
$view->$action();
}
return $view;
I find the following a very easy to understand way of dealing with controller methods/actions. Let's say you have a login controller, the user sends a GET request to it first and a POST request to it when sending the login details in the form.
public function getIndex() { }
public function postIndex() {
$username = $this->request->post('username');
$password = $this->request->post('password');
}
The get and post in front of the method name is the request type, this prevents you having to do something like this
public function index() {
if( $this->request->getType() === 'POST' ) {
$username = $this->request->post('username');
$password = $this->request->post('password');
}
}
It also gives you more control over authorisation(if you do it at the routing layer) because you can easily allow a user to send a GET request to the controller but deny them access to sending a POST request.
Each controller has a one to one relationship with a view. The view get's injected into the controller on construction, preferably using a controller factory.
What would happen when you send a GET request http://domain.com/user/details/121 is the router would break up the URI and turn it into a route targeting the User controller, the getDetails() method with the parameter 121, the dispatcher checks if the controller and method exist, it then calls the method supplying the user ID as an argument, the controller sets the user ID in the view. Below is the User controller.
public function getDetails($userId) {
$this->getView()->setUserId( (int)$userId );
}
The view then has a method called details(). The same name as the method called in the controller, just without the request type in front of it.
The dispatcher then calls the details() method of the view which then fetches the required data.
Setting the title of the page is done in the view, as it is for presentation purposes only.
Part of the view that is related to the User controller
public function details() {
// Fetch the user by using the previously set user ID from the controller.
// If he doesn't exist set an error template, set the response code to 404,
// or redirect. Do whatever you want really.
$this->setTitle('User Details');
// Build template objects, bind the fetched user data to main template.
}
How you implement the setTitle method and all over view related stuff is up to you.
The view sends the response back to the client, whether it is HTML content, JSON, XML, or any other content type.
For example your application lets you search for users and export them to a Microsoft Excel Workbook file(.xlsx) and prompt the user to download it.
The view would:
Fetch the users
Generate the file
Set the HTTP response headers like Content-Type
Send the response