How can I password protect every page in NextJS and Supabase, using the Supabase default auth helpers and UI?

huangapple go评论70阅读模式
英文:

How can I password protect every page in NextJS and Supabase, using the Supabase default auth helpers and UI?

问题

我正在尝试在我的NextJS项目中向每个页面添加用户身份验证(页面,而不是应用程序)。这个教程非常有帮助(也正是我想做的) - https://alexsidorenko.com/blog/next-js-protected-routes/ - 但我在将Supabase的默认身份验证界面和功能集成到该模型(https://supabase.com/docs/guides/auth/auth-helpers/nextjs)方面遇到了困难。

我的基本目标是将身份验证分支移到_app.tsx,而不是每个页面上:

// _app.tsx

import { useEffect, useState } from "react";
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs';
import { SessionContextProvider, useUser, useSession, useSupabaseClient, Session } from '@supabase/auth-helpers-react';
import { Auth, ThemeSupa } from '@supabase/auth-ui-react';
import { AppProps } from 'next/app';
import { UserContext } from "@components/user";

function MyApp({ Component, pageProps }: AppProps<{ initialSession: Session }>) {
  const [supabase] = useState(() => createBrowserSupabaseClient());
  const session = useSession();
  const user = useUser();

  console.log("session:" + session);
  console.log("user:" + user);

  useEffect(() => {
    if (pageProps.protected) {
      return <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />;
    }
  }, []);

  return (
    <SessionContextProvider supabaseClient={supabase} session={session} initialSession={pageProps.initialSession}>
      <Component {...pageProps} />
    </SessionContextProvider>
  );
}

export default MyApp;

我想要保护的页面(例如,首页)如下所示:

// index.tsx

import Account from "@components/account";

const Home = () => {
  return (
    <div>
      <Account session={session} />
    </div>
  );
};

export async function getStaticProps(context) {
  return {
    props: {
      protected: true,
    },
  };
}

export default Home;

然后在首页页面中包含的Account组件是Supabase的开箱即用的个人资料面板,尽管它可以是任何内容:

// @components/account.tsx

import { useState, useEffect } from 'react';
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react';
import { Database } from '@utils/database.types';
type Profiles = Database['public']['Tables']['profiles']['Row'];

export default function Account({ session }: { session: Session }) {
  const supabase = useSupabaseClient<Database>();
  const user = useUser();
  const [loading, setLoading] = useState(true);
  const [username, setUsername] = useState<Profiles['username']>(null);

  useEffect(() => {
    getProfile();
  }, [session]);

  async function getProfile() {
    try {
      setLoading(true);
      if (!user) throw new Error('No user');

      let { data, error, status } = await supabase
        .from('profiles')
        .select(`username`)
        .eq('id', user.id)
        .single();

      if (error && status !== 406) {
        throw error;
      }

      if (data) {
        setUsername(data.username);
      }
    } catch (error) {
      alert('Error loading user data!');
      console.log(error);
    } finally {
      setLoading(false);
    }
  }

  async function updateProfile({
    username,    
  }: {
    username: Profiles['username'];    
  }) {
    try {
      setLoading(true);
      if (!user) throw an Error('No user');

      const updates = {
        id: user.id,
        username,
        updated_at: new Date().toISOString(),
      };

      let { error } = await supabase.from('profiles').upsert(updates);
      if (error) throw error;
      alert('Profile updated!');
    } catch (error) {
      alert('Error updating the data!');
      console.log(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session.user.email} disabled />
      </div>
      <div>
        <label htmlFor="username">Username</label>
        <input id="username" type="text" value={username || ''} onChange={(e) => setUsername(e.target.value)} />
      </div>      
      <div>
        <button onClick={() => updateProfile({ username })} disabled={loading} >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>
      <div>
        <button onClick={() => supabase.auth.signOut()}>
          Sign Out
        </button>
      </div>
    </div>
  );
}

我认为我对受保护路由Supabase的会话和用户使用之间的关系有基本的误解。希望这可以帮助你理解如何在Next.js项目中实现用户身份验证和保护路由。

英文:

I'm trying to add user authentication to every page in my NextJS project (pages, not app.) This tutorial was very helpful (and is exactly what I want to do) - <https://alexsidorenko.com/blog/next-js-protected-routes/> - but I'm having trouble integrating Supabase's default auth UI and capabilities into that model (<https://supabase.com/docs/guides/auth/auth-helpers/nextjs>).

My basic goal is to move authentication branching into _app.tsx, rather than on each page:


// _app.tsx
import { useEffect, useState } from &quot;react&quot;;
import { createBrowserSupabaseClient } from &#39;@supabase/auth-helpers-nextjs&#39;
import { SessionContextProvider, useUser, useSession, useSupabaseClient, Session } from &#39;@supabase/auth-helpers-react&#39;
import { Auth, ThemeSupa } from &#39;@supabase/auth-ui-react&#39;
import { AppProps } from &#39;next/app&#39;
import { UserContext } from &quot;@components/user&quot;
function MyApp({Component, pageProps}: AppProps&lt;{ initialSession: Session }&gt;) {
const [supabase] = useState(() =&gt; createBrowserSupabaseClient())
const session = useSession()
const user = useUser()
console.log(&quot;session:&quot; + session);
console.log(&quot;user:&quot; + user);
useEffect(() =&gt; {
if (
pageProps.protected
) {
return &lt;Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme=&quot;dark&quot; /&gt;    
}
}, [])
return (
&lt;SessionContextProvider supabaseClient={supabase} session={session} initialSession={pageProps.initialSession}&gt;
&lt;Component {...pageProps} /&gt;
&lt;/SessionContextProvider&gt;
)
}
export default MyApp

A page I want to protect (for example, the index page) looks like this:

// index.tsx
import Account from &quot;@components/account&quot;;
const Home = () =&gt; {
return (
&lt;div&gt;
&lt;Account session={session} /&gt;
&lt;/div&gt;
)
}
export async function getStaticProps(context) {
return {
props: {
protected: true,
},
}
}
export default Home

And then the Account component that's included on the index page is the Supabase out of the box profile panel, although it could be any content:

// @components/account.tsx
import { useState, useEffect } from &#39;react&#39;
import { useUser, useSupabaseClient, Session } from &#39;@supabase/auth-helpers-react&#39;
import { Database } from &#39;@utils/database.types&#39;
type Profiles = Database[&#39;public&#39;][&#39;Tables&#39;][&#39;profiles&#39;][&#39;Row&#39;]
export default function Account({ session }: { session: Session }) {
const supabase = useSupabaseClient&lt;Database&gt;()
const user = useUser()
const [loading, setLoading] = useState(true)
const [username, setUsername] = useState&lt;Profiles[&#39;username&#39;]&gt;(null)
useEffect(() =&gt; {
getProfile()
}, [session])
async function getProfile() {
try {
setLoading(true)
if (!user) throw new Error(&#39;No user&#39;)
let { data, error, status } = await supabase
.from(&#39;profiles&#39;)
.select(`username`)
.eq(&#39;id&#39;, user.id)
.single()
if (error &amp;&amp; status !== 406) {
throw error
}
if (data) {
setUsername(data.username)
}
} catch (error) {
alert(&#39;Error loading user data!&#39;)
console.log(error)
} finally {
setLoading(false)
}
}
async function updateProfile({
username,    
}: {
username: Profiles[&#39;username&#39;]    
}) {
try {
setLoading(true)
if (!user) throw new Error(&#39;No user&#39;)
const updates = {
id: user.id,
username,
updated_at: new Date().toISOString(),
}
let { error } = await supabase.from(&#39;profiles&#39;).upsert(updates)
if (error) throw error
alert(&#39;Profile updated!&#39;)
} catch (error) {
alert(&#39;Error updating the data!&#39;)
console.log(error)
} finally {
setLoading(false)
}
}
return (
&lt;div&gt;
&lt;div&gt;
&lt;label htmlFor=&quot;email&quot;&gt;Email&lt;/label&gt;
&lt;input id=&quot;email&quot; type=&quot;text&quot; value={session.user.email} disabled /&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;label htmlFor=&quot;username&quot;&gt;Username&lt;/label&gt;
&lt;input id=&quot;username&quot; type=&quot;text&quot; value={username || &#39;&#39;} onChange={(e) =&gt; setUsername(e.target.value)} /&gt;
&lt;/div&gt;      
&lt;div&gt;
&lt;button onClick={() =&gt; updateProfile({ username })} disabled={loading} &gt;
{loading ? &#39;Loading ...&#39; : &#39;Update&#39;}
&lt;/button&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;button onClick={() =&gt; supabase.auth.signOut()}&gt;
Sign Out
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
)
}

I think I have a fundamental misunderstanding of the relationship between protected routes and Supabase's use of session and user.

Any help would be very much appreciated.

答案1

得分: 1

我建议在这种情况下使用Next.js中间件:https://supabase.com/docs/guides/auth/auth-helpers/nextjs#auth-with-nextjs-middleware

import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  // 我们需要创建一个响应并将其交给Supabase客户端,以便能够修改响应标头。
  const res = NextResponse.next()
  // 创建经过身份验证的Supabase客户端。
  const supabase = createMiddlewareSupabaseClient({ req, res })
  // 检查是否有会话
  const {
    data: { session },
  } = await supabase.auth.getSession()

  // 检查身份验证条件
  if (session?.user.email?.endsWith('@gmail.com')) {
    // 身份验证成功,将请求转发到受保护的路由。
    return res
  }

  // 未满足身份验证条件,重定向到主页。
  const redirectUrl = req.nextUrl.clone()
  redirectUrl.pathname = '/'
  redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
  return NextResponse.redirect(redirectUrl)
}

export const config = {
  matcher: '/middleware-protected/:path*',
}
英文:

I'd recommend using Next.js middleware for this: https://supabase.com/docs/guides/auth/auth-helpers/nextjs#auth-with-nextjs-middleware

import { createMiddlewareSupabaseClient } from &#39;@supabase/auth-helpers-nextjs&#39;
import { NextResponse } from &#39;next/server&#39;
import type { NextRequest } from &#39;next/server&#39;

export async function middleware(req: NextRequest) {
  // We need to create a response and hand it to the supabase client to be able to modify the response headers.
  const res = NextResponse.next()
  // Create authenticated Supabase Client.
  const supabase = createMiddlewareSupabaseClient({ req, res })
  // Check if we have a session
  const {
    data: { session },
  } = await supabase.auth.getSession()

  // Check auth condition
  if (session?.user.email?.endsWith(&#39;@gmail.com&#39;)) {
    // Authentication successful, forward request to protected route.
    return res
  }

  // Auth condition not met, redirect to home page.
  const redirectUrl = req.nextUrl.clone()
  redirectUrl.pathname = &#39;/&#39;
  redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
  return NextResponse.redirect(redirectUrl)
}

export const config = {
  matcher: &#39;/middleware-protected/:path*&#39;,
}

huangapple
  • 本文由 发表于 2023年2月19日 23:26:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/75501191.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定