Cloudflare Workers - SPA with Vuejs - vue.js

Hello I have deployed my Vue.js app to Cloudflare workers using the following commands:
wrangler generate --site
wrangler publish --env dev
This is my wrangler.toml:
account_id = "xxx"
name = "name"
type = "webpack"
workers_dev = true
[site]
bucket = "./dist"
entry-point = "workers-site"
[env.dev]
name = "name"
route = "xxx.com/*"
zone_id = "XXX"
account_id = "XXX"
The website is fine and live on "xxx.com" but when I refresh the page on any other route I get this error message:
could not find es-es/index.html in your content namespace
Or for example:
could not find category/65/index.html in your content namespace
On nginx I had to create a .htaccess, but I have no idea on how to make it work here.
This is my index.js in case it helps:
import { getAssetFromKV, mapRequestToAsset } from '#cloudflare/kv-asset-handler'
/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = false
addEventListener('fetch', event => {
try {
event.respondWith(handleEvent(event))
} catch (e) {
if (DEBUG) {
return event.respondWith(
new Response(e.message || e.toString(), {
status: 500,
}),
)
}
event.respondWith(new Response('Internal Error', { status: 500 }))
}
})
async function handleEvent(event) {
const url = new URL(event.request.url)
let options = {}
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
try {
if (DEBUG) {
// customize caching
options.cacheControl = {
bypassCache: true,
}
}
return await getAssetFromKV(event, options)
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
}
/**
* Here's one example of how to modify a request to
* remove a specific prefix, in this case `/docs` from
* the url. This can be useful if you are deploying to a
* route on a zone, or if you only want your static content
* to exist at a specific path.
*/
function handlePrefix(prefix) {
return request => {
// compute the default (e.g. / -> index.html)
let defaultAssetKey = mapRequestToAsset(request)
let url = new URL(defaultAssetKey.url)
// strip the prefix from the path for lookup
url.pathname = url.pathname.replace(prefix, '/')
// inherit all other props from the default request
return new Request(url.toString(), defaultAssetKey)
}
}

As you know, Vue.js (like many other SPA frameworks) expects that for any path that doesn't map to a specific file, the server falls back to serving the root /index.html file. Vue will then do routing in browser-side JavaScript. You mentioned that you know how to accomplish this fallback with .htaccess, but how can we do it with Workers?
Good news: In Workers, we can write code to do whatever we want!
In fact, the worker code already has a specific block of code to handle "404 not found" errors. One way to solve the problem would be to change this block of code so that instead of returning the 404 error, it returns /index.html.
The code we want to change is this part:
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
We want to change it to:
} catch (e) {
// Fall back to serving `/index.html` on errors.
return getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/index.html`, req),
})
}
That should do the trick.
However, the above solution has a slight problem: For any HTML page (other than the root), it will do two lookups, first for the specific path, and only after that will it look for /index.html as a fallback. These lookups are pretty fast, but maybe we can make things faster by being a little bit smarter and detecting HTML pages upfront based on the URL.
To do this, we want to customize the mapRequestToAsset function. You can see a hint about this in a comment in the code:
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
Let's go ahead and use it. Replace the above comment with this:
options.mapRequestToAsset = req => {
// First let's apply the default handler, which we imported from
// '#cloudflare/kv-asset-handler' at the top of the file. We do
// this because the default handler already has logic to detect
// paths that should map to HTML files, for which it appends
// `/index.html` to the path.
req = mapRequestToAsset(req)
// Now we can detect if the default handler decided to map to
// index.html in some specific directory.
if (req.url.endsWith('/index.html')) {
// Indeed. Let's change it to instead map to the root `/index.html`.
// This avoids the need to do a redundant lookup that we know will
// fail.
return new Request(`${new URL(req.url).origin}/index.html`, req)
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return req
}
}
Now the code detects specifically HTML requests and replaces them with the root /index.html, so there's no need to waste time looking up a file that doesn't exist only to catch the resulting error. For other kinds of files (images, JS, CSS, etc.) the code will not modify the filename.

There appears to be a built-way to do this now:
import { getAssetFromKV, serveSinglePageApp } from '#cloudflare/kv-asset-handler'
...
let asset = await getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp })
https://github.com/cloudflare/kv-asset-handler#servesinglepageapp

Related

Next.js: _Middleware with NextResponse blocks images from rendering

This question extends this question. The _middleware in Next.js with import { NextResponse } from "next/server"; can be used for JWT authentication but blocks all the routes including images. This means that if you have images that you want to load in the redirect route by CSS or Image, will not load. The code below blocks address bar redirect and allows image load. Access Token would probably be better
Update: after some debugging, this is what I've come up with. The previous code that I wrote does not let you be redirected to the home page after login. The reason being that the _Middleware seems to runs before /api/login and based on the prev conditional, just redirects them to the login again and returns void (_Middleware "includes" on redirect).
This updated code allows /api/login to be routed on without a refresh token and sends them back to login if they navigate through address bar without a token
import { NextResponse } from "next/server";
export default function (req: {
url?: any;
cookies?: any;
}): void | NextResponse {
const { cookies } = req;
const url: string = req.url;
const refreshToken: string | undefined = cookies?.refresh_token_extreme;
const baseUrl: string = "http://localhost:3000";
// vercel.svg is used in /login
const unprotectedPaths: string[] = [
`${baseUrl}/login`,
`${baseUrl}/favicon.ico`,
`${baseUrl}/vercel.svg`,
`${baseUrl}/_next/webpack-hmr`,
`${baseUrl}/attachables/campus-images/image1.jpg`,
`${baseUrl}/attachables/mnhs-images/logos/login_logo.png`,
`${baseUrl}/attachables/mnhs-images/logos/mnhs_favicon_og.ico`,
]
if (unprotectedPaths.includes(url)) {
return void 0;
} else if (!refreshToken && url === "http://localhost:3000/api/login") {
return NextResponse.next();
} else if (!refreshToken) {
return NextResponse.redirect(`${baseUrl}/login`);
} else {
return NextResponse.next();
}
}
Middleware will be invoked for every route in your project. The following is the execution order:
headers from next.config.js
redirects from next.config.js
Middleware (rewrites, redirects, etc.)
beforeFiles (rewrites) from next.config.js
Filesystem routes (public/, _next/static/, Pages, etc.)
afterFiles (rewrites) from next.config.js
Dynamic Routes (/blog/[slug])
fallback (rewrites) from next.config.js
There are two ways to define which paths Middleware will run on:
Custom matcher config
Conditional statements
for more informations

How can I use the AttributeRewriter to cache inline css background images using Cloudflare workers?

I'm using a worker in Cloudflare to rewrite attributes for caching
const OLD_URL = 'cdn.MYDOMAIN.com';
const NEW_URL = 'cdn-uk.MYDOMAIN.com';
class AttributeRewriter {
constructor(attributeName) {
this.attributeName = attributeName
}
element(element) {
const attribute = element.getAttribute(this.attributeName)
if (attribute) {
element.setAttribute(
this.attributeName,
attribute.replace(OLD_URL,NEW_URL),
)
}
}
}
const rewriter = new HTMLRewriter()
.on('a', new AttributeRewriter('href'))
.on('img', new AttributeRewriter('src'));
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* Respond to the request
* #param {Request} request
*/
async function handleRequest(request) {
let response = await fetch(request);
return rewriter.transform(response)
}
The problem is that I'm still seeing lots of "Non cloudflare" files using the Dr.Flare chrome extension. Inspecting these shows me that the images are being injected into inline styling in Shopify. How can I use the rewriter to ensure I catch all the images?

FirebaseUI auth redirecting before Firestore document is created

Setup: Vue.js, Vuetify, FirebaseUI, Firestore, Vue-router, Vue CLI
My expectation: Oauth would succeed, set userEmail in localStorage, create a Firestore document in the users collection, then the page would redirect.
Reality: Oath succeeds, userEmail is set in localStorage, page redirects
I have tried using async/await to no avail and returning nothing from signInSuccessWithAuthResult and using signInSuccessUrl for the redirect didn't work either. window.location.href = "/" and location.href.replace("/") also didn't change anything. If I remove the redirect, the document is created which leads me to believe the redirect interrupts the document creation. I am very new to Firebase but I don't see why this isn't working. Any help would be greatly appreciated and if you need more details please comment.
let ui = firebaseui.auth.AuthUI.getInstance();
if (!ui) {
ui = new firebaseui.auth.AuthUI(firebase.auth());
}
let uiConfig = {
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID
],
signInFlow: "popup",
callbacks: {
signInSuccessWithAuthResult: function(authResult) {
let email = authResult.user.isAnonymous
? "guest"
: authResult.user.email;
localStorage.setItem("userEmail", email);
if (
authResult.additionalUserInfo.isNewUser &&
!authResult.user.isAnonymous
) {
db.collection("users")
.add({
email: authResult.user.email,
expire: new firebase.firestore.Timestamp.now(),
positions: [0, 0, 0],
premium: false
})
.then(() => {
window.location.pathname = "/";
});
}
window.location.pathname = "/";
return false;
}
}
};
ui.start("#firebaseui-auth-container", uiConfig);
I think there are a couple of problems with the flow here, one is general with how async works and the other is specific to FirebaseUI. I'll cover both.
First let's look at the sequence in which your callback code will run:
signInSuccessWithAuthResult: function(authResult) {
// 1. BEGINS RUNNING HERE
let email = authResult.user.isAnonymous
? "guest"
: authResult.user.email;
localStorage.setItem("userEmail", email);
if (
authResult.additionalUserInfo.isNewUser &&
!authResult.user.isAnonymous
) {
// 2. THIS FUNCTION KICKS OFF BUT WILL RETURN IMMEDIATELY
db.collection("users")
.add({
email: authResult.user.email,
expire: new firebase.firestore.Timestamp.now(),
positions: [0, 0, 0],
premium: false
})
.then(() => {
// 4. THIS WOULD RUN LAST, BUT PROBABLY IS ABORTED/SKIPPED DUE TO 3
window.location.pathname = "/";
});
}
// 3. THIS REDIRECT WILL BE TRIGGERED WHILE THE .add() IS EXECUTING async
window.location.pathname = "/";
return false;
}
}
};
So to fix that, you'd need to remove the redirect at 3) and only redirect after the collection has been saved.
BUT! FirebaseUI will automatically redirects to the "success" page when the callback returns. So even if you did the above change, the FirebaseUI redirect would randomly abort the db.collection.add() and reload the page to '/success' the default.
To stop this you have to block the return of the callback, by using async/await, and remove your manual redirect and configure the success url.
const uiConfig = {
signInSuccessUrl: '/success',
callbacks: {
signInSuccessWithAuthResult: async function(authResult) {
// ...
await db.collection("users").add({ ... });
}
}
};
Adding async to the callback allows you to block and await for the db.collection.add to return. Then you shouldn't redirect manually as the signInSuccessUrl does it for you so you'd get a second race-condition.
I've simplified it here, so ensure you don't add any other non-awaited async tasks else they'd race too. (e.g. LocalStorage.setItem() is blocking, but if you used AsyncStorage.setItem() you'd have the same issue and should await that too).

Nuxt: Inside a plugin, how to add dynamic script tag to head?

I'm trying to build a Google Analytics plugin to Nuxt that will fetch tracking IDs from the CMS. I am really close I think.
I have a plugin file loading on client side only. The plugin is loaded from nuxt.config.js via the plugins:[{ src: '~/plugins/google-gtag.js', mode: 'client' }] array.
From there the main problem is that the gtag script needs the UA code in it's URL, so I can't just add that into the regular script object in nuxt.config.js. I need to get those UA codes from the store (which is hydrated form nuxtServerInit.
So I'm using head.script.push in the plugin to add the gtag script with the UA code in the URL. But that doesn't result in the script being added on first page load, but it does for all subsequent page transitions. So clearly I'm running head.script.push too late in the render of the page.
But I don't know how else to fetch tracking IDs, then add script's to the head.
// plugins/google.gtag.client.js with "mode": "client
export default ({ store, app: { head, router, context } }, inject) => {
// Remove any empty tracking codes
const codes = store.state.siteMeta.gaTrackingCodes.filter(Boolean)
// Add script tag to head
head.script.push({
src: `https://www.googletagmanager.com/gtag/js?id=${codes[0]}`,
async: true
})
console.log('added script')
// Include Google gtag code and inject it (so this.$gtag works in pages/components)
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
inject('gtag', gtag)
gtag('js', new Date())
// Add tracking codes from Vuex store
codes.forEach(code => {
gtag('config', code, {
send_page_view: false // necessary to avoid duplicated page track on first page load
})
console.log('installed code', code)
// After each router transition, log page event to Google for each code
router.afterEach(to => {
gtag('event', 'page_view', { page_path: to.fullPath })
console.log('afterEach', code)
})
})
}
I ended up getting this to work and we use it in production here.
Code as of this writing looks like this:
export default ({ store, app: { router, context } }, inject) => {
// Remove any empty tracking codes
let codes = _get(store, "state.siteMeta.gaTrackingCodes", [])
codes = codes.filter(Boolean)
// Abort if no codes
if (!codes.length) {
if (context.isDev) console.log("No Google Anlaytics tracking codes set")
inject("gtag", () => {})
return
}
// Abort if in Dev mode, but inject dummy functions so $gtag events don't throw errors
if (context.isDev) {
console.log("No Google Anlaytics tracking becuase your are in Dev mode")
inject("gtag", () => {})
return
}
// Abort if we already added script to head
let gtagScript = document.getElementById("gtag")
if (gtagScript) {
return
}
// Add script tag to head
let script = document.createElement("script")
script.async = true
script.id = "gtag"
script.src = "//www.googletagmanager.com/gtag/js"
document.head.appendChild(script)
// Include Google gtag code and inject it (so this.$gtag works in pages/components)
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
inject("gtag", gtag)
gtag("js", new Date())
// Add tracking codes from Vuex store
codes.forEach(code => {
gtag("config", code, {
send_page_view: false // Necessary to avoid duplicated page track on first page load
})
// After each router transition, log page event to Google for each code
router.afterEach(to => {
gtag("event", code, { page_path: to.fullPath })
})
})
}
If not in a plug-in, this was a good read on how to load 3rd party scripts: How to Load Third-Party Scripts in Nuxt.js

Can I set authorization headers with RequireJS?

We want to have 2 sets of resources for our AngularJS app (public/private) which uses RequireJS for dependency management. Basically everything on the login page would be public and once logged in, another angularjs app would be loaded (new requirejs config) that would load resources that require authentication to access.
Is there a way to configure requirejs to set an authorization header when loading resources?
It depends on what you mean by "resources" and how your server is configured. But in general - yes, since you are using AngularJS you can use the $httpProvider to inject an interceptor service.
For example, in a service:
var dependencies = ['$rootScope', 'userService'];
var service = function ($rootScope, userService) {
return {
request: function(config) {
var currentUser = userService.getCurrentUser();
var access_token = currentUser ? currentUser.access_token : null;
if(access_token) {
config.headers.authorization = access_token;
}
return config;
},
responseError: function (response) {
if(response.status === 401) {
$rootScope.$broadcast('unauthorized');
}
return response;
}
};
};
module.factory(name, dependencies.concat(service));
Then, after you configure your routes, you can use:
$httpProvider.interceptors.push( 'someService');
You can find some more information on interceptors here: https://docs.angularjs.org/api/ng/service/$http#interceptors
UPDATE
You might be able to use the text plugin to try and receive it, but I don't see the point in protecting client side code. Plus, if you want to use optimization the resources will just come in one file anyway...
config: {
text: {
onXhr: function (xhr, url) {
xhr.setRequestHeader('Authorization','Basic ' + token);
}
}
}
Refer to: custom-xhr-hooks
Another UPDATE
You could also use urlArgs (mainly used for cache invalidation) without using the text plugin:
require.config({
urlArgs: 'token='+token,
...
)}