tsensei / nextjs-pocketbase-starter-template

14 stars 0 forks source link

Can't seem to get createBrowserClient to work #2

Open kacperkwapisz opened 1 month ago

kacperkwapisz commented 1 month ago

Hi so, I have been struggling for the last 3-4 days trying to get everything set up. Yesterday I noticed that createBrowserClient seems not to be initiated correctly and does not have the auth store set.

This is my login function:

"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { createServerClient } from "@/utils/pocketbase";

export async function login(formData: FormData) {
  const pocketbase = createServerClient();

  // type-casting here for convenience
  // in practice, you should validate your inputs
  const data = {
    identity: formData.get("identity") as string,
    password: formData.get("password") as string,
  };

  const auth = await pocketbase
    .collection("users")
    .authWithPassword(data.identity, data.password);

  if (pocketbase.authStore.isValid) {
    const auth_cookie = pocketbase.authStore.exportToCookie({
      httpOnly: false,
    });

    // Extract the different parts of the auth_cookie
    const [pb_auth_encoded, path, expires, secure, sameSite] =
      auth_cookie.split("; ");

    // Decode the URL-encoded pb_auth
    const pb_auth = pb_auth_encoded.replace("pb_auth=", ""); // decodeURIComponent();

    // Set the cookie with the parsed values
    cookies().set({
      name: "pb_auth",
      value: pb_auth,
      path: path.replace("Path=", ""),
      expires: new Date(expires.replace("Expires=", "")),
      secure: secure === "Secure",
      sameSite: sameSite.replace("SameSite=", "").toLowerCase() as
        | "strict"
        | "lax"
        | "none",
    });
    revalidatePath("/", "layout");
    redirect("/");
  }
}

export async function signup(formData: FormData) {
  const pocketbase = createServerClient(cookies());

  // type-casting here for convenience
  // in practice, you should validate your inputs
  const data = {
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  };

  const user = await pocketbase.collection("users").create({
    email: data.email,
    password: data.password,
    passwordConfirm: data.password,
  });

  if (!user) {
    redirect("/error");
  }

  revalidatePath("/", "layout");
  redirect("/");
}

After that, I want to access the pocketbase authStore on a client component with createBrowserClient but the authStore is empty.

Would love your help here because if not, I will need to switch to something else, I really wanted to use pocketbase.

tmountain commented 3 weeks ago

I had the same issue. I fixed it as follows. This code could use some clean up, but it works.

import type { TypedPocketBase } from "@/types/pocketbase-types";
import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import { cookies } from "next/headers";
import PocketBase from "pocketbase";

export const parseCookie = (str: string) =>
    Object.fromEntries(
        str.split("; ").map((v) => v.split(/=(.*)/s).map(decodeURIComponent)),
    );

export const setAuthCookie = (pb: PocketBase) => {
    const authCookie = parseCookie(
        pb.authStore.exportToCookie({ httpOnly: false }),
    );
    cookies().set("pb_auth", authCookie.pb_auth, {
        httpOnly: false, // This is important!!!
        path: "/",
        expires: new Date(authCookie.Expires),
    });
};

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;
}

let singletonClient: TypedPocketBase | null = null;

export function createBrowserClient() {
    if (!process.env.NEXT_PUBLIC_POCKETBASE_API_URL) {
        throw new Error("Pocketbase API url not defined !");
    }

    console.log(
        "process.env.NEXT_PUBLIC_POCKETBASE_API_URL",
        process.env.NEXT_PUBLIC_POCKETBASE_API_URL,
    );

    const cookie = parseCookie(document.cookie).pb_auth;

    if (!cookie) {
        throw new Error("Auth cookie not found");
    }

    const createNewClient = () => {
        const client = new PocketBase(
            process.env.NEXT_PUBLIC_POCKETBASE_API_URL,
        ) as TypedPocketBase;
        client.authStore.loadFromCookie(`pb_auth=${cookie}`);
        return client;
    };

    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;
}
tmountain commented 3 weeks ago

Also, set the cookie on login.

./login/page.tsx

import { login } from "./page.action";
import classes from "./page.module.css";

export default function Page() {
    return (
        <main className={classes.main}>
            Login form
            <form className={classes.form} action={login}>
                <label className={classes.label}>
                    E-mail
                    <input name="email" type="email" />
                </label>
                <label className={classes.label}>
                    Password
                    <input name="password" type="password" />
                </label>
                <button type="submit">Login</button>
            </form>
        </main>
    );
}

./login/page.action.ts

"use server";

import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { setAuthCookie } from "@/utils/pocketbase";
import PocketBase from "pocketbase";

export async function login(formData: FormData) {
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_API_URL);
    const authData = await pb.admins.authWithPassword(email, password);
    const isValid = pb.authStore.isValid;
    console.log(`is valid: ${isValid}`);

    if (!isValid) {
        redirect("/?error=invalid-credentials");
        return;
    }

    setAuthCookie(pb);
    redirect("/dashboard");
}

export async function logout() {
    const pb = new PocketBase(process.env.POCKETBASE_URL);
    pb.authStore.clear();
    redirect("/");
}

./login/page.module.css

.main {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
  }

  .form {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 300px;
  }

  .label {
    display: flex;
    flex-direction: column;
    gap: 5px;
  }