From c733021784c9205220893d6407dfbdcc55c1a34c Mon Sep 17 00:00:00 2001 From: Jon Shipman Date: Wed, 10 Jan 2024 08:58:57 -0600 Subject: [PATCH] Pocketbase Auth with Realtime Data --- remix-auth-pocketbase/.eslintrc.cjs | 30 +++++ remix-auth-pocketbase/.gitignore | 6 + remix-auth-pocketbase/README.md | 49 ++++++++ remix-auth-pocketbase/app/pb.client.ts | 8 ++ remix-auth-pocketbase/app/pb.server.ts | 41 +++++++ remix-auth-pocketbase/app/root.tsx | 50 ++++++++ remix-auth-pocketbase/app/routes/_index.tsx | 37 ++++++ remix-auth-pocketbase/app/routes/admin.tsx | 60 ++++++++++ .../app/routes/forgot-password.tsx | 70 ++++++++++++ remix-auth-pocketbase/app/routes/login.tsx | 82 ++++++++++++++ remix-auth-pocketbase/app/routes/logout.tsx | 6 + remix-auth-pocketbase/app/routes/register.tsx | 107 ++++++++++++++++++ remix-auth-pocketbase/package.json | 44 +++++++ remix-auth-pocketbase/pocketbase/.gitignore | 3 + .../pocketbase/pb_hooks/realtime.pb.js | 33 ++++++ .../pb_migrations/991_init_admin.js | 20 ++++ .../pocketbase/pb_migrations/992_init_user.js | 31 +++++ .../pb_migrations/993_realtime_example.js | 47 ++++++++ remix-auth-pocketbase/public/favicon.ico | Bin 0 -> 16958 bytes remix-auth-pocketbase/remix.config.js | 8 ++ remix-auth-pocketbase/remix.env.d.ts | 12 ++ remix-auth-pocketbase/scripts/install.mjs | 67 +++++++++++ remix-auth-pocketbase/scripts/pocketbase.mjs | 37 ++++++ remix-auth-pocketbase/tsconfig.json | 22 ++++ 24 files changed, 870 insertions(+) create mode 100644 remix-auth-pocketbase/.eslintrc.cjs create mode 100644 remix-auth-pocketbase/.gitignore create mode 100644 remix-auth-pocketbase/README.md create mode 100644 remix-auth-pocketbase/app/pb.client.ts create mode 100644 remix-auth-pocketbase/app/pb.server.ts create mode 100644 remix-auth-pocketbase/app/root.tsx create mode 100644 remix-auth-pocketbase/app/routes/_index.tsx create mode 100644 remix-auth-pocketbase/app/routes/admin.tsx create mode 100644 remix-auth-pocketbase/app/routes/forgot-password.tsx create mode 100644 remix-auth-pocketbase/app/routes/login.tsx create mode 100644 remix-auth-pocketbase/app/routes/logout.tsx create mode 100644 remix-auth-pocketbase/app/routes/register.tsx create mode 100644 remix-auth-pocketbase/package.json create mode 100644 remix-auth-pocketbase/pocketbase/.gitignore create mode 100644 remix-auth-pocketbase/pocketbase/pb_hooks/realtime.pb.js create mode 100644 remix-auth-pocketbase/pocketbase/pb_migrations/991_init_admin.js create mode 100644 remix-auth-pocketbase/pocketbase/pb_migrations/992_init_user.js create mode 100644 remix-auth-pocketbase/pocketbase/pb_migrations/993_realtime_example.js create mode 100644 remix-auth-pocketbase/public/favicon.ico create mode 100644 remix-auth-pocketbase/remix.config.js create mode 100644 remix-auth-pocketbase/remix.env.d.ts create mode 100644 remix-auth-pocketbase/scripts/install.mjs create mode 100644 remix-auth-pocketbase/scripts/pocketbase.mjs create mode 100644 remix-auth-pocketbase/tsconfig.json diff --git a/remix-auth-pocketbase/.eslintrc.cjs b/remix-auth-pocketbase/.eslintrc.cjs new file mode 100644 index 00000000..287534bb --- /dev/null +++ b/remix-auth-pocketbase/.eslintrc.cjs @@ -0,0 +1,30 @@ +/* eslint-env es6 */ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: ["@remix-run/eslint-config/internal", "plugin:markdown/recommended"], + plugins: ["markdown"], + settings: { + "import/internal-regex": "^~/", + }, + ignorePatterns: ["pocketbase/**"], + rules: { + "prefer-let/prefer-let": OFF, + "prefer-const": WARN, + + "import/order": [ + ERROR, + { + alphabetize: { caseInsensitive: true, order: "asc" }, + groups: ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + }, + ], + + "react/jsx-no-leaked-render": [WARN, { validStrategies: ["ternary"] }], + }, +}; diff --git a/remix-auth-pocketbase/.gitignore b/remix-auth-pocketbase/.gitignore new file mode 100644 index 00000000..3f7bf98d --- /dev/null +++ b/remix-auth-pocketbase/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/remix-auth-pocketbase/README.md b/remix-auth-pocketbase/README.md new file mode 100644 index 00000000..5f860701 --- /dev/null +++ b/remix-auth-pocketbase/README.md @@ -0,0 +1,49 @@ +# Pocketbase example + +This is an example showing a basic integration of Remix with [Pocketbase](https://pocketbase.io/). + +## Example + +### Getting started + +First, install dependencies in both the root folder (right here) + +```bash +npm i +``` + +Then, start both the Remix and Pocketbase with + +```bash +npm run dev +``` + +### Pocketbase + +In this example, a Pocketbase instance will be downloaded to `pocketbase/`. Using the migration framework, an admin user and app user will be created. A `realtime_example` collection will be created and supported with `pocketbase/pb_hooks/realtime.pb.js` by a `cronAdd` function. __In order for the email verification and forgot-password emails to work, you will need to setup SMTP in the Pocketbase admin.__ You can also manually verify new accounts in the Pocketbase admin for testing. + +> Note that in a real app, you'd likely not have your admin password commited in a migration. This is for demo purposes only. + +#### Administration Panel + +Pocketbase's administration panel is at [http://localhost:8090/_](http://localhost:8090/_). + +
+# Credentials
+Email:    pocketbase@remix.example
+Password: Passw0rd
+
+ +### Remix + +The Remix app is at http://localhost:3000. The following routes are provided: + +- __/__ - with links to the below +- __/login__ - populated with the test user by default +- __/register__ - populated with `2+pocketbase@remix.example` by default +- __/forgot-password__ - populated with the test user's email by default +- __/admin__ - accessible only after login and count is auto updated by way of Pocketbase's Realtime API + +There are two Pocketbase files, `pb.server.ts` and `pb.client.ts`. `pb.server.ts` handles the connection to the server for the auth and setting the cookies for persistence. It can also be used in the `loader` functions to prepopulate data on the server. `pb.client.ts` creates a new Pocketbase instance for the client. It uses the cookie setup on server for authenticating. You can use the client export for `useEffect` hooks or the realtime data API. `admin.tsx` has an example of loading data on the server and the realtime API. + +You may want to implement a `Content Security Policy` as this setup requires `httpOnly: false` set on the Pocketbase cookie to share between the server and client. This demo does not cover CSP. diff --git a/remix-auth-pocketbase/app/pb.client.ts b/remix-auth-pocketbase/app/pb.client.ts new file mode 100644 index 00000000..35d3f8e3 --- /dev/null +++ b/remix-auth-pocketbase/app/pb.client.ts @@ -0,0 +1,8 @@ +import Pocketbase from "pocketbase"; + +export let pb: Pocketbase | null = null; + +if (typeof window !== "undefined") { + pb = new Pocketbase(window.ENV.POCKETBASE_URL); + pb.authStore.loadFromCookie(document.cookie); +} diff --git a/remix-auth-pocketbase/app/pb.server.ts b/remix-auth-pocketbase/app/pb.server.ts new file mode 100644 index 00000000..ff992762 --- /dev/null +++ b/remix-auth-pocketbase/app/pb.server.ts @@ -0,0 +1,41 @@ +import { redirect } from "@remix-run/node"; +import Pocketbase from "pocketbase"; + +export function getPocketbase(request?: Request) { + const pb = new Pocketbase( + process.env.POCKETBASE_URL || "http://localhost:8090", + ); + + if (request) { + pb.authStore.loadFromCookie(request.headers.get("cookie") || ""); + } else { + pb.authStore.loadFromCookie(""); + } + + return pb; +} + +export function getUser(pb: Pocketbase) { + if (pb.authStore.model) { + return structuredClone(pb.authStore.model); + } + + return null; +} + +export function createSession(redirectTo: string, pb: Pocketbase) { + return redirect(redirectTo, { + headers: { + "set-cookie": pb.authStore.exportToCookie({ + secure: redirectTo.startsWith("https:"), + httpOnly: false, + }), + }, + }); +} + +export function destroySession(pb: Pocketbase) { + pb.authStore.clear(); + + return createSession("/", pb); +} diff --git a/remix-auth-pocketbase/app/root.tsx b/remix-auth-pocketbase/app/root.tsx new file mode 100644 index 00000000..7e1c3ce7 --- /dev/null +++ b/remix-auth-pocketbase/app/root.tsx @@ -0,0 +1,50 @@ +import { cssBundleHref } from "@remix-run/css-bundle"; +import type { LinksFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from "@remix-run/react"; + +export const links: LinksFunction = () => [ + ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), +]; + +export async function loader() { + return json({ + ENV: { + POCKETBASE_URL: process.env.POCKETBASE_URL || "http://localhost:8090", + }, + }); +} + +export default function App() { + const data = useLoaderData(); + + return ( + + + + + + + + + +