AngularJS oauth endpoint with hash in url (SpotifyAPI) - javascript

I want to receive the oauth callback from Spotify and have problems with the # in my URL.
routes.js
app.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
...
when('/callback', {
templateUrl: 'views/callback.html',
controller: 'CallbackCtrl'
})
...
}]);
So to access that route the URL is e. g. http://test.com/#/callback
For the spotify endpoint the redirect_uri has to be url-encoded:
https://accounts.spotify.com/authorize/?client_id=...&response_type=code&redirect_uri=http%3A%2F%2Ftest.com%2F%23%2Fcallback&scope=user-read-private%20user-read-email&state=profile%2Factivity
The redirect from spotify:
http://test.com/%23/callback?code=..&state=profile%2Factivity
Which results in a 404
I know there are some workarounds like using the / route or enabling html5mode to get rid of the # but I hope there is a true solution for this.

Well your situation is not that awkward at all.
Since you can't control the way that spotify's redirect after OAuth works, it leaves you with two options:
1) Create a URL Path for example http://test.com/redirect_uri/?code=.... which will automatically redirect the user to the webapp in the state of logged in.
This method is not a good practice, unless you really know exactly what you are doing. The major problem here is security. Unless you add really good Mechanism to the redirection page.
2) Easier, and actually better in all aspects:
$locationProvider.html5Mode(true);
A small introduction on this:
Why does this remove the #? Because when HTML5 mode is on, it will use history to refer to native paths in your app.
But hold on this is not the only thing you have to do:
You have to redirect all request to go through the main page normally index.html.
I use to do this with .htaccess, But since of AngularJS 1.3 I know there is another method with adding meta tag of <base href=/base/path/of/app/directory">. Usually <base href="/">
But I still prefer .htacces rewriterule or w/e webserver you are using accordingly.

It looks like you are going to implement the Authorization Code flow client-side, exposing the secret key you were provided when you register your app. This is wrong, since someone might generate tokens on behalf of your application using the client id and secret key.
A better approach is to either use Implicit Grant (in example how it is done on https://github.com/possan/webapi-player-example) or implement the token exchange server-side and pass the token to your AngularJS webapp.

Check out this article on Scotch.io for "pretty URL's" that should fix this issue.
You will have to set html5Mode to true:
// Remember to inject $locationProvider
$locationProvider.html5Mode(true);
Then, include a base in the of your html file:
<base href="/">
This will let you navigate the webpage using relative links.
The article I mentioned also goes into fallbacks for older browsers.

Related

How to make Angular Universal and PWA work together?

I have a SSR Angular app which I am trying to transform into a PWA. I want it to be server-side rendered for SEO and for the "fast first rendering" that it provides.
The PWA mode works fine when combined with SSR, but once the app is loaded, when we refresh it, the client index HTML file is loaded instead of the server-side rendered page.
I have dug into the code of ngsw-worker.js and I saw this:
// Next, check if this is a navigation request for a route. Detect circular
// navigations by checking if the request URL is the same as the index URL.
if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
// This was a navigation request. Re-enter `handleFetch` with a request for
// the URL.
return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
}
I have no control over this file since it's from the framework and not exposed to developers.
Did anybody find a solution or workaround for this?
Up-to-date answer (v11.0.0)
Angular now has a navigationRequestStrategy option which allows to prioritize server requests for navigation. Extract of the changelog:
service-worker: add the option to prefer network for navigation
requests (#38565) (a206852), closes #38194
To be used wisely! This warning appears in the documentation:
The freshness strategy usually results in more requests sent to the
server, which can increase response latency. It is recommended that
you use the default performance strategy whenever possible.
Old answer (for archaeological purposes)
I have found a working solution, the navigationUrls property of ngsw-config.json contains a list of navigation URLs included or excluded (with an exclamation mark) like explained in the documentation.
Then I configured it like this:
"navigationUrls": [
"!/**"
]
This way, none of the URLs redirect to index.html and the server-side rendered app comes into play when the app is first requested (or refreshed), whatever the URL is.
To go further, the three kinds of URLs managed by the service worker are:
Non-navigation URLs: static files cached by the service worker and listed in the generated ngsw.json file with their corresponding hashes
Navigation URLs: redirected to index.html by default, forwarded to the server if the "!/**" configuration is used
GET requests to the backend: forwarded to the backend
In order to distinguish a GET XMLHttpRequest from a navigation request, the service worker uses the Request.mode property and the Accept header that contains text/html when navigating and application/json, text/plain, */* when requesting the backend.
Edit: This is actually not a good practice to do that for two reasons:
Depending on the network quality, there is no guarantee that the server-side version will render faster than the cached browser version
It breaks the "update in background" mechanism. Indeed, the server-side rendered app will always refer to the latest versions of the JavaScript files
For more details on this, please take a look at the Angular's team member answer to my feature request: https://github.com/angular/angular/issues/30861

How to work with Authentication in Framework7 Vue

I am using the latest Framework7 Vue Webpack starter pack.
My default page ('/') is a login page. My plan was to run a xhr request as soon as any page in the app is requested.
So I tried putting the isLoggedIn() call in the onF7Ready(f7). If logged in I thought I would use this to take the user to the home screen - self.$f7router.navigate('/home/'); else I would take the user to the Login page. Then I learnt the $f7router is only accessible in the Route Components.
Then I thought I will put the isLoggedIn() check in each and every page's pageInit(). So I tried putting that code in the login.vue file in
on: {
pageInit(e) {
The $f7router is available here but the self.$f7router.navigate('/home/'); does not work. The same self.$f7router.navigate('/home/'); however does work if I use it in one of the dummy methods in the same login.vue file.
Even if the above code did work, there must be a better way of checking if a user is logged in and then do things in a much better way then I am doing. All my routes except for the ('/') require authentication.
Can anyone tell me how I should approach this very standard issue? Thanks a lot.
I found that the self.$f7router.navigate is not available in the pageInit(), however, it is available in the pageBeforeIn() and other functions that follows the pageInit().
Sidenote: I found pageInit() to be quite dangerous for this particular use case because everytime you navigate to this page the page is going to run the AJAX request not just when the App is first accessed.

Angular route parameter causing site to crash then redirect

I've been working on this project and we are working in Angular 1.6. I've done routes and route parameters before. Mind you I was brought in to this project as a consultant, so I'm going with what they wrote, and it's been, let's just say, interesting.
Problem
URL format:
https://{baseurl}/#/newPassword/{authstring}
This is the URL that is getting clicked from a validation e-mail sent to a client when they request to change their password. Use your imagination to fill in content, as the bracket notation are placeholders for security purposes.
authstring is your standard encryption, base64/md5 I'm not exactly sure, probably md5. So there is nothing special.
When passing to the route of newPassword, the route crashes, and then it is immediately redirected to the home page of the site and a standard Windows Authentication box pops up. As seen in the image below.
I have no idea why this is showing up, as there is no call for it in the code anywhere to my knowledge, and this has been confirmed by other sources. It's not an IIS issue either, I have already confirmed that too. So there are two items out. The other interesting issue is why is it redirecting as well. The route clearly exists.
Change Password Controller and Route Code
In order to see if the current controller was the issue, I broke the controller down to it's most basic format and did a console log on $routeParams to see if anything changed. The same error occurs.
Here is the controller code I tested with:
angular.module('app').controller('newPasswordController', function ($scope, $route, $routeParams) {
console.log($routeParams);
});
Here is what the route looks like in the router file:
.when("/newPassword/:auth", {
templateUrl: "newPassword/new-password.html",
controller: 'newPasswordController',
})
What am I doing wrong?

How to make AngularJS not to prefix hash-fragment with "/"

I am using a third party service on my site with its own authentication flow. There is an opportunity to reset password. This feature has the following flow. You become the email with the link on yoursite.com/#xxx=123456
After you enter this page third party library is downloaded, and after some init process it checks the location url for #xxx=\d+ fragment to make some ajax call and prompt some ui modals
var hashKeyElements = window.location.toString().match('#xxx=(.*)');
There is no way to change the way they are doing it.
And here is the problem - Angular rewrites yoursite.com/#xxx=123456 to yoursite.com/#/xxx=123456 and after that regexp is not matched
I tried to create double # in url but it causes errors.
UPDATE: Using angular 1.2.4, it's not a SPA and all of the related links should not be prevented and pushed via pushState
Thank you
update
After you mentioned that you are not using client-side routing I checked again and it seems that angular.js (all versions) does not add hashes (/#/) to URL unless you use $locationProvider or $routeProvider even when html5Mode=false.
`
If you do client-side routing or using $location:
you need to use html5Mode - docs:
app.config(function($locationProvider){
$locationProvider.html5Mode(true)
})

What's the AngularJS "way" of handling a CRUD resource

I am interested in moving a lot of my client's "logic" away from Rails routing to AngularJS. I have slight confusion in one topic and that is linking. Now, I do understand there's more than one way to handle this, but what is the common practice in the AngularJS community for handling URLs on handling CRUD for resources. Imagine in the case of an athlete we have a URL such as the following to list all athletes:
http://example.com/athletes
To view an individual athlete:
http://example.com/athletes/1
To edit an individual athlete:
http://example.com/athletes/1/edit
To create a new athlete:
http://example.com/athletes/new
In AngularJS, is it common practice to reroute to similar URLs to create/edit/update? Would you just have one URL handle all of the CRUD type actions in one interface and never change the URL? If you were to change the URL, does that get handled via ng-click and in the click event would you use the $location object to change URLs? I'd love to be able to read up on common practices such as these, but having a difficult time in finding more recent literature on it in an AngularJS context.
** NOTE **
I totally get that you can still use RESTful routes to the backend in order to interact with server-side resources. My question is, what is the style that is recommended to use when updating URLs on the client-side. Are you using AngularJS to do that for each of the CRUD operations?
I would definitely recommend separate URLs for each operation (to enable direct linking). The ones you suggest look fine.
In AngularJS you can use the $route service in combination with the ngView directive to load the appropriate template for each operation and handle the browser location and history mechanics for you.
Step 7 of the AngularJS tutorial gives an example of using Views, Routing and Templates the way I describe here. The following is a simplified version for your example:
Define the routes
In your main application script (e.g. app.js):
angular.module('AthletesApp', []).
config(['$routeProvider', function($routeProvider, $locationProvider) {
// Configure routes
$routeProvider.
when('/athletes', {templateUrl: 'partials/athletes-list.html', controller: AthleteListCtrl}).
when('/athletes/:athleteId', {templateUrl: 'partials/athlete-detail.html', controller: AthleteDetailCtrl}).
when('/athletes/:athleteId/edit', {templateUrl: 'partials/athlete-edit.html', controller: AthleteEditCtrl}).
when('/athletes/:athleteId/new', {templateUrl: 'partials/athlete-new.html', controller: AthleteNewCtrl}).
otherwise({redirectTo: '/athletes'});
// Enable 'HTML5 History API' mode for URLs.
// Note this requires URL Rewriting on the server-side. Leave this
// out to just use hash URLs `/#/athletes/1/edit`
$locationProvider.html5Mode(true);
}]);
We also enable 'HTML Mode' for URLs, see note below.
2. Add an ngView directive to your HTML
In your main index.html you specify where the selected partial template will go in the overall layout:
<!doctype html>
<html ng-app="AthletesApp">
...
<!-- Somewhere within the <body> tag: -->
<div ng-view></div>
...
</html>
3. Create templates and controllers
Then you create the partial view templates and matching controllers for each of the operations. E.g. for the athlete detail view:
partials/athelete-detail.html:
<div>
... Athete detail view here
</div>
athleteDetailCtrl.js:
angular.module('AthletesApp').controller('AtheleteDetailCtrl',
function($scope, $routeParams) {
$scope.athleteId = $routeParams.athleteId;
// Load the athlete (e.g. using $resource) and add it
// to the scope.
}
You get access to the route parameter (defined using :athleteId in the route config) via the $routeParams service.
4. Add links
The final step is to actually have links and buttons in your HTML to get to the different views. Just use standard HTML and specify the URL such as:
Edit
Note: Standard vs Hash URLs
In older browsers that don't support the HTML5 History API your URLs would look more like http://example.com/#/athletes and http://example.com/#/athletes/1.
The $location service (used automatically by $route) can handle this for you, so you get nice clean URLs in modern browsers and fallback to hash URLs in older browsers. You still specify your links as above and $location will handle rewriting them for older clients. The only additional requirement is that you configure URL Rewriting on the server side so that all URLs are rewritten to your app's main index.html. See the AngularJS $location Guide for more details.
The angular way is the restful way:
GET all http://example.com/athletes
GET one http://example.com/athletes/1
POST new http://example.com/athletes
PUT edit http://example.com/athletes/1
DELETE remove http://example.com/athletes/1
Note that $resource also expects a few other things, like resource URLs not ending with a slash, PUT requests returning the updated resource, etc.
If your API doesn't meet these criteria, or you simply need more flexibility, you can build your own $resource-like CRUD service based on the lower-level $http service. One way of doing the latter is explained here
Option 1: $http service
AngularJS provides the $http service that does exactly what you want: Sending AJAX requests to web services and receiving data from them, using JSON (which is perfectly for talking to REST services).
To give an example (taken from the AngularJS documentation and slightly adapted):
$http({ method: 'GET', url: '/foo' }).
success(function (data, status, headers, config) {
// ...
}).
error(function (data, status, headers, config) {
// ...
});
Option 2: $resource service
Please note that there is also another service in AngularJS, the $resource service which provides access to REST services in a more high-level fashion (example again taken from AngularJS documentation):
var Users = $resource('/user/:userId', { userId: '#id' });
var user = Users.get({ userId: 123 }, function () {
user.abc = true;
user.$save();
});
Option 3: Restangular
Moreover, there are also third-party solutions, such as Restangular. See its documentation on how to use it. Basically, it's way more declarative and abstracts more of the details away from you.
In AngularJS you can definitely use RESTful server side data sources, there is build in service called $resource.
Alternatively you can also use restangular which has additional features over $resource.
If you want to have full control you can always use $http service which is low level angular component for interacting with http.
Simply implement something that is RESTful, that is the angularJS way. If you have no idea what RESTful is or, know a little and want to know a lot more, then I would recommend that you read this article.
Basically, REST is what is understood to be, an intuitive implementation of WEB URIs, it also makes use of all HTTP verbs, their correct use actually. REST is an approach, and architecture to building web apps.

Categories

Resources