From f5b0111e75c023c822afbf480de4bd81132d146b Mon Sep 17 00:00:00 2001 From: Peter O'Connor Date: Sat, 3 Aug 2024 11:18:41 +0000 Subject: [PATCH] Updates kindle clippings page --- .../cors-settings.png | Bin 0 -> 18331 bytes src/markdown/kindle-clippings.mdx | 106 +++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/markdown/images/kindle-clippings-project/cors-settings.png diff --git a/src/markdown/images/kindle-clippings-project/cors-settings.png b/src/markdown/images/kindle-clippings-project/cors-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4a0ad480c375a9e1792ab19a10c020cd1c3dadca GIT binary patch literal 18331 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 L)z4*}Q$iB}hGnhD literal 0 HcmV?d00001 diff --git a/src/markdown/kindle-clippings.mdx b/src/markdown/kindle-clippings.mdx index e322829..bca1a95 100644 --- a/src/markdown/kindle-clippings.mdx +++ b/src/markdown/kindle-clippings.mdx @@ -1,10 +1,10 @@ --- -title: "Kindle Clippings" +title: "Kindle Clippings 🆙" date: 2019-09-19 period: "September 2019" featuredImage: images/kindle-clippings-project/kindle-clippings-bg.jpg languages: "JavaScript" -tools: "React | Airtable | Wordnik | Goodreads" +tools: "React | Azure Table Storage | Wordnik | Goodreads" demo: "https://kindle-clippings.netlify.app" class: "kindle-clippings-project" static: false @@ -14,6 +14,8 @@ link: "" import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCamera } from '@fortawesome/free-solid-svg-icons' +[Updated 3rd August 2024](#update) + I love to read, and if I’m reading on my Kindle then I like to highlight my favourite passages and words to create a collection of interesting information for future reference. Unfortunately, the Kindle ecosystem is quite limited on what you can do with these ‘clippings’. You can view them on the Kindle cloud reader app, and I’ve used the Clippings.io web app which enables me to organise and tag them. However, I wanted a @@ -47,4 +49,102 @@ Then it’s a simply a case of calling the Function App’s endpoint and the boo At this current stage I’m just displaying a random clipping, the book information, and a definition if the clipping is a single word. Future features include arranging the clippings by book, navigation buttons to get the next clipping or a random clipping, retrieve more detailed book/definition information and some NLP -to retrieve sentiment and keywords to do automatic tagging. The [live app is available now hosted on Netlify](https://kindle-clippings.netlify.app). \ No newline at end of file +to retrieve sentiment and keywords to do automatic tagging. The [live app is available now hosted on Netlify](https://kindle-clippings.netlify.app). + +## Azure Table Storage 🆙 + + +The backend data store has now been migrated from Airtable to Azure Table Storage due to changes in Airtable's free tier offering. + +Azure Table Storage is a simple NoSQL data store. It is a key/value type of NoSQL data store, with a key comprising of two properties - a partition key, which groups +together related rows to aid in load balancing and query speed, and a row key, which is a unique identifier for a row in a given partition. This made it a good fit for the +data as it is structured in so much as it has a *schema* (a set of column headings), but it is just a single list of clippings, so isn't relational. The Goodreads book ID was a good +candidate for the partition key, and a unique ID was generated for the row key. + +The schemaless approach is beneficial in terms of giving you the flexibility to evolve the data structure as you develop. So no up-front database +design/ERM is required to get started. Table Storage is also built on top of Azure Storage Accounts, meaning it can +store huge amounts of data, is available over the web, and being a lot cheaper than a traditional SQL database, made it an ideal choice for this project. Although inevitably +there were a couple of gotchas. + +### Authorisation + +Use managed identities! That is the moral of the tale that is to follow. But in my haste, and foolishly thinking that using +[the storage account key](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key) would be quicker +and easier, I went down the key route. As this was just a demo project, mainly for personal consumption, I considered this a valid approach, although for +production I would definitely use managed identities. + +Storage account key authorisation isn't *that* bad, it can just be fiddly in terms of having to generate the authorisation header that you pass in the API request, which consists of these steps: + +1. [**Construct the signature string**](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#constructing-the-signature-string): This depends +on the service (Blobs, Files, Queues or Tables) and version you are targeting. For Table Storage there are two options: +Shared Key and [Shared Key Lite](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#table-service-shared-key-lite-authorization). This +just defines which properties the signature string consists of. As the lite version requires fewer properties, I chose this. It only requires two properties, the date +and the [`CanonicalizedResource`](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#shared-key-lite-and-table-service-format-for-2009-09-19-and-later): + +- The date is fairly straight-forward although it must be [formatted exactly as expected (as UTC)](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#specifying-the-date-header). +- The `CanonicalizedResource` represents the resource you are targeting, which for Table Storage consists of `//?`. In my case I was +targeting the `clippings` entity, which has this URI: `https://.table.core.windows.net/clippings()`, which equates to `//clippings()` +as the `CanonicalizedResource`. + +2. [**Sign the signature string**](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#encoding-the-signature): This can be fiddly to get right. +You have to encode the signature string using the HMAC-SHA256 algorithm and sign it with your storage account key, which **must be base-64 decoded**. The result **must be +base-64 encoded** before passing it in the authorisation header. An example JavaScript function implementing steps 1 & 2 is shown below: + +```javascript +const generateSignature = () => { + const date = new Date().toUTCString(); + const canonicalizedResource = "//clippings()"; + const stringToSign = `${date}\n${canonicalizedResource}`; + const key = ""; + const hash = CryptoJS.HmacSHA256(stringToSign, CryptoJS.enc.Base64.parse(key)); + const signature = hash.toString(CryptoJS.enc.Base64); + + return signature; +} +``` + +3. [**Specify the Authorization header**](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#specifying-the-authorization-header): This is +simply a case of passing the signature from step 2 in the `Authorization` header of your API call, which must be formatted as follows: +`[SharedKey|SharedKeyLite] :`. Using the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) it looks like this +(note you must always pass date and version headers): + +```javascript +fetch("https://.table.core.windows.net/clippings()", { + method: "GET", + headers: { + 'Authorization': `SharedKeyLite :${signature}`, + 'x-ms-version': '2021-04-10', + 'x-ms-date': new Date().toUTCString(), + 'Accept': 'application/json;odata=nometadata' + } +}) +``` + +As you can see it isn't *that* bad, but takes a bit of work to get things in place as expected; formatting, encoding, hashing and signing in just the right order. It took quite a bit +of back-and-forth before getting it working. Did I mention I should have used managed identities instead?! + +### Query Strings + +A small gotcha is in the case of +[query strings when calling the Table Storage API](https://learn.microsoft.com/en-us/rest/api/storageservices/querying-tables-and-entities#query-string-encoding). They +must be encoded and if they contain a single quote, they must be escaped by another single quote e.g. `o'clock` becomes `o''clock`. + +### Pagination + +If a query returns more than 1,000 items, or executes for longer than five seconds, or crosses the partition boundary, the [response will include continuation tokens in the +header](https://learn.microsoft.com/en-us/rest/api/storageservices/query-timeout-and-pagination) for you to use in subsequent requests to get the next set of results. + +This is simply a case of checking the response for `x-ms-continuation-NextPartitionKey` and `x-ms-continuation-NextRowKey` headers, and if present sending another request with +those values set in the query string e.g. `https://.table.core.windows.net/clippings()?NextPartitionKey=${nextPartitionKey}&NextRowKey=${nextRowKey}`. However, there +is a gotcha in cases where you are making a CORS request, which was the case here. + +I was trying to parse the response headers via `res.headers.get('x-ms-continuation-NextPartitionKey')` but they were coming back as null. When making a CORS request, +[only certain headers can be accessed for security reasons](https://web.dev/articles/introduction-to-fetch#response-types). The only headers you can access in these cases +are the standard ones such as `Content-Type`, `Last-Modified`, `Expires` etc. hence the reason the continuation tokens were null. This is not a client-side issue, but a +server-side restriction to prevent giving a client too much information which could be used in potential exploits. So you need control over the server to be able to +explicitly send additional headers in CORS requests, which we can do via the Storage Account CORS settings: + +![Azure Function Proxy Setup](images/kindle-clippings-project/cors-settings.png) +
+ Storage account CORS settings to expose the Table storage continuation tokens. +
\ No newline at end of file