-
Notifications
You must be signed in to change notification settings - Fork 133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can't set up properly Next.js's SSR #69
Comments
This seems like a common misunderstanding but I'm not sure how to explain it better. First you need to clarify how do you plan to interact with the SDK:
I don't understand this. When the SDK is instantieted for example in getServerSideProps you don't have access anymore to the LocalStorage. Do you mean that you use the SDK both in the client and server? If that's the case then you'll have to change the
I'm not sure what this cookie is for. Is this some nextjs cookie or some other custom cookie? In any case, you need to specify where your code is running (server, client or both). The |
First of all, I apologize if I wasn't clear enough. I was trying to run Pocketbase queries server-side for better performance, but it didn't work. The The thing is that I can't understand how does the cookie that |
The default cookie entry should look something like this:
(the expires date should be the the same as the token "exp" claim)
If you have a localStorage entry, this means that you are currently handling the OAuth2 redirect in the browser. Or in other words, the PocketBase instance is running client-side. If you want to have a mixed SDK access (aka. making requests both client-side and server-side) then you'll have to export the cookie with import PocketBase from 'pocketbase';
const client = new PocketBase("http://127.0.0.1:8090");
client.authStore.loadFromCookie(document.cookie);
client.authStore.onChange(() => {
document.cookie = client.authStore.exportToCookie({ httpOnly: false });
})
export default client; (the above will always update the document.cookie that will be added automatically to every request from the browser to the node-server) Could you provide a code sample/repo of what you are trying to do? |
Well, I added the code you wrote and simply worked as I was expecting, being able to work client-side as well as server-side. My final code looks something like this: // lib/server.ts
import Pocketbase from 'pocketbase'
const client = new Pocketbase(process.env.NEXT_PUBLIC_POCKETBASE)
typeof document !== 'undefined' && client.authStore.loadFromCookie(document.cookie)
client.authStore.onChange(() => {
document.cookie = client.authStore.exportToCookie({ httpOnly: false })
})
export default client A final question on this because I think it's solved, it's better to have an unique PocketBase instance or to instantiate it at every request? |
I think there is some misunderstanding. The above will work only in a browser context because But if you want to make requests from the node-server you'll need to read and set the cookie from the I understand that modern frameworks blur the line between client and server but please make sure that your code is executed where you expect it to avoid accidentally leaking sensitive information. I still haven't got the time to explore the new api of nextjs13 and sometime after the v0.8.0 release I'll try to test it and will add a SSR example for it in the readme. |
I need your NextJS 13 SSR example so much! I'm using NextJS 13 with PocketBase. I'm very confused with how to use PocketBase for user authentication in client components and then fetching user's data in server components. In my project, I didn't get and set cookies (I don't know how to do that), and I'm not able to get user's data after user login. It is as if the PocketBase client in the server component that performs data fetching is never aware of the PocketBase client in the client component that performs user authentication. |
Note: according to https://beta.nextjs.org/docs/api-reference/cookies, "the I'm not sure if this implies that currently we are not able to set cookies when making requests from the server. |
In my case, I was working with Next.js 12 (mostly it was something I knew better) so I can't give you a precise example. What I can say to you is that, while I was working on it, Next didn't really append any cookie to the requests, I read someone said that it was because of the localhost but I didn't investigate further on that. After Gani's reply, I was able to work with the cookie as it was being exported to the server side when it changed. So maybe you can try to append that onChange method to your Pocketbase's instance and then using the exported model for your queries, but I really don't know how Pocketbase works in Next.js 13 |
Thank you for your reply! I tried a little bit and it seems to solve my current problem, which is to get data on the server side after user login. Following Gani's suggestion, I create a new PocketBase client instance, get the cookie using the // app/something/page.tsx. This is a server component.
import PocketBase from 'pocketbase';
import { cookies } from 'next/headers';
export default async function Home() {
const client = new PocketBase('http://127.0.0.1:8090');
const nextCookies = cookies();
const cookie_object = nextCookies.get('pb_auth');
const cookie_string = cookie_object.name + '=' + cookie_object.value;
client.authStore.loadFromCookie(cookie_string);
const data = await client.records.getList('collection_name', 1, 100);
return (
<div>
<h1>Results</h1>
<div>
{data.items.map(item => {
return <SomeComponent key={item.id} item={item}/>;
})}
</div>
</div>
)
} I'm quite new to web development and not very familiar with JS/TS, so maybe there are better ways to write the code. |
Hey! I didn't know about |
So what I did for my solution is the following...
So the above helps me on the server and then for the client I have the following.
|
I've managed to get it working in the new
|
Also note that currently there's no way to send headers to the client inside a Server Component, it's WIP atm. (This means that refreshing the cookie everytime the store updates is not possible) |
I'm guessing as long as we're not doing anything to the |
Also would add the cookie to |
@samit43 I'm not sure what are your concern. If you are not using the SDK server side then the token will be stored in the browser's LocalStorage. For SSR you'll need a cookie flow if you want to "persist" the auth state between requests. It is safe as long as your cookie is set with |
Oh, thank you! Sorry, I missed the |
thx @rafifos Your solution works great. The nextjs 13 is a hard one to play with....in comparison SvelteKit is such a better expirence.... |
@rafifos solution is great, but what if the user data changed like the username, and on the user browser the cookie still the same with the old username, this means that the server and the browser will have incorrect user data with an old username. |
What's about using route handlers (https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to get and set cookies? My following code is not working, because
|
This is how I use it with app router I have;
Remember to middleware.tsimport { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import PocketBase from "pocketbase";
import { env } from "@/env.mjs";
export async function middleware(request: NextRequest) {
const cookie = request.cookies.get("pb_auth")?.value;
const response = NextResponse.next();
const pocketbase = new PocketBase(env.NEXT_PUBLIC_POCKET_BASE_URL);
if (cookie) {
const pocketbaseModel = JSON.parse(cookie);
pocketbase.authStore.save(pocketbaseModel.token, pocketbaseModel.model);
}
try {
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
pocketbase.authStore.isValid &&
(await pocketbase.collection("users").authRefresh());
} catch (err) {
// clear the auth store on failed refresh
pocketbase.authStore.clear();
// clear the cookie on failed refresh
response.headers.set(
"set-cookie",
pocketbase.authStore.exportToCookie({ httpOnly: false })
);
}
if (
!pocketbase.authStore.model &&
!request.nextUrl.pathname.startsWith("/auth")
) {
const redirectTo = new URL("/auth/signin", request.url);
// add the current path as a query param to the signin URL
redirectTo.search = new URLSearchParams({
next: request.nextUrl.pathname,
}).toString();
return NextResponse.redirect(redirectTo);
}
if (
pocketbase.authStore.model &&
request.nextUrl.pathname.startsWith("/auth")
) {
return NextResponse.redirect(new URL(request.nextUrl.origin));
}
return response;
}
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
*/
"/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)",
],
}; actions.tssignin"use server";
import { cookies } from "next/headers";
import { getTokenPayload } from "pocketbase";
import { pocketbase } from "@/lib/db/pocketbase";
import { UserSignupSchemaType } from "@/lib/validations/auth";
export const signIn = async (data: any) => {
try {
const user = await pocketbase
.collection("users")
.authWithPassword(data.email, data.password);
const payload = getTokenPayload(pocketbase.authStore.token);
if (user) {
cookies().set(
"pb_auth",
JSON.stringify({ token: pocketbase.authStore.token }),
{
path: "/",
secure: false,
httpOnly: true,
sameSite: "strict",
expires: new Date(payload.exp * 1000),
}
);
}
return JSON.parse(JSON.stringify(user));
} catch (error) {
return JSON.parse(JSON.stringify(error));
}
}; get user"use server";
import { cookies } from "next/headers";
import { getTokenPayload } from "pocketbase";
import { pocketbase } from "@/lib/db/pocketbase";
import { UserSignupSchemaType } from "@/lib/validations/auth";
export const getUser = async () => {
try {
const authCookie = cookies().get("pb_auth");
if (authCookie) {
pocketbase.authStore.loadFromCookie(
`${authCookie.name}=${authCookie.value}`
);
pocketbase.authStore.isValid &&
(await pocketbase.collection("users").authRefresh());
}
const user = pocketbase.authStore.model;
return JSON.parse(JSON.stringify(user));
} catch (error) {
return JSON.parse(JSON.stringify(error));
}
}; fetchers.tsget dataimport { unstable_cache } from "next/cache";
import { pocketbase } from "@/lib/db/pocketbase";
export async function getPosts() {
return await unstable_cache(
async () =>
await pocketbase.collection("posts").getFullList({
sort: "-created",
}),
[`posts`],
{
revalidate: 900,
tags: [`posts`],
}
)();
}
Planning to release a pocketbase version of my paid nextjs starter kit Next Edge Starter soon |
I was able to simplify what I shared above after checking supabase's auth helpers implementation for nextjs. Basically separated initPocketBaseClientShould be used whenever we interact with pocketbase on client sideimport PocketBase from "pocketbase";
import { env } from "@/env.mjs";
export async function initPocketBaseClient() {
const pocketbase: PocketBase = new PocketBase(
env.NEXT_PUBLIC_POCKET_BASE_URL
);
pocketbase.authStore.loadFromCookie(document.cookie);
pocketbase.authStore.onChange(() => {
document.cookie = pocketbase.authStore.exportToCookie({
httpOnly: false,
});
});
return pocketbase;
} initPocketBaseServerimport { cookies } from "next/headers";
import { NextResponse } from "next/server";
import PocketBase from "pocketbase";
import { env } from "@/env.mjs";
export async function initPocketBaseServer() {
const pocketbase: PocketBase = new PocketBase(
env.NEXT_PUBLIC_POCKET_BASE_URL
);
let response = NextResponse.next();
const authCookie = cookies().get("pb_auth");
if (authCookie) {
pocketbase.authStore.loadFromCookie(
`${authCookie.name}=${authCookie.value}`
);
try {
if (pocketbase.authStore.isValid) {
await pocketbase.collection("users").authRefresh();
}
} catch (error) {
pocketbase.authStore.clear();
}
}
pocketbase.authStore.onChange(() => {
response?.headers.set("set-cookie", pocketbase.authStore.exportToCookie());
});
return pocketbase;
} Usagemiddleware.tsimport { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { initPocketBaseServer } from "@/lib/pocketbase/server";
export async function middleware(request: NextRequest) {
const pocketbase = await initPocketBaseServer();
if (
!pocketbase.authStore.model &&
!request.nextUrl.pathname.startsWith("/auth")
) {
return NextResponse.redirect(new URL("/auth/signin", request.url));
}
if (
pocketbase.authStore.model &&
request.nextUrl.pathname.startsWith("/auth")
) {
return NextResponse.redirect(new URL(request.nextUrl.origin));
}
}
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
*/
"/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)",
],
}; serverAction.tsexport const updateUser = async (id: any, data: any) => {
const pocketbase = await initPocketBaseServer();
try {
const result = await pocketbase.collection("users").update(id, data);
return JSON.parse(JSON.stringify(result));
} catch (error) {
return JSON.parse(JSON.stringify(error));
}
}; page.tsxexport default async function Home() {
const pocketbase = await initPocketBaseServer();
const userPosts = await pocketbase
.collection("posts")
.getFullList<PostsRecord>();
return (<div>{userPosts.map((post) => {return <p>{post.title}</p>})}</div>);
} OAuthButton.tsx (Client side)"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { initPocketBaseClient } from "@/lib/pocketbase/client";
import { Button } from "@/components/ui/button";
interface OauthButtonProps {
provider: "github" | "google";
icon: React.ReactNode;
text: string;
isLoading: boolean;
isDisabled: boolean;
setLoading: (state: boolean) => void;
}
export const OauthButton = ({
provider,
icon,
isDisabled,
setLoading,
isLoading,
}: OauthButtonProps) => {
const router = useRouter();
useEffect(() => {
return () => setLoading?.(false);
}, [setLoading]);
return (
<Button
type="button"
variant="outline"
isLoading={isLoading}
disabled={isDisabled}
onClick={async () => {
const pocketbase = await initPocketBaseClient();
setLoading(true);
await pocketbase
.collection("users")
.authWithOAuth2({ provider: "github" });
router.refresh();
}}
>
<div className="hover:text-gray-12 flex items-center pr-3">{icon}</div>
<span className="text-gray-12">
Sign in with {provider.charAt(0).toUpperCase() + provider.slice(1)}
</span>
</Button>
);
}; |
@orenaksakal thank's a lot for sharing your code, i gonna try this in my application. One question, why do you |
It is optional but i find it useful to redirect user to dashboard if user is already authenticated and trying to reach auth pages |
@orenaksakal it seems, that
I think this happen on line I read to docs again and can't find any reasons why this error occurs. Do you have any idea? |
No idea TBH, but it works for me on both local and vercel domain one thing I never tried is logging in as admin |
@orenaksakal if I used initPocketBaseServer in multi rsc inside a page, is it not bad? because pocketbase.collection("users").authRefresh() will execute multi times |
I think it is the same case when you use sveltekit's handle server hook see it will be executed on each request but incase its something to be prevented one way to do it would be having 3 instances middleware, server, client where middleware handles auth refreshes only and other two is for environment specific work Note: I have been looking at pocketbase only for couple of days its likely there is huge room for improvement on what I shared |
I want to keep pocketbase completely on the server side but I cannot find a way to update the cookie from there, your solution with the response did not work for me. |
I managed using the Next.js "use server";
import PocketBase from "pocketbase";
import { cookies } from "next/headers";
import { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies";
export interface PocketbaseLogin {
token?: string;
name?: string;
email?: string;
isAdmin?: boolean;
error?: { message: string };
}
const pb = new PocketBase(process.env.PB_URL);
const loginPb = async ({ email, password }: { email: string; password: string }) => {
let response: PocketbaseLogin = {};
try {
const authData = await pb.collection("users").authWithPassword(email, password);
if (authData) {
const record = authData.record;
console.log(`User ${record.name} authenticated`);
// set the client cookie
cookies().set("pb_auth", pb.authStore.exportToCookie());
response.token = authData.token;
response.name = record.name;
response.email = record.email;
response.isAdmin = record.isAdmin;
}
} catch (err: any) {
console.log(`${email} ${err.message}`);
response.error = err.response;
} |
Here is what I have done to fix this for Next 13 In src/app/api/login/route.tsThis is the export async function POST(request: Request, { params }: any) {
const body = await request.json();
const { username, password } = body;
const result = await pb.collection('users').authWithPassword(username, password);
if (!result?.token) {
return new Response(
JSON.stringify({error: 'Invalid credentials'}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
},
},
);
}
cookies().set("pb_auth", pb.authStore.exportToCookie());
return NextResponse.json(result);
} In src/app/layout.tsximport { use } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
const getUserDetails = async () => {
try {
const cookieStore = cookies();
const cookie = cookieStore.get('pb_auth');
pb.authStore.loadFromCookie(cookie?.value || '');
return pb.authStore.isValid ? pb.authStore.model : null;
} catch (err) {
console.log('getUserDetails error:', err);
return null;
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const userDetails = use(getUserDetails());
if (!userDetails) {
redirect('/login');
}
return (
<html lang="en">
<body>
{children}
</body>
</html>
)
} In src/middleware.tsexport async function middleware(request: NextRequest) {
const cookie = request.cookies.get('pb_auth');
pb.authStore.loadFromCookie(cookie?.value || '');
if (!pb.authStore.isValid) {
return NextResponse.redirect(new URL('/login', request.url))
}
NextResponse.next();
} |
Hey, I have written functions returning PocketBase instances for both Server and Client components. I have done it in a style For client components : This instantiates a PocketBase instance using the singleton pattern, so you can call it anywhere you want. It also syncs the cookie with localStorage, thus seamlessly responding to auth state change createBrowserClientimport { TypedPocketBase } from "@/types/pocketbase-types";
import PocketBase from "pocketbase";
let singletonClient: TypedPocketBase | null = null;
export function createBrowserClient() {
if (!process.env.NEXT_PUBLIC_POCKETBASE_API_URL) {
throw new Error("Pocketbase API url not defined !");
}
const createNewClient = () => {
return new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_API_URL
) as TypedPocketBase;
};
const _singletonClient = singletonClient ?? createNewClient();
if (typeof window === "undefined") return _singletonClient;
if (!singletonClient) singletonClient = _singletonClient;
singletonClient.authStore.onChange(() => {
document.cookie = singletonClient!.authStore.exportToCookie({
httpOnly: false,
});
});
return singletonClient;
} For server components : This instantiates a PocketBase instance for each call, you can pass it a cookieStore, in which case you will get the authStore instantiated. For static routes, you can use it without passing any cookieStore. createServerClientimport { TypedPocketBase } from "@/types/pocketbase-types";
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import PocketBase from "pocketbase";
export function createServerClient(cookieStore?: ReadonlyRequestCookies) {
if (!process.env.NEXT_PUBLIC_POCKETBASE_API_URL) {
throw new Error("Pocketbase API url not defined !");
}
if (typeof window !== "undefined") {
throw new Error(
"This method is only supposed to call from the Server environment"
);
}
const client = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_API_URL
) as TypedPocketBase;
if (cookieStore) {
const authCookie = cookieStore.get("pb_auth");
if (authCookie) {
client.authStore.loadFromCookie(`${authCookie.name}=${authCookie.value}`);
}
}
return client;
} Middleware : You can have a middleware.tsimport { cookies } from "next/headers";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createServerClient } from "./lib/pocketbase";
// For protected pages
// If auth is not valid for matching routes
// Redirect to a redirect path
export function middleware(request: NextRequest) {
const redirect_path = "http://localhost:3000/login";
const cookieStore = cookies();
const { authStore } = createServerClient(cookieStore);
if (!authStore.isValid) {
return NextResponse.redirect(redirect_path);
}
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - login (login route)
* - / (root path)
*/
"/((?!api|_next/static|_next/image|favicon.ico|login|$).*)",
],
}; Note :
|
This is really amazing work @tsensei! For the middleware.ts, |
I am curious about PocketBase. After reading your solution, I do have a question. In this case, I want to understand how PocketBase cookie from authStore.exportToCookie() sign the domain? How does it work actually with this solution ? I really wish to learn that part a little more so the I can secure route from server side. Marry Christmas to you! |
@KweeBoss, as you need the cookie accessible to the root domain, you can just export it with the root domain and it should be accessible too all subdomains if I recall correctly. |
Thank you so much @tsensei 🙏. I have found it. |
Hi I have issues with request not having Authorization header added to PB request from server. I have this setup for the pocketbase server client on my server actions async function initPocketbase() {
const pb = new PocketBase("http://127.0.0.1:8090");
// load the store data from the request cookie string
const authCookie = cookies().get("pb_auth");
if(authCookie) {
pb.authStore.loadFromCookie(`${authCookie.name}=${authCookie.value}`);
console.log("Should be here", pb.authStore.token)
try {
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
pb.authStore.isValid && (await pb.admins.authRefresh());
} catch (_) {
// clear the auth store on failed refresh
pb.authStore.clear();
redirect("/login");
}
}
// send back the default 'pb_auth' cookie to the client with the latest store state
pb.authStore.onChange((token, model) => {
console.log("valid", pb.authStore.isValid)
console.log("token", token);
cookies().set("pb_auth", pb.authStore.token, {
httpOnly: true
});
});
return pb;
}
export default initPocketbase; Cookie is available and has this I tried with both admin auth and user auth. The components that uses the code above lookes like this export async function getOrders() {
const pb = await initPocketbase()
const records = await pb.collection("orders").getFullList({
sort: "-created",
});
return records;
} The orders endpoint for now only allows admins, but in the PB logs all request are done as quest. |
@bentrynning Are you sure that Note you don't have to use In any case, I'm closing this issue as it is starting to become too off-topic and there isn't any clear actionable item here. I've tried to check if anything has changed in Next.js couple months ago and unfortunately I wasn't able to find an easy solution for sharing the SDK AuthStore state between server components and I'm reaching a point where I'd rather not recommend using PocketBase with Next.js as it feels too much work for very little benefit, at least to me. If someone has a suggestion without having to maintain special Next.js helpers, feel free to open a PR with an example for the README. |
ganigeorgiev Thanks for the reply, and sorry for adding just another of topic question to this thread. Next only gives you the cookie name and cookie value trough the cookie api. So it is not a serialized cookie string. But just saw all the other examples and looked like it was working for them with just a cookieName=cookieValue (token) string. I'll try to set the token directly in the store then! Thanks for the tip. By the way thanks for the tremendous nice work you have done with PocketBase <3 very cool project! |
I've followed the examples displayed in the README, as well as looked for solutions in other frameworks but I simply can't fix this problem.
Doing a little bit of debug and research, I can come to the conclusion that the cookie contained by the request may be not what the
authStore
expects, because, according to the examples, the keypb_auth=
is set as the default, while my cookie has the following structure:Also, if it has anything to do with this, the token stored in localStorage it's not equal to the one served on the cookie.
As extra information, I leave the code I wrote.
Versions: pocketbase v0.7.10; pocketbase (sdk) v0.7.4
The text was updated successfully, but these errors were encountered: