{"id":52575,"date":"2024-12-03T10:04:43","date_gmt":"2024-12-03T02:04:43","guid":{"rendered":"https:\/\/fwq.ai\/blog\/52575\/"},"modified":"2024-12-03T10:04:43","modified_gmt":"2024-12-03T02:04:43","slug":"%e5%9c%a8-nextjs-app-router-%e4%b8%ad%e4%bd%bf%e7%94%a8-authjs-%e8%bf%9b%e8%a1%8c%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd%e9%aa%8c%e8%af%81","status":"publish","type":"post","link":"https:\/\/fwq.ai\/blog\/52575\/","title":{"rendered":"\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1"},"content":{"rendered":"<p><b><\/b>     <\/p>\n<h1>\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1<\/h1>\n<p><span style=\"cursor: pointer\"><i><\/i>\u6536\u85cf<\/span>    <\/p>\n<p>\u7c73\u4e91\u4eca\u5929\u5c06\u7ed9\u5927\u5bb6\u5e26\u6765<span style=\"color: #FF6600;, Helvetica, Arial, sans-serif;font-size: 14px;background-color: #FFFFFF\">\u300a\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u300b<\/span>\uff0c\u611f\u5174\u8da3\u7684\u670b\u53cb\u8bf7\u7ee7\u7eed\u770b\u4e0b\u53bb\u5427\uff01\u4ee5\u4e0b\u5185\u5bb9\u5c06\u4f1a\u6d89\u53ca\u5230<span style=\"color: #FF6600;, Helvetica, Arial, sans-serif;font-size: 14px;background-color: #FFFFFF\"><\/span>\u7b49\u7b49\u77e5\u8bc6\u70b9\uff0c\u5982\u679c\u4f60\u662f\u6b63\u5728\u5b66\u4e60<span style=\"color: #FF6600;, Helvetica, Arial, sans-serif;font-size: 14px;background-color: #FFFFFF\">\u6587\u7ae0<\/span>\u6216\u8005\u5df2\u7ecf\u662f\u5927\u4f6c\u7ea7\u522b\u4e86\uff0c\u90fd\u975e\u5e38\u6b22\u8fce\u4e5f\u5e0c\u671b\u5927\u5bb6\u90fd\u80fd\u7ed9\u6211\u5efa\u8bae\u8bc4\u8bba\u54c8~\u5e0c\u671b\u80fd\u5e2e\u52a9\u5230\u5927\u5bb6\uff01<\/p>\n<h2> \u76ee\u5f55 <\/h2>\n<h3> \u521d\u59cb\u8bbe\u7f6e <\/h3>\n<ul>\n<li>\u5b89\u88c5<\/li>\n<li> \u914d\u7f6e\n<ul>\n<li>nextauthconfig \u8bbe\u7f6e<\/li>\n<li>\u8def\u7531\u5904\u7406\u7a0b\u5e8f\u8bbe\u7f6e<\/li>\n<li>\u4e2d\u95f4\u4ef6<\/li>\n<li>\u5728\u670d\u52a1\u5668\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd<\/li>\n<li>\u5728\u5ba2\u6237\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd<\/li>\n<\/ul>\n<\/li>\n<li>\u6587\u4ef6\u5939\u7ed3\u6784<\/li>\n<\/ul>\n<h3> \u5b9e\u65bd\u8eab\u4efd\u9a8c\u8bc1\uff1a\u51ed\u636e\u548c google oauth <\/h3>\n<ul>\n<li>\u8bbe\u7f6e prisma<\/li>\n<li>\u51ed\u8bc1<\/li>\n<li> \u6dfb\u52a0 google oauth \u63d0\u4f9b\u5546\n<ul>\n<li>\u8bbe\u7f6e google oauth \u5e94\u7528\u7a0b\u5e8f<\/li>\n<li>\u8bbe\u7f6e\u91cd\u5b9a\u5411 uri<\/li>\n<li>\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf<\/li>\n<li>\u8bbe\u7f6e\u63d0\u4f9b\u5546<\/li>\n<\/ul>\n<\/li>\n<li>\u521b\u5efa\u767b\u5f55\u548c\u6ce8\u518c\u9875\u9762<\/li>\n<li>\u6587\u4ef6\u5939\u7ed3\u6784<\/li>\n<\/ul>\n<h2> \u521d\u59cb\u8bbe\u7f6e <\/h2>\n<h2> \u5b89\u88c5 <\/h2>\n<pre>npm install next-auth@beta\n<\/pre>\n<pre>\/\/ env.local\nauth_secret=generatetd_random_value\n<\/pre>\n<h2> \u914d\u7f6e <\/h2>\n<h3> nextauthconfig \u8bbe\u7f6e <\/h3>\n<pre>\/\/ src\/auth.ts\nimport nextauth from \"next-auth\"\n\nexport const config = {\n  providers: [],\n}\n\nexport const { handlers, signin, signout, auth } = nextauth(config)\n<\/pre>\n<blockquote>\n<p>\u5b83\u5e94\u8be5\u653e\u5728<strong>src\u6587\u4ef6\u5939\u5185<\/strong><\/p>\n<\/blockquote>\n<p><strong>providers<\/strong> \u5728 auth.js \u4e2d\u8868\u793a\u662f\u53ef\u7528\u4e8e\u767b\u5f55\u7528\u6237\u7684\u670d\u52a1\u3002\u7528\u6237\u53ef\u4ee5\u901a\u8fc7\u56db\u79cd\u65b9\u5f0f\u767b\u5f55\u3002<\/p>\n<ul>\n<li>\u4f7f\u7528\u5185\u7f6e\u7684 oauth \u63d0\u4f9b\u7a0b\u5e8f\uff08\u4f8b\u5982 github\u3001google \u7b49&#8230;\uff09<\/li>\n<li>\u4f7f\u7528\u81ea\u5b9a\u4e49 oauth \u63d0\u4f9b\u7a0b\u5e8f<\/li>\n<li>\u4f7f\u7528\u7535\u5b50\u90ae\u4ef6<\/li>\n<li>\u4f7f\u7528\u51ed\u8bc1<\/li>\n<\/ul>\n<p>https:\/\/authjs.dev\/reference\/nextjs#providers<\/p>\n<h3> \u8def\u7531\u5904\u7406\u7a0b\u5e8f\u8bbe\u7f6e <\/h3>\n<pre>\/\/ src\/app\/api\/auth\/[...nextauth]\/route.ts\nimport { handlers } from \"@\/auth\" \/\/ referring to the auth.ts we just created\nexport const { get, post } = handlers\n<\/pre>\n<p>\u6b64\u6587\u4ef6\u7528\u4e8e\u4f7f\u7528 next.js app router \u8bbe\u7f6e\u8def\u7531\u5904\u7406\u7a0b\u5e8f\u3002<\/p>\n<h3> \u4e2d\u95f4\u4ef6 <\/h3>\n<pre>\/\/ src\/middleware.ts\nimport { auth } from \"@\/auth\"\n\nexport default auth((req) =&gt; {\n\u3000\u3000\/\/ add your logic here\n}\n\nexport const config = {\n  matcher: [\"\/((?!api|_next\/static|_next\/image|favicon.ico).*)\"],\u3000\/\/  it's default setting\n}\n<\/pre>\n<blockquote>\n<p>\u5728src\u6587\u4ef6\u5939\u5185\u5199\u5165<strong><\/strong>\u3002<br \/> \u5982\u679c\u5199\u5728 src \u6587\u4ef6\u5939\u4e4b\u5916\uff0c\u4e2d\u95f4\u4ef6\u5c06\u65e0\u6cd5\u5de5\u4f5c\u3002<\/p>\n<\/blockquote>\n<p><strong>\u4e2d\u95f4\u4ef6<\/strong>\u662f\u4e00\u4e2a<strong>\u5141\u8bb8\u60a8\u5728\u8bf7\u6c42\u5b8c\u6210\u4e4b\u524d\u8fd0\u884c\u4ee3\u7801<\/strong>\u7684\u51fd\u6570\u3002\u5b83\u5bf9\u4e8e<strong>\u4fdd\u62a4\u8def\u7531\u548c\u5904\u7406\u6574\u4e2a\u5e94\u7528\u7a0b\u5e8f\u7684\u8eab\u4efd\u9a8c\u8bc1<\/strong>\u7279\u522b\u6709\u7528\u3002<\/p>\n<p><strong>matcher<\/strong> \u662f <strong>\u4e00\u4e2a\u914d\u7f6e\u9009\u9879\uff0c\u7528\u4e8e\u6307\u5b9a\u54ea\u4e9b\u8def\u7531\u4e2d\u95f4\u4ef6\u5e94\u5e94\u7528\u4e8e<\/strong>\u3002\u5b83\u6709\u52a9\u4e8e<strong>\u4ec5\u5728\u5fc5\u8981\u7684\u8def\u7531\u4e0a\u8fd0\u884c\u4e2d\u95f4\u4ef6\u6765\u4f18\u5316\u6027\u80fd<\/strong>\u3002<br \/> \u793a\u4f8b\u5339\u914d\u5668\uff1a [&#8216;\/dashboard\/:path*&#8217;] \u4ec5\u5c06\u4e2d\u95f4\u4ef6\u5e94\u7528\u4e8e\u4eea\u8868\u677f\u8def\u7531\u3002<\/p>\n<p>https:\/\/authjs.dev\/getting-started\/session-management\/protecting?framework=express#nextjs-middleware<\/p>\n<h3> \u5728\u670d\u52a1\u5668\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd <\/h3>\n<pre>\/\/ src\/app\/page.tsx\nimport { auth } from \"@\/auth\"\nimport { redirect } from \"next\/navigation\"\n\nexport default async function page() {\n  const session = await auth()\n\n  if (!session) {\n    redirect('\/login')\n  }\n\n  return (\n    &lt;div&gt;\n      &lt;h1&gt;hello world!&lt;\/h1&gt;\n      &lt;img src={session.user.image} alt=\"user avatar\" \/&gt;\n    &lt;\/div&gt;\n  )\n}\n<\/pre>\n<h3> \u5728\u5ba2\u6237\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd <\/h3>\n<pre>\/\/ src\/app\/page.tsx\n\"use client\"\nimport { usesession } from \"next-auth\/react\"\nimport { userouter } from \"next\/navigation\"\n\nexport default async function page() {\n  const { data: session } = usesession()\n  const router = userouter()\n\n  if (!session.user) {\n    router.push('\/login')\n  }\n\n  return (\n    &lt;div&gt;\n      &lt;h1&gt;hello world!&lt;\/h1&gt;\n      &lt;img src={session.user.image} alt=\"user avatar\" \/&gt;\n    &lt;\/div&gt;\n  )\n}\n\n\/\/ src\/app\/layout.tsx\nimport type { metadata } from \"next\";\nimport \".\/globals.css\";\nimport { sessionprovider } from \"next-auth\/react\"\n\nexport const metadata: metadata = {\n  title: \"create next app\",\n  description: \"generated by create next app\",\n};\n\nexport default function rootlayout({\n  children,\n}: readonly&lt;{\n  children: react.reactnode;\n}&gt;) {\n  return (\n    &lt;html lang=\"en\"&gt;\n      &lt;body&gt;\n        &lt;sessionprovider&gt;\n          {children}\n        &lt;\/sessionprovider&gt;\n      &lt;\/body&gt;\n    &lt;\/html&gt;\n  );\n}\n<\/pre>\n<h2> \u6587\u4ef6\u5939\u7ed3\u6784 <\/h2>\n<pre>\/src\n  \/app\n    \/api\n      \/auth\n        [...nextauth]\n          \/route.ts  \/\/ route handler\n    layout.tsx\n    page.tsx\n\n  auth.ts  \/\/ provider, callback, logic etc\n  middleware.ts  \/\/ a function before request\n<\/pre>\n<h2> \u5b9e\u65bd\u8eab\u4efd\u9a8c\u8bc1\uff1a\u51ed\u636e\u548c google oauth <\/h2>\n<h2> \u8bbe\u7f6e\u68f1\u955c <\/h2>\n<pre>\/\/ prisma\/schema.prisma\n\nmodel user {\n  id            string    @id @default(cuid())\n  name          string?\n  email         string?   @unique\n  emailverified datetime?\n  image         string?\n  password      string?\n  accounts      account[]\n  sessions      session[]\n}\n\nmodel account {\n  \/\/ ... (standard auth.js account model)\n}\n\nmodel session {\n  \/\/ ... (standard auth.js session model)\n}\n\n\/\/ ... (other necessary models)\n\n<\/pre>\n<pre>\/\/ src\/lib\/prisma.ts\n\nimport { prismaclient } from \"@prisma\/client\"\n\nconst globalforprisma = globalthis as unknown as { prisma: prismaclient }\n\nexport const prisma = globalforprisma.prisma || new prismaclient()\n\nif (process.env.node_env !== \"production\") globalforprisma.prisma = prisma\n<\/pre>\n<h2> \u8bc1\u4e66 <\/h2>\n<p><strong>\u51ed\u8bc1<\/strong>\uff0c\u5728\u8eab\u4efd\u9a8c\u8bc1\u7684\u4e0a\u4e0b\u6587\u4e2d\uff0c\u6307\u7684\u662f<strong>\u4f7f\u7528\u7528\u6237\u63d0\u4f9b\u7684\u4fe1\u606f<\/strong>\u9a8c\u8bc1\u7528\u6237\u8eab\u4efd\u7684\u65b9\u6cd5\uff0c\u901a\u5e38\u662f\u7528\u6237\u540d\uff08\u6216\u7535\u5b50\u90ae\u4ef6\uff09\u548c\u5bc6\u7801\u3002<\/p>\n<p>\u6211\u4eec\u53ef\u4ee5\u5728 src\/auth.ts \u4e2d\u6dfb\u52a0\u51ed\u636e\u3002<\/p>\n<pre>\/\/ src\/auth.ts\n\nimport nextauth from \"next-auth\";\nimport type { nextauthconfig } from \"next-auth\";\nimport credentials from \"next-auth\/providers\/credentials\"\nimport { prismaadapter } from \"@auth\/prisma-adapter\"\nimport { prisma } from \"@\/lib\/prisma\"\nimport bcrypt from 'bcryptjs';\n\nexport const config = {\n  adapter: prismaadapter(prisma),\n  providers: [\n    credentials({\n      credentials: {\n        email: { label: \"email\", type: \"text\" },\n        password: { label: \"password\", type: \"password\" }\n      },\n      authorize: async (credentials): promise&lt;any&gt; =&gt; {\n        if (!credentials?.email || !credentials?.password) {\n          return null;\n        }\n\n        try {\n          const user = await prisma.user.findunique({\n            where: {\n              email: credentials.email as string\n            }\n          })\n\n          if (!user || !user.hashedpassword) {\n            return null\n          }\n\n          const ispasswordvalid = await bcrypt.compare(\n            credentials.password as string,\n            user.hashedpassword\n          )\n\n          if (!ispasswordvalid) {\n            return null\n          }\n\n          return {\n            id: user.id as string,\n            email: user.email as string,\n            name: user.name as string,\n          }\n        } catch (error) {\n          console.error('error during authentication:', error)\n          return null \n        }\n      }\n    })\n  ],\n  secret: process.env.auth_secret,\n  pages: {\n    signin: '\/login',\n  },\n  session: {\n    strategy: \"jwt\",\n  },\n  callbacks: {\n    async jwt({ token, user }) {\n      if (user) {\n        token.id = user.id\n        token.email = user.email\n        token.name = user.name\n      }\n      return token\n    },\n    async session({ session, token }) {\n      if (session.user) {\n        session.user.id = token.id as string\n        session.user.email = token.email as string\n        session.user.name = token.name as string\n      }\n      return session\n    },\n  },\n} satisfies nextauthconfig;\n\nexport const { handlers, auth, signin, signout } = nextauth(config);\n\n<\/pre>\n<p>\u9002\u914d\u5668\uff1a<\/p>\n<ul>\n<li>\u5c06\u8eab\u4efd\u9a8c\u8bc1\u7cfb\u7edf\u8fde\u63a5\u5230\u6570\u636e\u5e93\u6216\u6570\u636e\u5b58\u50a8\u89e3\u51b3\u65b9\u6848\u7684\u6a21\u5757\u3002<\/li>\n<\/ul>\n<p>\u79d8\u5bc6\uff1a<\/p>\n<ul>\n<li>\u8fd9\u662f\u4e00\u4e2a\u968f\u673a\u5b57\u7b26\u4e32\uff0c\u7528\u4e8e\u54c8\u5e0c\u4ee4\u724c\u3001\u7b7e\u540d\/\u52a0\u5bc6 cookie \u4ee5\u53ca\u751f\u6210\u52a0\u5bc6\u5bc6\u94a5\u3002<\/li>\n<li>\u8fd9\u5bf9\u4e8e\u5b89\u5168\u81f3\u5173\u91cd\u8981\uff0c\u5e94\u8be5\u4fdd\u5bc6\u3002<\/li>\n<li>\u5728\u672c\u4f8b\u4e2d\uff0c\u5b83\u662f\u4f7f\u7528\u73af\u5883\u53d8\u91cf auth_secret \u8bbe\u7f6e\u7684\u3002<\/li>\n<\/ul>\n<p>\u9875\u9762\uff1a<\/p>\n<ul>\n<li>\u6b64\u5bf9\u8c61\u5141\u8bb8\u60a8\u81ea\u5b9a\u4e49\u8eab\u4efd\u9a8c\u8bc1\u9875\u9762\u7684 url\u3002<\/li>\n<li>\u5728\u60a8\u7684\u793a\u4f8b\u4e2d\uff0csignin: &#8216;\/login&#8217; \u8868\u793a\u767b\u5f55\u9875\u9762\u5c06\u4f4d\u4e8e &#8216;\/login&#8217; \u8def\u7531\uff0c\u800c\u4e0d\u662f\u9ed8\u8ba4\u7684 &#8216;\/api\/auth\/signin&#8217;\u3002<\/li>\n<\/ul>\n<p>\u4f1a\u8bdd\uff1a<\/p>\n<ul>\n<li>\u8fd9\u914d\u7f6e\u4e86\u4f1a\u8bdd\u7684\u5904\u7406\u65b9\u5f0f\u3002<\/li>\n<li> \u7b56\u7565\uff1a\u201cjwt\u201d\u8868\u793a json web token \u5c06\u7528\u4e8e\u4f1a\u8bdd\u7ba1\u7406\u800c\u4e0d\u662f\u6570\u636e\u5e93\u4f1a\u8bdd\u3002<\/li>\n<\/ul>\n<p>\u56de\u8c03\uff1a<\/p>\n<ul>\n<li>\u8fd9\u4e9b\u662f\u5728\u8eab\u4efd\u9a8c\u8bc1\u6d41\u7a0b\u4e2d\u7684\u5404\u4e2a\u70b9\u8c03\u7528\u7684\u51fd\u6570\uff0c\u5141\u8bb8\u60a8\u81ea\u5b9a\u4e49\u6d41\u7a0b\u3002<\/li>\n<\/ul>\n<p>jwt \u56de\u8c03\uff1a<\/p>\n<ul>\n<li>\u5b83\u5728\u521b\u5efa\u6216\u66f4\u65b0 jwt \u65f6\u8fd0\u884c\u3002<\/li>\n<li>\u5728\u60a8\u7684\u4ee3\u7801\u4e2d\uff0c\u5b83\u5c06\u7528\u6237\u4fe1\u606f\uff08id\u3001\u7535\u5b50\u90ae\u4ef6\u3001\u59d3\u540d\uff09\u6dfb\u52a0\u5230\u4ee4\u724c\u4e2d\u3002<\/li>\n<\/ul>\n<p>\u4f1a\u8bdd\u56de\u8c03\uff1a<\/p>\n<ul>\n<li>\u6bcf\u5f53\u68c0\u67e5\u4f1a\u8bdd\u65f6\u90fd\u4f1a\u8fd0\u884c\u3002<\/li>\n<li>\u60a8\u7684\u4ee3\u7801\u6b63\u5728\u5c06\u7528\u6237\u4fe1\u606f\u4ece\u4ee4\u724c\u6dfb\u52a0\u5230\u4f1a\u8bdd\u5bf9\u8c61\u3002<\/li>\n<\/ul>\n<h2> \u6dfb\u52a0 google oauth \u63d0\u4f9b\u5546 <\/h2>\n<h3> \u8bbe\u7f6e google oauth \u5e94\u7528\u7a0b\u5e8f <\/h3>\n<p>\u4ece gcp console \u521b\u5efa\u65b0\u7684 oauth \u5ba2\u6237\u7aef id &gt; api \u548c\u670d\u52a1 &gt; \u51ed\u636e<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.17golang.com\/uploads\/20241121\/1732186133673f1015e4c89.jpg\" class=\"aligncenter\" title=\"\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u63d2\u56fe\" alt=\"\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u63d2\u56fe\" \/><\/p>\n<p>\u521b\u5efa\u540e\uff0c\u4fdd\u5b58\u60a8\u7684\u5ba2\u6237\u7aef id \u548c\u5ba2\u6237\u7aef\u5bc6\u94a5\u4ee5\u4f9b\u4ee5\u540e\u4f7f\u7528\u3002<\/p>\n<h3> \u8bbe\u7f6e\u91cd\u5b9a\u5411 uri <\/h3>\n<p>\u5f53\u6211\u4eec\u5728\u672c\u5730\u5de5\u4f5c\u65f6\uff0c\u8bbe\u7f6ehttp:\/\/localhost:3000\/api\/auth\/callback\/google<\/p>\n<p>\u751f\u4ea7\u73af\u5883\u4e2d\uff0c\u53ea\u9700\u5c06 http:\/\/localhost:3000 \u66ff\u6362\u4e3a https:\/\/&#8212;&#8211;\u5373\u53ef\u3002<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.17golang.com\/uploads\/20241121\/1732186133673f1015e571b.jpg\" class=\"aligncenter\" title=\"\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u63d2\u56fe1\" alt=\"\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u63d2\u56fe1\" \/><\/p>\n<h3> \u8bbe\u7f6e\u73af\u5883\u53d8\u91cf <\/h3>\n<pre>\/\/ .env.local\ngoogle_client_id={client_id}\ngoogle_client_secret={client_secret}\n<\/pre>\n<h3> \u8bbe\u7f6e\u63d0\u4f9b\u5546 <\/h3>\n<pre>\/\/ src\/auth.ts\n\nimport googleprovider from \"next-auth\/providers\/google\"  \/\/ add this import.\n\nexport const { handlers, auth } = nextauth({\n  adapter: prismaadapter(prisma),\n  providers: [\n    credentialsprovider({\n      \/\/ ... (previous credentials configuration)\n    }),\n    googleprovider({\n      clientid: process.env.google_client_id,\n      clientsecret: process.env.google_client_secret,\n    }),\n  ],\n  \/\/ ... other configurations\n})\n<\/pre>\n<p>https:\/\/authjs.dev\/getting-started\/authentication\/oauth<\/p>\n<h2> \u521b\u5efa\u767b\u5f55\u548c\u6ce8\u518c\u9875\u9762 <\/h2>\n<pre>\/\/\/\/ ui pages\n\/\/ src\/app\/login\/loginpage.tsx\nimport link from 'next\/link'\nimport { loginform } from '@\/components\/auth\/loginform'\nimport { separator } from '@\/components\/auth\/separator'\nimport { authlayout } from '@\/components\/auth\/authlayout'\nimport { googleauthbutton } from '@\/components\/auth\/googleauthbutton'\n\nexport default function loginpage() {\n  return (\n    &lt;authlayout title=\"welcome back!\"&gt;\n      &lt;loginform \/&gt;\n      &lt;separator \/&gt;\n      &lt;googleauthbutton text=\"sign in with google\" \/&gt;\n      &lt;div classname=\"mt-6 text-center\"&gt;\n        &lt;p classname=\"text-sm text-gray-400\"&gt;\n          do not have an account?{' '}\n          &lt;link href=\"\/signup\" classname=\"pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]\"&gt;\n            sign up\n          &lt;\/link&gt;\n        &lt;\/p&gt;\n      &lt;\/div&gt;\n    &lt;\/authlayout&gt;\n  )\n}\n\n\n\/\/ src\/app\/signup\/signuppage.tsx\nimport link from 'next\/link'\nimport { signupform } from '@\/components\/auth\/signupform'\nimport { separator } from '@\/components\/auth\/separator'\nimport { authlayout } from '@\/components\/auth\/authlayout'\nimport { googleauthbutton } from '@\/components\/auth\/googleauthbutton'\n\nexport default function signuppage() {\n  return (\n    &lt;authlayout title=\"welcome!\"&gt;\n      &lt;signupform \/&gt;\n      &lt;separator \/&gt;\n      &lt;googleauthbutton text=\"sign up with google\" \/&gt;\n      &lt;div classname=\"mt-6 text-center\"&gt;\n        &lt;p classname=\"text-sm text-gray-400\"&gt;\n          already have an account?{' '}\n          &lt;link href=\"\/login\" classname=\"pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]\"&gt;\n            sign in\n          &lt;\/link&gt;\n        &lt;\/p&gt;\n      &lt;\/div&gt;\n    &lt;\/authlayout&gt;\n  )\n}\n\n<\/pre>\n<pre>\/\/\/\/ components\n\/\/ src\/components\/auth\/authlayout.tsx\nimport react from 'react'\n\ninterface authlayoutprops {\n  children: react.reactnode\n  title: string\n}\n\nexport const authlayout: react.fc&lt;authlayoutprops&gt; = ({ children, title }) =&gt; {\n  return (\n    &lt;div classname=\"min-h-screen bg-[#36393f] flex flex-col justify-center py-12 sm:px-6 lg:px-8\"&gt;\n      &lt;div classname=\"sm:mx-auto sm:w-full sm:max-w-md\"&gt;\n        &lt;h2 classname=\"mt-6 text-center text-3xl font-extrabold text-white\"&gt;\n          {title}\n        &lt;\/h2&gt;\n      &lt;\/div&gt;\n\n      &lt;div classname=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\"&gt;\n        &lt;div classname=\"bg-[#2f3136] py-8 px-4 shadow sm:rounded-lg sm:px-10\"&gt;\n          {children}\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  )\n}\n\n\/\/ src\/components\/auth\/googleauthbutton.tsx\nimport { signin } from \"@\/auth\"\nimport { button } from \"@\/components\/ui\/button\"\n\ninterface googleauthbuttonprops {\n  text: string\n}\n\nexport const googleauthbutton: react.fc&lt;googleauthbuttonprops&gt; = ({ text }) =&gt; {\n  return (\n    &lt;form\n      action={async () =&gt; {\n        \"use server\"\n        await signin(\"google\", { redirectto: '\/' })\n      }}\n    &gt;\n      &lt;button\n        classname=\"my-1 w-full bg-white text-gray-700 hover:bg-slate-100\"\n      &gt;\n        &lt;svg classname=\"h-5 w-5 mr-2\" viewbox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"&gt;\n          &lt;path d=\"m22.56 12.25c0-.78-.07-1.53-.2-2.25h12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\" fill=\"#4285f4\"\/&gt;\n          &lt;path d=\"m12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h2.18v2.84c3.99 20.53 7.7 23 12 23z\" fill=\"#34a853\"\/&gt;\n          &lt;path d=\"m5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09v7.07h2.18c1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\" fill=\"#fbbc05\"\/&gt;\n          &lt;path d=\"m12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15c17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\" fill=\"#ea4335\"\/&gt;\n          &lt;path d=\"m1 1h22v22h1z\" fill=\"none\"\/&gt;\n        &lt;\/svg&gt;\n        {text}\n      &lt;\/button&gt;\n    &lt;\/form&gt;\n  )\n}\n\n\/\/ src\/components\/auth\/loginform.tsx\n'use client'\n\nimport { usetransition } from \"react\"\nimport { useform } from \"react-hook-form\"\nimport {\n  form,\n  formcontrol,\n  formfield,\n  formitem,\n  formlabel,\n  formmessage,\n} from \"@\/components\/ui\/form\"\nimport { input } from \"@\/components\/ui\/input\"\nimport { button } from \"@\/components\/ui\/button\"\nimport { loginresolver, loginschema } from \"@\/schema\/login\"\nimport { usestate } from \"react\"\nimport { userouter } from \"next\/navigation\"\nimport { formerror } from \"@\/components\/auth\/formerror\"\nimport { formsuccess } from \"@\/components\/auth\/formsuccess\"\nimport { login } from \"@\/app\/actions\/auth\/login\"\nimport { loader2 } from \"lucide-react\"\n\nexport const loginform = () =&gt; {\n  const [error, seterror] = usestate&lt;string | undefined&gt;('')\n  const [success, setsuccess] = usestate&lt;string | undefined&gt;('')\n  const [ispending, starttransition] = usetransition()\n  const router = userouter();\n\n  const form = useform&lt;loginschema&gt;({\n    defaultvalues: { email: '', password: ''},\n    resolver: loginresolver,\n  })\n\n  const onsubmit = (formdata: loginschema) =&gt; { \n    starttransition(() =&gt; {\n      seterror('')\n      setsuccess('')\n      login(formdata)\n        .then((data) =&gt; {\n          if (data.success) {\n            setsuccess(data.success)\n            router.push('\/setup')\n          } else if (data.error) {\n            seterror(data.error)\n          }\n        })\n        .catch((data) =&gt; {\n          seterror(data.error)\n        })\n    })\n  }\n\n  return (\n    &lt;form {...form}&gt;\n      &lt;form onsubmit={form.handlesubmit(onsubmit)}&gt;\n        &lt;div classname=\"space-y-3\"&gt;\n          &lt;formfield\n            control={form.control}\n            name=\"email\"\n            render={({ field }) =&gt; (\n              &lt;formitem&gt;\n                &lt;formlabel classname=\"text-white\"&gt;email address&lt;\/formlabel&gt;\n                &lt;formcontrol&gt;\n                  &lt;input\n                    placeholder=\"enter your email address\" \n                    {...field} \n                    disabled={ispending}\n                    classname=\"bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]\"\n                  \/&gt;\n                &lt;\/formcontrol&gt;\n                &lt;formmessage \/&gt;\n              &lt;\/formitem&gt;\n            )}\n          \/&gt;\n          &lt;formfield\n            control={form.control}\n            name=\"password\"\n            render={({ field }) =&gt; (\n              &lt;formitem&gt;\n                &lt;formlabel classname=\"text-white\"&gt;password&lt;\/formlabel&gt;\n                &lt;formcontrol&gt;\n                  &lt;input \n                    type=\"password\"\n                    placeholder=\"enter your password\" \n                    {...field} \n                    disabled={ispending}\n                    classname=\"bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]\"\n                  \/&gt;\n                &lt;\/formcontrol&gt;\n                &lt;formmessage \/&gt;\n              &lt;\/formitem&gt;\n            )}\n          \/&gt;\n          &lt;formerror message={error} \/&gt;\n          &lt;formsuccess message={success} \/&gt;\n        &lt;\/div&gt;\n        &lt;button\n          type=\"submit\"\n          disabled={ispending}\n          classname=\"mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white\"\n        &gt;\n          {ispending ? (\n            &lt;&gt;\n              &lt;loader2 classname=\"mr-2 h-4 w-4 animate-spin\" \/&gt;\n              loading...\n            &lt;\/&gt;\n          ) : (\n            'login'\n          )}\n        &lt;\/button&gt;\n      &lt;\/form&gt;\n    &lt;\/form&gt;\n  )\n}\n\n\n\/\/ src\/components\/auth\/signupform.tsx\n'use client'\n\nimport { usetransition } from \"react\"\nimport { useform } from \"react-hook-form\"\nimport {\n  form,\n  formcontrol,\n  formfield,\n  formitem,\n  formlabel,\n  formmessage,\n} from \"@\/components\/ui\/form\"\nimport { input } from \"@\/components\/ui\/input\"\nimport { button } from \"@\/components\/ui\/button\"\nimport { signupresolver, signupschema } from \"@\/schema\/signup\"\nimport { usestate } from \"react\"\nimport { userouter } from \"next\/navigation\"\nimport { formerror } from \"@\/components\/auth\/formerror\"\nimport { formsuccess } from \"@\/components\/auth\/formsuccess\"\nimport { signup } from \"@\/app\/actions\/auth\/signup\"\nimport { loader2 } from \"lucide-react\"\n\nexport const signupform = () =&gt; {\n  const [error, seterror] = usestate&lt;string | undefined&gt;('')\n  const [success, setsuccess] = usestate&lt;string | undefined&gt;('')\n  const [ispending, starttransition] = usetransition()\n  const router = userouter();\n\n  const form = useform&lt;signupschema&gt;({\n    defaultvalues: { name: '', email: '', password: ''},\n    resolver: signupresolver,\n  })\n\n  const onsubmit = async (formdata: signupschema) =&gt; { \n    starttransition(() =&gt; {\n      seterror('')\n      setsuccess('')\n      signup(formdata)\n        .then((data) =&gt; {\n          if (data.success) {\n            setsuccess(data.success)\n            router.push('\/login')\n          } else if (data.error) {\n            seterror(data.error)\n          }\n        })\n        .catch((data) =&gt; {\n          seterror(data.error)\n        })\n    })\n  }\n\n  return (\n    &lt;form {...form}&gt;\n      &lt;form onsubmit={form.handlesubmit(onsubmit)}&gt;\n        &lt;div classname=\"space-y-3\"&gt;\n          &lt;formfield\n            control={form.control}\n            name=\"name\"\n            render={({ field }) =&gt; (\n              &lt;formitem&gt;\n                &lt;formlabel classname=\"text-white\"&gt;username&lt;\/formlabel&gt;\n                &lt;formcontrol&gt;\n                  &lt;input\n                    placeholder=\"enter your name\" \n                    {...field} \n                    disabled={ispending}\n                    classname=\"bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]\"\n                  \/&gt;\n                &lt;\/formcontrol&gt;\n                &lt;formmessage \/&gt;\n              &lt;\/formitem&gt;\n            )}\n          \/&gt;\n          &lt;formfield\n            control={form.control}\n            name=\"email\"\n            render={({ field }) =&gt; (\n              &lt;formitem&gt;\n                &lt;formlabel classname=\"text-white\"&gt;email address&lt;\/formlabel&gt;\n                &lt;formcontrol&gt;\n                  &lt;input\n                    placeholder=\"enter your email address\" \n                    {...field} \n                    disabled={ispending}\n                    classname=\"bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]\"\n                  \/&gt;\n                &lt;\/formcontrol&gt;\n                &lt;formmessage \/&gt;\n              &lt;\/formitem&gt;\n            )}\n          \/&gt;\n          &lt;formfield\n            control={form.control}\n            name=\"password\"\n            render={({ field }) =&gt; (\n              &lt;formitem&gt;\n                &lt;formlabel classname=\"text-white\"&gt;password&lt;\/formlabel&gt;\n                &lt;formcontrol&gt;\n                  &lt;input \n                    type=\"password\"\n                    placeholder=\"enter your password\" \n                    {...field} \n                    disabled={ispending}\n                    classname=\"bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]\"\n                  \/&gt;\n                &lt;\/formcontrol&gt;\n                &lt;formmessage \/&gt;\n              &lt;\/formitem&gt;\n            )}\n          \/&gt;\n          &lt;formerror message={error} \/&gt;\n          &lt;formsuccess message={success} \/&gt;\n        &lt;\/div&gt;\n        &lt;button\n          type=\"submit\"\n          disabled={ispending}\n          classname=\"mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white\"\n        &gt;\n          {ispending ? (\n            &lt;&gt;\n              &lt;loader2 classname=\"mr-2 h-4 w-4 animate-spin\" \/&gt;\n              loading...\n            &lt;\/&gt;\n          ) : (\n            'sign up'\n          )}\n        &lt;\/button&gt;\n      &lt;\/form&gt;\n    &lt;\/form&gt;\n  )\n}\n\n\n\/\/ src\/components\/auth\/formsuccess.tsx\nimport { checkcircledicon } from \"@radix-ui\/react-icons\";\n\ninterface formsuccessprops {\n  message?: string;\n}\n\nexport const formsuccess = ({ message }: formsuccessprops) =&gt; {\n  if (!message) return null;\n\n  return (\n    &lt;div classname=\"bg-emerald-500\/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-emerald-500\"&gt;\n      &lt;checkcircledicon classname=\"h-4 w-4\" \/&gt;\n      &lt;p&gt;{message}&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n};\n\n\n\/\/ src\/components\/auth\/formerror.tsx\nimport { exclamationtriangleicon } from \"@radix-ui\/react-icons\";\n\ninterface formerrorprops {\n  message?: string;\n}\n\nexport const formerror = ({ message }: formerrorprops) =&gt; {\n  if (!message) return null;\n\n  return (\n    &lt;div classname=\"bg-destructive\/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive\"&gt;\n      &lt;exclamationtriangleicon classname=\"h-4 w-4\" \/&gt;\n      &lt;p&gt;{message}&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n};\n\n\n\/\/ src\/components\/auth\/separator.tsx\nexport const separator = () =&gt; {\n  return (\n    &lt;div classname=\"my-4 relative\"&gt;\n      &lt;div classname=\"absolute inset-0 flex items-center\"&gt;\n        &lt;div classname=\"w-full border-t border-gray-600\" \/&gt;\n      &lt;\/div&gt;\n      &lt;div classname=\"relative flex justify-center text-sm\"&gt;\n        &lt;span classname=\"px-2 bg-[#2f3136] text-gray-400\"&gt;or continue with&lt;\/span&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  )\n}\n\n<\/pre>\n<pre>\/\/\/\/ actions\n\/\/ src\/app\/actions\/auth\/login.ts\n'use server'\n\nimport { loginschema, loginschema } from '@\/schema\/login'\nimport { signin } from '@\/auth'\n\n\nexport const login = async (formdata: loginschema) =&gt; {\n  const email = formdata['email'] as string\n  const password = formdata['password'] as string\n\n  const validatedfields = loginschema.safeparse({\n    email: formdata.email as string,\n    password: formdata.password as string,\n  })\n\n  if (!validatedfields.success) {\n    return { \n      errors: validatedfields.error.flatten().fielderrors,\n      message: 'login failed. please check your input.'\n    }\n  }\n\n  try {\n    const result = await signin('credentials', {\n      redirect: false,\n      callbackurl: '\/setup',\n      email,\n      password\n    })\n\n    if (result?.error) {\n      return { error : 'invalid email or password'}\n    } else {\n      return { success : 'login successfully'}\n    }\n  } catch {\n    return { error : 'login failed'}\n  }\n}\n\n\/\/ src\/app\/actions\/auth\/signup.ts\n'use server'\n\nimport bcrypt from 'bcryptjs'\nimport { signupschema, signupschema } from \"@\/schema\/signup\"\nimport { prisma } from '@\/lib\/prisma';\n\nexport const signup = async (formdata: signupschema) =&gt; {\n  const validatedfields = signupschema.safeparse({\n    name: formdata.name as string,\n    email: formdata.email as string,\n    password: formdata.password as string,\n  })\n\n  if (!validatedfields.success) {\n    return { \n      errors: validatedfields.error.flatten().fielderrors,\n      message: 'sign up failed. please check your input.'\n    }\n  }\n\n  try {\n    const hashedpassword = await bcrypt.hash(validatedfields.data.password, 10);\n    const existinguser = await prisma.user.findunique({\n      where: { email: validatedfields.data.email }\n    })\n\n    if (existinguser) {\n      return { error: 'user already exists!' }\n    }\n\n    await prisma.user.create({\n      data: {\n        name:  validatedfields.data.name,\n        email:  validatedfields.data.email,\n        hashedpassword: hashedpassword,\n      },\n\n    });\n\n    return { success: 'user created successfully!' }\n  } catch (error) {\n    return { error : `sign up failed`}\n  }\n}\n<\/pre>\n<pre>\/\/\/\/ validations\n\/\/ src\/schema\/login.ts\nimport * as z from 'zod';\nimport { zodresolver } from '@hookform\/resolvers\/zod'; \n\nexport const loginschema = z.object({\n  email: z.string().email('this is not valid email address'),\n  password: z\n    .string()\n    .min(8, { message: 'password must contain at least 8 characters' }),\n});\n\nexport type loginschema = z.infer&lt;typeof loginschema&gt;;\nexport const loginresolver = zodresolver(loginschema);\n\n\/\/ src\/schema\/signup.ts\nimport * as z from 'zod';\nimport { zodresolver } from '@hookform\/resolvers\/zod'; \n\nexport const signupschema = z.object({\n  name: z.string().min(1, {\n    message: 'name is required'\n  }),\n  email: z.string().email('this is not valid email address'),\n  password: z\n    .string()\n    .min(8, { message: 'password must contain at least 8 characters' }),\n});\n\nexport type signupschema = z.infer&lt;typeof signupschema&gt;;\nexport const signupresolver = zodresolver(signupschema);\n\n<\/pre>\n<pre>\/\/ src\/middleware.ts\nimport { nextresponse } from 'next\/server'\nimport { auth } from \"@\/auth\"\n\nexport default auth((req) =&gt; {\n  const { nexturl, auth: session } = req\n  const isloggedin = !!session\n  const isloginpage = nexturl.pathname === \"\/login\"\n  const issignuppage = nexturl.pathname === \"\/signup\"\n  const issetuppage = nexturl.pathname === \"\/setup\"\n\n  \/\/ if trying to access \/setup while not logged in\n  if (!isloggedin &amp;&amp; issetuppage) {\n    const loginurl = new url(\"\/login\", nexturl.origin)\n    return nextresponse.redirect(loginurl)\n  }\n\n  \/\/ if trying to access \/login or \/signup while already logged in\n  if (isloggedin &amp;&amp; (isloginpage || issignuppage)) {\n    const dashboardurl = new url(\"\/\", nexturl.origin)\n    return nextresponse.redirect(dashboardurl)\n  }\n\n  \/\/ for all other cases, allow the request to pass through\n  return nextresponse.next()\n})\n\nexport const config = {\n  matcher: [\"\/login\",\"\/signup\", \"\/setup\", \"\/\"],\n};\n\n<\/pre>\n<h2> \u6587\u4ef6\u5939\u7ed3\u6784 <\/h2>\n<pre>\/src\n  \/app\n    \/actions\n      \/login.ts  \/\/ Login Action\n      \/signup.ts  \/\/ Signup Action\n    \/api\n      \/auth\n        [...nextauth]\n          \/route.ts\n    \/login\n      page.tsx  \/\/ Login Page\n    \/signup\n      page.tsx  \/\/ Sign Up Page\n    layout.tsx\n    page.tsx\n\n  \/components\n    \/auth\n      AuthLayout.tsx\n      GoogleAuthButton.tsx\n      LoginForm.tsx\n      SignupForm.tsx\n      FormSuccess.tsx\n      FormError.tsx\n      Separator.tsx\n\n  \/schema\n    login.ts\n    signup.ts\n\n  auth.ts  \/\/ in src folder\n  middleware.ts  \/\/ in src folder\n\n<\/pre>\n<p>\u4eca\u5929\u5173\u4e8e\u300a\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u300b\u7684\u5185\u5bb9\u5c31\u4ecb\u7ecd\u5230\u8fd9\u91cc\u4e86\uff0c\u662f\u4e0d\u662f\u5b66\u8d77\u6765\u4e00\u76ee\u4e86\u7136\uff01\u60f3\u8981\u4e86\u89e3\u66f4\u591a\u5173\u4e8e\u7684\u5185\u5bb9\u8bf7\u5173\u6ce8\u7c73\u4e91\u516c\u4f17\u53f7\uff01<\/p>\n<p>      \u7248\u672c\u58f0\u660e \u672c\u6587\u8f6c\u8f7d\u4e8e\uff1adev.to \u5982\u6709\u4fb5\u72af\uff0c\u8bf7\u8054\u7cfb\u5220\u9664    <\/p>\n<dl>\n<dt><\/dt>\n<dd>\n   \u5982\u4f55\u907f\u514d Tree \u7ec4\u4ef6\u70b9\u51fb\u8282\u70b9\u591a\u6b21\u89e6\u53d1\u63a5\u53e3\u8bf7\u6c42\uff1f\n <\/dd>\n<\/dl>\n","protected":false},"excerpt":{"rendered":"<p>\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1 \u6536\u85cf \u7c73\u4e91\u4eca\u5929\u5c06\u7ed9\u5927\u5bb6\u5e26\u6765\u300a\u5728 Nextjs App Router \u4e2d\u4f7f\u7528 Authjs \u8fdb\u884c\u7528\u6237\u8eab\u4efd\u9a8c\u8bc1\u300b\uff0c\u611f\u5174\u8da3\u7684\u670b\u53cb\u8bf7\u7ee7\u7eed\u770b\u4e0b\u53bb\u5427\uff01\u4ee5\u4e0b\u5185\u5bb9\u5c06\u4f1a\u6d89\u53ca\u5230\u7b49\u7b49\u77e5\u8bc6\u70b9\uff0c\u5982\u679c\u4f60\u662f\u6b63\u5728\u5b66\u4e60\u6587\u7ae0\u6216\u8005\u5df2\u7ecf\u662f\u5927\u4f6c\u7ea7\u522b\u4e86\uff0c\u90fd\u975e\u5e38\u6b22\u8fce\u4e5f\u5e0c\u671b\u5927\u5bb6\u90fd\u80fd\u7ed9\u6211\u5efa\u8bae\u8bc4\u8bba\u54c8~\u5e0c\u671b\u80fd\u5e2e\u52a9\u5230\u5927\u5bb6\uff01 \u76ee\u5f55 \u521d\u59cb\u8bbe\u7f6e \u5b89\u88c5 \u914d\u7f6e nextauthconfig \u8bbe\u7f6e \u8def\u7531\u5904\u7406\u7a0b\u5e8f\u8bbe\u7f6e \u4e2d\u95f4\u4ef6 \u5728\u670d\u52a1\u5668\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd \u5728\u5ba2\u6237\u7aef\u7ec4\u4ef6\u4e2d\u83b7\u53d6\u4f1a\u8bdd \u6587\u4ef6\u5939\u7ed3\u6784 \u5b9e\u65bd\u8eab\u4efd\u9a8c\u8bc1\uff1a\u51ed\u636e\u548c google oauth \u8bbe\u7f6e prisma \u51ed\u8bc1 \u6dfb\u52a0 google oauth \u63d0\u4f9b\u5546 \u8bbe\u7f6e google oauth \u5e94\u7528\u7a0b\u5e8f \u8bbe\u7f6e\u91cd\u5b9a\u5411 uri \u8bbe\u7f6e\u73af\u5883\u53d8\u91cf \u8bbe\u7f6e\u63d0\u4f9b\u5546 \u521b\u5efa\u767b\u5f55\u548c\u6ce8\u518c\u9875\u9762 \u6587\u4ef6\u5939\u7ed3\u6784 \u521d\u59cb\u8bbe\u7f6e \u5b89\u88c5 npm install next-auth@beta \/\/ env.local auth_secret=generatetd_random_value \u914d\u7f6e [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16],"tags":[],"class_list":["post-52575","post","type-post","status-publish","format-standard","hentry","category-16"],"_links":{"self":[{"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/posts\/52575","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/comments?post=52575"}],"version-history":[{"count":0,"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/posts\/52575\/revisions"}],"wp:attachment":[{"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/media?parent=52575"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/categories?post=52575"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fwq.ai\/blog\/wp-json\/wp\/v2\/tags?post=52575"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}