Supabase Auth with Plasmic RBAC using Plasmic Codegen

This is sharing of integrating Supabase Auth using Plasmic Codegen in Plasmic. This is based on code in plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub

Supabase Auth accessible from Global Action in Plasmic Studio:

Sign In Action:

Sign Up Action:

Forgot Password Trigger: (call this first as Supabase will sent email with reset link)

Reset Password: (the action to enter new password)

Response and Error Message from Supabase Auth:

[1] npx create-plasmic-app@latest (creating new project!)
[1][0]-[ x] follow the flow. (Make sure to select NEXTJS, CODEGEN, Typescript.)

[2] Codes/ Folder to copy
[2][1] components/PasswordAuth.tsx

import React, { useState, useMemo } from "react";
import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
import { mutate } from "swr";
import { PLASMIC_AUTH_DATA_KEY } from "../utils/cache-keys";
import { GlobalActionsProvider, DataProvider } from "@plasmicapp/host";
import { useRouter } from "next/router";

const PasswordAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const supabaseClient = createPagesBrowserClient();
  const [latestResponse, setLatestResponse] = useState<any>(null);
  const [authError, setAuthError] = useState<{ error: string } | null>(null);
  const router = useRouter();

  const actions = useMemo(
    () => ({
      signIn: async (email: string, password: string, redirectUrl?: string) => {
        try {
          const { data, error } = await supabaseClient.auth.signInWithPassword({ email, password });
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(data);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Sign In Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      signUp: async (email: string, password: string, roleId?: string, redirectUrl?: string) => {
        try {
          const { data, error } = await supabaseClient.auth.signUp({ email, password });
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            const plasmicAuthDataResponse = await fetch("/api/plasmic-auth", {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ action: "signUp", roleId }),
            });

            const plasmicAuthData = await plasmicAuthDataResponse.json();
            console.log("Plasmic Auth Data:", plasmicAuthData);

            setLatestResponse(data);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Sign Up Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      signOut: async (redirectUrl?: string) => {
        try {
          const { error } = await supabaseClient.auth.signOut();
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(null);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { error };
        } catch (err: any) {
          console.error("Sign Out Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      forgotPassword: async (email: string, redirectUrl?: string) => {
        try {
          const origin = typeof window !== "undefined" ? window.location.origin : "";
          const { data, error } = await supabaseClient.auth.resetPasswordForEmail(email, {
            redirectTo: redirectUrl || `${origin}/reset-password`,
          });

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(data);
            setAuthError(null);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Forgot Password Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      updatePassword: async (newPassword: string, redirectUrl: string = "/sign-in") => {
        try {
          // Now you can safely use the extracted token without the "pkce_" prefix
          const { data, error } = await supabaseClient.auth.updateUser( {
            password: newPassword, // The new password to update
          });
      
          if (error) {
            setAuthError({ error: error.message || "unknown_error" });
          } else {
            setLatestResponse(data);
            setAuthError(null);
            router.push(redirectUrl); // Redirect to the sign-in page after successful password update
          }
      
          return { data, error };
        } catch (err: any) {
          console.error("Update Password Error:", err);
          setAuthError({ error: err.message || "unknown_error" });
          throw err;
        }
      },
    }),
    [supabaseClient, router]
  );

  return (
    <GlobalActionsProvider contextName="PasswordAuth" actions={actions}>
      <DataProvider name="supabaseAuth" data={{ latestResponse, authError }}>
        {children}
      </DataProvider>
    </GlobalActionsProvider>
  );
};

export { PasswordAuth };

[2][2] pages/api/plasmic-auth.ts

import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
import type { NextApiRequest, NextApiResponse } from "next";
import { getPlasmicAuthData } from "../../utils/plasmic-auth";

// This API endpoint is used to provide the Plasmic user in client-side code.
export default async function getPlasmicAuthDataHandler(
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> {
  try {
    const { roleId } = req.body;

    const supabaseServerClient = createPagesServerClient({ req, res });

    const plasmicAuthData = await getPlasmicAuthData(supabaseServerClient, roleId);

    res.status(200).json(plasmicAuthData);
  } catch (error) {
    console.error("Error in API handler:", error);
    res.status(500).json({ error: "Internal Server Error" });
  }
}

[2][3] pages/_app.tsx (just copy the relevant portion if you had changes made prior)

import '@/styles/globals.css'
import { PlasmicRootProvider } from "@plasmicapp/react-web";
import type { AppProps } from "next/app";
import Head from "next/head";
import { usePlasmicAuthData } from "../utils/usePlasmicAuth";

function PlasmicRootProviderWithUser(props: { children: React.ReactNode }) {
  const { isUserLoading, plasmicUser, plasmicUserToken } = usePlasmicAuthData();

  return (
    <PlasmicRootProvider
      Head={Head}
      isUserLoading={isUserLoading}
      user={plasmicUser}
      userAuthToken={plasmicUserToken}
      disableLoadingBoundary={true}
    >
      {props.children}
    </PlasmicRootProvider>
  );
}

export default function MyApp({ Component, pageProps }: AppProps) {
  if (pageProps.withoutUseAuth) {
    return (
      <PlasmicRootProvider Head={Head}>
        <Component {...pageProps} />
      </PlasmicRootProvider>
    );
  }
  return (
    <PlasmicRootProviderWithUser>
      <Component {...pageProps} />
    </PlasmicRootProviderWithUser>
  );
}

[2][4] pages/plasmic-host.tsx (copy the “registerGlobalContext(PasswordAuth…” and to import the component also)

import * as React from 'react';
import { PlasmicCanvasHost, registerGlobalContext, registerComponent } from '@plasmicapp/react-web/lib/host';
import {PasswordAuth} from '../components/PasswordAuth'

// You can register any code components that you want to use here; see
// https://docs.plasmic.app/learn/code-components-ref/
// And configure your Plasmic project to use the host url pointing at
// the /plasmic-host page of your nextjs app (for example,
// http://localhost:3000/plasmic-host).  See
// https://docs.plasmic.app/learn/app-hosting/#set-a-plasmic-project-to-use-your-app-host

// registerComponent(...)

registerGlobalContext(PasswordAuth, {
  name: "PasswordAuth",
  props: {},
  providesData: true,
  globalActions: {
    signIn: {
      parameters: [
        { name: "email", type: "string" },
        { name: "password", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    signUp: {
      parameters: [
        { name: "email", type: "string" },
        { name: "password", type: "string" },
        { name: "roleId", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    signOut: { 
      parameters:[
        { name: "redirectUrl", type: "string" },
      ],
     },
    forgotPassword: {
      parameters: [
        { name: "email", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    updatePassword: {
      parameters: [
        { name: "newPassword", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
  },
  importPath: "./components/PasswordAuth",
});

export default function PlasmicHost() {
  return <PlasmicCanvasHost />;
}

[2][5] utils folder (create utils folder in your roots and copy over all 3 files from plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub)

[2][6] .env.local (create this file in your root directory and update the following variables accordingly)
[2][6][1] NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
[2][6][2] NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
[2][6][3] PLASMIC_AUTH_SECRET=your-plasmic-auth-secret (Authentication > Settings > Custom Auth > Token)

[2][7] middleware.ts (create this in your root directory. In this code place those web pages that are limited by access to public to protect it from public access).

import { NextRequest, NextResponse } from "next/server";
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";

export default async function middleware(request: NextRequest): Promise<NextResponse> {
  // Initialize Supabase client (res is omitted in middleware context)
  const supabase = createMiddlewareClient({
    req: request,
    res: NextResponse.next(), // Provide a placeholder NextResponse
  });

  try {
    // Get the user's session from Supabase
    const {
      data: { session },
    } = await supabase.auth.getSession();

    // Check if the user is signed in
    if (!session) {
      // Redirect to the homepage if unauthenticated
      return NextResponse.redirect(new URL("/", request.url));
    }

    // Allow the request to proceed
    return NextResponse.next();
  } catch (error) {
    console.error("Middleware error:", error);
    // Redirect to an error page if something goes wrong
    return NextResponse.redirect(new URL("/error", request.url));
  }
}

// List of all secured/ private pages 
export const config = {
  matcher: ["/private-page" , "/secured-page"], // Applies to routes matching "/article"
};

[3] install dependencies
[3][1] npm install @plasmicapp/auth-api
[3][2] npm install @supabase/auth-helpers-nextjs (WARNING! Supabase has deprecated this. Use at your own risks.)
[3][3] npm i swr

[0] Reference
[0][1] New Plasmic codebase (CLI) | Learn Plasmic
[0][2] Auth integration | Learn Plasmic
[0][3] JavaScript API Reference | Supabase Docs
[0][4] Getting Started – SWR
[0][5] Password-based Auth | Supabase Docs
[0][6] plasmic/packages/auth-api at fca35655e7b175e26560ca009cb398060a6ab0d4 · plasmicapp/plasmic · GitHub
[0][7] Sign Up problem
[0][8] plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub
[0][9] Auth integration | Learn Plasmic
[10][0] File Conventions: middleware.js | Next.js

Hey @papan_digital, thanks for the step by step integration.

Could you update the post to use “```ts” instead of “>” for the code parts? This way it doesnt lose indentation.

Yes, @icaro_guerra Sir!

Every time I want to secure a page and re-route accordingly I have to manually entered it in the middleware like such

Is there a way where I can set a page as secured in the Plasmic Studio and the path is then automatically updated to the middleware?

Hi, you can add arbitrary data in the more metadata section of a page, but there is no good way to consume it in the middleware :frowning:.