-
Notifications
You must be signed in to change notification settings - Fork 236
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7c087e4
commit c733021
Showing
24 changed files
with
870 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] }], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
/public/build | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/_). | ||
|
||
<pre> | ||
# Credentials | ||
Email: <strong>[email protected]</strong> | ||
Password: <strong>Passw0rd</strong> | ||
</pre> | ||
|
||
### 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 `[email protected]` 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof loader>(); | ||
|
||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
<Outlet /> | ||
<script | ||
dangerouslySetInnerHTML={{ | ||
__html: `window.ENV = ${JSON.stringify(data.ENV)}`, | ||
}} | ||
/> | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
<LiveReload /> | ||
</body> | ||
</html> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; | ||
import { json } from "@remix-run/node"; | ||
import { Link, useLoaderData } from "@remix-run/react"; | ||
|
||
import { getPocketbase, getUser } from "~/pb.server"; | ||
|
||
export const meta: MetaFunction = () => { | ||
return [ | ||
{ title: "New Remix App" }, | ||
{ name: "description", content: "Welcome to Remix!" }, | ||
]; | ||
}; | ||
|
||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const pb = getPocketbase(request); | ||
const user = getUser(pb); | ||
|
||
return json({ user }); | ||
} | ||
|
||
export default function Index() { | ||
const data = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<div style={{ display: "flex", gap: "1rem" }}> | ||
{data.user ? ( | ||
<Link to="/logout">Logout</Link> | ||
) : ( | ||
<> | ||
<Link to="/login">Login</Link> | ||
<Link to="/register">Register</Link> | ||
<Link to="/forgot-password">Forgot Password</Link> | ||
</> | ||
)} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { LoaderFunctionArgs } from "@remix-run/node"; | ||
import { json } from "@remix-run/node"; | ||
import { Link, useLoaderData } from "@remix-run/react"; | ||
import { useEffect, useState } from "react"; | ||
|
||
import { pb } from "~/pb.client"; | ||
import { createSession, getPocketbase, getUser } from "~/pb.server"; | ||
|
||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const pb = getPocketbase(request); | ||
const user = getUser(pb); | ||
|
||
const redirectUrl = "/admin"; | ||
|
||
if (!user) { | ||
return createSession("/", pb); | ||
} | ||
|
||
let realtime_example = null; | ||
|
||
try { | ||
realtime_example = await pb.collection("realtime_example").getFullList(); | ||
} catch (_) {} | ||
|
||
return json({ redirectUrl, user, realtime_example }); | ||
} | ||
|
||
export default function Admin() { | ||
const loaderData = useLoaderData<typeof loader>(); | ||
const [count, setCount] = useState( | ||
loaderData.realtime_example?.[0]?.count || 0, | ||
); | ||
|
||
useEffect(() => { | ||
pb?.collection("realtime_example").subscribe("*", (data) => { | ||
setCount(data.record.count); | ||
}); | ||
|
||
return () => { | ||
pb?.collection("realtime_example").unsubscribe("*"); | ||
}; | ||
}, [setCount]); | ||
|
||
return ( | ||
<div> | ||
<div>Hello {loaderData.user.name || loaderData.user.email}</div> | ||
<div style={{ display: "flex", gap: "1rem", margin: "1rem 0" }}> | ||
<Link to="/logout" reloadDocument> | ||
Logout | ||
</Link> | ||
|
||
<Link to="/">Home</Link> | ||
</div> | ||
|
||
<div> | ||
Realtime Data Demo: <span>{count}</span> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; | ||
import { json } from "@remix-run/node"; | ||
import { Form, Link, useActionData } from "@remix-run/react"; | ||
import { ClientResponseError } from "pocketbase"; | ||
|
||
import { createSession, getPocketbase, getUser } from "~/pb.server"; | ||
|
||
interface ForgotPasswordRequestData { | ||
email: string; | ||
} | ||
|
||
export async function action({ request }: ActionFunctionArgs) { | ||
const pb = getPocketbase(request); | ||
|
||
const result = (await request.formData()) as unknown as Iterable< | ||
[ForgotPasswordRequestData, FormDataEntryValue] | ||
>; | ||
const data: ForgotPasswordRequestData = Object.fromEntries(result); | ||
|
||
try { | ||
await pb.collection("users").requestPasswordReset(data.email); | ||
|
||
return json({ | ||
success: true, | ||
error: false, | ||
message: "An email has been sent to reset your password!", | ||
}); | ||
} catch (error) { | ||
if (error instanceof ClientResponseError) { | ||
return json({ success: false, error: true, message: error.message }); | ||
} | ||
} | ||
} | ||
|
||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const pb = getPocketbase(request); | ||
const user = getUser(pb); | ||
|
||
const redirectUrl = "/admin"; | ||
|
||
if (user) return createSession(redirectUrl, pb); | ||
|
||
return json({ redirectUrl, user }); | ||
} | ||
|
||
export default function Login() { | ||
const actionData = useActionData<typeof action>(); | ||
|
||
return ( | ||
<Form method="post"> | ||
{actionData?.error ? <div>{actionData.message}</div> : null} | ||
{actionData?.success ? ( | ||
<div style={{ color: "green" }}>{actionData.message}</div> | ||
) : null} | ||
<div> | ||
<label htmlFor="email">Email</label> | ||
<input | ||
type="email" | ||
name="email" | ||
id="email" | ||
defaultValue="[email protected]" | ||
/> | ||
</div> | ||
|
||
<button>Forgot Password</button> | ||
|
||
<Link to="/login">Login</Link> | ||
</Form> | ||
); | ||
} |
Oops, something went wrong.