Skip to content

Commit 8a7dd2a

Browse files
committed
[#15] Added helper cookie utils and refactored authStore.model definition
1 parent 4074892 commit 8a7dd2a

21 files changed

Lines changed: 1042 additions & 179 deletions

README.md

Lines changed: 222 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ Official JavaScript SDK (browser and node) for interacting with the [PocketBase
1111
- [AuthStore](#authstore)
1212
- [Auto cancellation](#auto-cancellation)
1313
- [Send hooks](#send-hooks)
14+
- [SSR integration](#ssr-integration)
15+
- [Security](#security)
1416
- [Definitions](#definitions)
1517
- [Development](#development)
1618

1719

1820
## Installation
1921

20-
#### Browser (manually via script tag)
22+
### Browser (manually via script tag)
2123

2224
```html
2325
<script src="/path/to/dist/pocketbase.umd.js"></script>
@@ -31,7 +33,7 @@ _OR if you are using ES modules:_
3133
</script>
3234
```
3335

34-
#### Node.js (via npm)
36+
### Node.js (via npm)
3537

3638
```sh
3739
npm install pocketbase --save
@@ -58,7 +60,7 @@ const PocketBase = require('pocketbase/cjs')
5860
> // npm install eventsource --save
5961
> global.EventSource = require('eventsource');
6062
> ```
61-
---
63+
6264
6365
## Usage
6466
@@ -77,7 +79,6 @@ const result = await client.records.getList('example', 1, 20, {
7779
// authenticate as regular user
7880
const userData = await client.users.authViaEmail('test@example.com', '123456');
7981
80-
8182
// or as admin
8283
const adminData = await client.admins.authViaEmail('test@example.com', '123456');
8384
@@ -88,7 +89,7 @@ const adminData = await client.admins.authViaEmail('test@example.com', '123456')
8889

8990
## Caveats
9091

91-
#### File upload
92+
### File upload
9293

9394
PocketBase Web API supports file upload via `multipart/form-data` requests,
9495
which means that to upload a file it is enough to provide a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object as body.
@@ -125,7 +126,7 @@ formData.append('title', 'Hello world!');
125126
const createdRecord = await client.Records.create('example', formData);
126127
```
127128

128-
#### Errors handling
129+
### Errors handling
129130

130131
All services return a standard [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)-based response, so the error handling is straightforward:
131132
```js
@@ -157,26 +158,32 @@ ClientResponseError {
157158
}
158159
```
159160

160-
#### AuthStore
161+
### AuthStore
161162

162163
The SDK keeps track of the authenticated token and auth model for you via the `client.authStore` instance.
163-
It has the following public members that you can use:
164+
165+
The default [`LocalAuthStore`](https://github.com/pocketbase/js-sdk/blob/master/src/stores/LocalAuthStore.ts) uses the browser's `LocalStorage` if available, otherwise - will fallback to runtime/memory (aka. on page refresh or service restart you'll have to authenticate again).
166+
167+
The default `client.authStore` extends [`BaseAuthStore`](https://github.com/pocketbase/js-sdk/blob/master/src/stores/BaseAuthStore.ts) and has the following public members that you can use:
164168

165169
```js
166-
AuthStore {
167-
// fields
168-
token: string // the authenticated token
169-
model: User|Admin|{} // the authenticated User or Admin model
170-
isValid: boolean // checks if the store has existing and unexpired token
170+
BaseAuthStore {
171+
// base fields
172+
token: string // the authenticated token
173+
model: User|Admin|null // the authenticated User or Admin model
174+
isValid: boolean // checks if the store has existing and unexpired token
171175

172-
// methods
176+
// main methods
173177
clear() // "logout" the authenticated User or Admin
174178
save(token, model) // update the store with the new auth data
179+
onChange(callback) // register a callback that will be called on store change
180+
181+
// cookie parse and serialize helpers
182+
loadFromCookie(cookieHeader, key = 'pb_auth')
183+
exportToCookie(options = {}, key = 'pb_auth')
175184
}
176185
```
177186

178-
By default the SDK initialize a [`LocalAuthStore`](https://github.com/pocketbase/js-sdk/blob/master/src/stores/LocalAuthStore.ts), which uses the browser's `LocalStorage` if available, otherwise - will fallback to runtime/memory store (aka. on page refresh or service restart you'll have to authenticate again).
179-
180187
To _"logout"_ an authenticated user or admin, you can just call `client.authStore.clear()`.
181188

182189
To _"listen"_ for changes in the auth store, you can register a new listener via `client.authStore.onChange`, eg:
@@ -194,13 +201,16 @@ If you want to create your own `AuthStore`, you can extend [`BaseAuthStore`](htt
194201
import PocketBase, { BaseAuthStore } from 'pocketbase';
195202

196203
class CustomAuthStore extends BaseAuthStore {
197-
...
204+
save(token, model) {
205+
super.save(token, model);
206+
// your custom business logic...
207+
}
198208
}
199209

200210
const client = new PocketBase('http://127.0.0.1:8090', 'en-US', CustomAuthStore());
201211
```
202212

203-
#### Auto cancellation
213+
### Auto cancellation
204214

205215
The SDK client will auto cancel duplicated pending requests for you.
206216
For example, if you have the following 3 duplicated calls, only the last one will be executed, while the first 2 will be cancelled with `ClientResponseError` error:
@@ -232,7 +242,7 @@ To manually cancel pending requests, you could use `client.cancelAllRequests()`
232242
> If you want to completelly disable the auto cancellation behavior, you could use the `client.beforeSend` hook and
233243
delete the `reqConfig.signal` property.
234244

235-
#### Send hooks
245+
### Send hooks
236246

237247
Sometimes you may want to modify the request sent data or to customize the response.
238248

@@ -268,9 +278,199 @@ To accomplish this, the SDK provides 2 function hooks:
268278
};
269279
```
270280

281+
### SSR integration
282+
283+
Unfortunately, **there is no "one size fits all" solution** because each framework handle SSR differently (_and even in a single framework there is more than one way of doing things_).
284+
285+
But in general, the idea is to use a cookie based flow:
286+
287+
1. Create a new `PocketBase` instance for each server-side request
288+
2. "Load/Feed" your `client.authStore` with data from the request cookie
289+
3. Perform your application server-side actions
290+
4. Before returning the response to the client, update the cookie with the latest `client.authStore` state
291+
292+
All [`BaseAuthStore`](https://github.com/pocketbase/js-sdk/blob/master/src/stores/BaseAuthStore.ts) instances have 2 helper methods that
293+
should make working with cookies a little bit easier:
294+
295+
```js
296+
// update the store with the parsed data from the cookie string
297+
client.authStore.loadFromCookie('pb_auth=...');
298+
299+
// exports the store data as cookie, with option to extend the default SameSite, Secure, HttpOnly, Path and Expires attributes
300+
client.authStore.exportToCookie({ httpOnly: false }); // Output: 'pb_auth=...'
301+
```
302+
303+
Below you could find several examples:
304+
305+
<details>
306+
<summary><strong>SvelteKit</strong></summary>
307+
308+
One way to integrate with SvelteKit SSR could be to create the PocketBase client in a [hook handle](https://kit.svelte.dev/docs/hooks#handle)
309+
and pass it to the other server-side actions using the `event.locals`.
310+
311+
```js
312+
// src/hooks.js
313+
import PocketBase from 'pocketbase';
314+
315+
export async function handle({ event, resolve }) {
316+
event.locals.pocketbase = new PocketBase("http://127.0.0.1:8090");
317+
318+
// load the store data from the request cookie string
319+
event.locals.pocketbase.authStore.loadFromCookie(event.request.headers.get('cookie') || '');
320+
321+
const response = await resolve(event);
322+
323+
// send back the default 'pb_auth' cookie to the client with the latest store state
324+
response.headers.set('set-cookie', event.locals.pocketbase.authStore.exportToCookie());
325+
326+
return response;
327+
}
328+
```
329+
330+
And then, in some of your server-side actions, you could directly access the previously created `event.locals.pocketbase` instance:
331+
332+
```js
333+
// src/routes/login/+server.js
334+
//
335+
// creates a `POST /login` server-side endpoint
336+
export function POST({ request, locals }) {
337+
const { email, password } = await request.json();
338+
339+
const { token, user } = await locals.pocketbase.users.authViaEmail(email, password);
340+
341+
return new Response('Success...');
342+
}
343+
```
344+
</details>
345+
346+
<details>
347+
<summary><strong>Nuxt 3</strong></summary>
348+
349+
One way to integrate with Nuxt 3 SSR could be to create the PocketBase client in a [nuxt plugin](https://v3.nuxtjs.org/guide/directory-structure/plugins)
350+
and provide it as a helper to the `nuxtApp` instance:
351+
352+
```js
353+
// plugins/pocketbase.js
354+
import PocketBase from 'pocketbase';
355+
356+
export default defineNuxtPlugin((nuxtApp) => {
357+
return {
358+
provide: {
359+
pocketbase: () => {
360+
const client = new PocketBase('http://127.0.0.1:8090');
361+
362+
// load the store data from the request cookie string
363+
client.authStore.loadFromCookie(nuxtApp.ssrContext?.event?.req?.headers?.cookie || '');
364+
365+
// send back the default 'pb_auth' cookie to the client with the latest store state
366+
client.authStore.onChange(() => {
367+
if (nuxtApp.ssrContext?.event?.res) {
368+
nuxtApp.ssrContext.event.res.setHeader('set-cookie', client.authStore.exportToCookie());
369+
}
370+
});
371+
372+
return client;
373+
}
374+
}
375+
}
376+
});
377+
```
378+
379+
And then in your component you could access it like this:
380+
381+
```html
382+
<template>
383+
<div>
384+
Show: {{ data }}
385+
</div>
386+
</template>
387+
388+
<script setup>
389+
const { data } = await useAsyncData(async (nuxtApp) => {
390+
const client = nuxtApp.$pocketbase();
391+
392+
// fetch and return all "demo" records...
393+
return await client.records.getFullList('demo');
394+
})
395+
</script>
396+
```
397+
398+
> For Nuxt 2 you could use similar approach, but instead of `nuxtApp` you could use a store state to store/create the local `PocketBase` instance.
399+
</details>
400+
401+
<details>
402+
<summary><strong>Next.js</strong></summary>
403+
404+
Next.js doesn't seem to have a central place where you can read/modify the server request and response.
405+
[There is support for middlewares](https://nextjs.org/docs/advanced-features/middleware),
406+
but they are very limited and, at the time of writing, you can't pass data from a middleware to the `getServerSideProps` functions (https://github.com/vercel/next.js/discussions/31792).
407+
408+
One way to integrate with Next.js SSR could be to create a custom `PocketBase` instance in each of your `getServerSideProps`:
409+
410+
```jsx
411+
import PocketBase, { BaseAuthStore } from 'pocketbase';
412+
413+
class NextAuthStore extends BaseAuthStore {
414+
constructor(req, res) {
415+
super();
416+
417+
this.req = req;
418+
this.res = res;
419+
420+
this.loadFromCookie(this.req?.headers?.cookie);
421+
}
422+
423+
save(token, model) {
424+
super.save(token, model);
425+
426+
this.res?.setHeader('set-cookie', this.exportToCookie());
427+
}
428+
429+
clear() {
430+
super.clear();
431+
432+
this.res?.setHeader('set-cookie', this.exportToCookie());
433+
}
434+
}
435+
436+
export async function getServerSideProps({ req, res }) {
437+
const client = new PocketBase("https://pocketbase.io");
438+
client.authStore = new NextAuthStore(req, res);
439+
440+
// fetch example records...
441+
const result = await client.records.getList("example", 1, 30);
442+
443+
return {
444+
props: {
445+
// ...
446+
},
447+
}
448+
}
449+
450+
export default function Home() {
451+
return (
452+
<div>Hello world!</div>
453+
)
454+
}
455+
```
456+
</details>
457+
458+
### Security
459+
460+
The most common frontend related vulnerability is XSS (and CSRF when dealing with cookies).
461+
Fortunately, modern browsers can detect and mitigate most of this type of attacks if [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) is provided.
462+
463+
**To prevent a malicious user or 3rd party script to steal your PocketBase auth token, it is recommended to configure a basic CSP for your application (either as `meta` tag or HTTP header).**
464+
465+
This is out of the scope of the SDK, but you could find more resources about CSP at:
466+
467+
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
468+
- https://content-security-policy.com
469+
470+
271471
## Definitions
272472

273-
#### Creating new client instance
473+
### Creating new client instance
274474

275475
```js
276476
const client = new PocketBase(
@@ -280,7 +480,7 @@ const client = new PocketBase(
280480
);
281481
```
282482

283-
#### Instance methods
483+
### Instance methods
284484

285485
> Each instance method returns the `PocketBase` instance allowing chaining.
286486

@@ -292,7 +492,7 @@ const client = new PocketBase(
292492
| `client.buildUrl(path, reqConfig = {})` | Builds a full client url by safely concatenating the provided path. |
293493

294494

295-
#### API services
495+
### API services
296496

297497
> Each service call returns a `Promise` object with the API response.
298498

0 commit comments

Comments
 (0)