From 94393054e6f7348faf2199b67a80feb7c325de9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Mon, 7 Oct 2024 13:04:24 +0200 Subject: [PATCH] next-auth implementation --- .npmrc | 1 + client/.env.development | 3 + client/.eslintrc.json | 107 ++++++++- client/.prettierrc | 5 + client/app/layout.tsx | 35 --- client/lib/queryClient.ts | 10 - client/middleware.ts | 31 +++ client/package.json | 6 +- .../src/app/auth/api/[...nextauth]/config.ts | 96 ++++++++ .../src/app/auth/api/[...nextauth]/route.ts | 7 + client/{ => src}/app/favicon.ico | Bin client/{ => src}/app/fonts/GeistMonoVF.woff | Bin client/{ => src}/app/fonts/GeistVF.woff | Bin client/{ => src}/app/globals.css | 0 client/src/app/layout.tsx | 34 +++ client/{ => src}/app/page.tsx | 17 +- client/src/app/providers.tsx | 46 ++++ client/src/lib/queryClient.ts | 27 +++ pnpm-lock.yaml | 214 +++++++++++++++++- 19 files changed, 572 insertions(+), 67 deletions(-) create mode 100644 .npmrc create mode 100644 client/.env.development create mode 100644 client/.prettierrc delete mode 100644 client/app/layout.tsx delete mode 100644 client/lib/queryClient.ts create mode 100644 client/middleware.ts create mode 100644 client/src/app/auth/api/[...nextauth]/config.ts create mode 100644 client/src/app/auth/api/[...nextauth]/route.ts rename client/{ => src}/app/favicon.ico (100%) rename client/{ => src}/app/fonts/GeistMonoVF.woff (100%) rename client/{ => src}/app/fonts/GeistVF.woff (100%) rename client/{ => src}/app/globals.css (100%) create mode 100644 client/src/app/layout.tsx rename client/{ => src}/app/page.tsx (70%) create mode 100644 client/src/app/providers.tsx create mode 100644 client/src/lib/queryClient.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..0a60c475 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-prefix='' \ No newline at end of file diff --git a/client/.env.development b/client/.env.development new file mode 100644 index 00000000..d99a62f9 --- /dev/null +++ b/client/.env.development @@ -0,0 +1,3 @@ +NEXTAUTH_URL=http://localhost:$PORT +NEXTAUTH_SECRET=WAzjpS46vFxp17TsRDU3FXo+TF0vrfy6uhCXwGMBUE8= +NEXT_PUBLIC_API_URL=http://localhost:4000 \ No newline at end of file diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 37224185..5b1dd152 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -1,3 +1,106 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] -} + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "next/core-web-vitals", + "next/typescript" + ], + "rules": { + "no-console": [1, { "allow": ["info", "error", "debug"] }], + "import/order": [ + "warn", + { + "groups": ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + }, + "pathGroups": [ + { + "pattern": "react", + "group": "builtin", + "position": "before" + }, + { + "pattern": "react**", + "group": "builtin" + }, + { + "pattern": "@react**", + "group": "builtin" + }, + { + "pattern": "next/**", + "group": "builtin", + "position": "after" + }, + { + "pattern": "node_modules/**", + "group": "builtin" + }, + { + "pattern": "@/env.mjs", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/lib/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/data/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/store", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/store/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/types/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/app/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/hooks/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/constants/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/containers/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@/components/**", + "group": "internal" + }, + { + "pattern": "@/services/**", + "group": "internal", + "position": "after" + } + ], + "pathGroupsExcludedImportTypes": ["react"] + } + ] + } +} \ No newline at end of file diff --git a/client/.prettierrc b/client/.prettierrc new file mode 100644 index 00000000..57f6457a --- /dev/null +++ b/client/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "semi": true, + "plugins": ["prettier-plugin-tailwindcss"] +} \ No newline at end of file diff --git a/client/app/layout.tsx b/client/app/layout.tsx deleted file mode 100644 index a36cde01..00000000 --- a/client/app/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/client/lib/queryClient.ts b/client/lib/queryClient.ts deleted file mode 100644 index 4dea80de..00000000 --- a/client/lib/queryClient.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { router } from "@shared/contracts"; -import { initQueryClient } from "@ts-rest/react-query"; - -// TODO: We need to get the baseUrl from the environment, pending to decide where to store this data. Right now the API -// is getting all the conf from the shared folder - -export const client = initQueryClient(router, { - validateResponse: true, - baseUrl: "localhost:4000", -}); diff --git a/client/middleware.ts b/client/middleware.ts new file mode 100644 index 00000000..3c52c3d8 --- /dev/null +++ b/client/middleware.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; + +import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; + +const PRIVATE_PAGES = /^(\/profile)/; + +export default function middleware(req: NextRequestWithAuth) { + if (PRIVATE_PAGES.test(req.nextUrl.pathname)) { + return withAuth(req, { + pages: { + signIn: "/auth/signin", + }, + }); + } + + return NextResponse.next(); +} + +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) + * - health (health check endpoint) + */ + "/((?!api|_next/static|_next/image|favicon.ico|health).*)", + ], +}; diff --git a/client/package.json b/client/package.json index 617b4794..8750773f 100644 --- a/client/package.json +++ b/client/package.json @@ -9,8 +9,10 @@ "lint": "next lint" }, "dependencies": { - "@ts-rest/react-query": "^3.51.0", + "@tanstack/react-query": "5.59.0", + "@ts-rest/react-query": "3.51.0", "next": "14.2.10", + "next-auth": "4.24.8", "react": "^18", "react-dom": "^18" }, @@ -21,6 +23,8 @@ "eslint": "^8", "eslint-config-next": "14.2.8", "postcss": "^8", + "prettier": "3.3.3", + "prettier-plugin-tailwindcss": "0.6.8", "tailwindcss": "^3.4.1", "typescript": "catalog:" } diff --git a/client/src/app/auth/api/[...nextauth]/config.ts b/client/src/app/auth/api/[...nextauth]/config.ts new file mode 100644 index 00000000..6e23ccef --- /dev/null +++ b/client/src/app/auth/api/[...nextauth]/config.ts @@ -0,0 +1,96 @@ +import { UserWithAccessToken } from "@shared/dtos/user.dto"; +import { LogInSchema } from "@shared/schemas/auth/login.schema"; +import type { + GetServerSidePropsContext, + NextApiRequest, + NextApiResponse, +} from "next"; +import { getServerSession, NextAuthOptions } from "next-auth"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { JWT } from "next-auth/jwt"; +import Credentials from "next-auth/providers/credentials"; + +import { client } from "@/lib/queryClient"; + +declare module "next-auth" { + interface Session { + user: UserWithAccessToken["user"]; + accessToken: UserWithAccessToken["accessToken"]; + } + + interface User extends UserWithAccessToken {} +} + +declare module "next-auth/jwt" { + interface JWT { + user: UserWithAccessToken["user"]; + accessToken: UserWithAccessToken["accessToken"]; + } +} + +export const config = { + providers: [ + Credentials({ + // @ts-expect-error - why is so hard to type this? + authorize: async (credentials) => { + let access: UserWithAccessToken | null = null; + + const { email, password } = await LogInSchema.parseAsync(credentials); + + const response = await client.auth.login.mutation({ + body: { + email, + password, + }, + }); + + if (response.status === 201) { + access = response.body; + } + + if (!access) { + if (response.status === 401) { + throw new Error( + response.body.errors?.[0]?.title || "unknown error", + ); + } + } + + return access; + }, + }), + ], + callbacks: { + jwt({ token, user: access, trigger, session }) { + if (access) { + token.user = access.user; + token.accessToken = access.accessToken; + } + + if (trigger === "update") { + token.user.email = session.email; + } + + return token; + }, + session({ session, token }) { + return { + ...session, + user: token.user, + accessToken: token.accessToken, + }; + }, + }, + pages: { + signIn: "/auth/signin", + }, +} as NextAuthOptions; + +export function auth( + ...args: + | [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] + | [NextApiRequest, NextApiResponse] + | [] +) { + return getServerSession(...args, config); +} diff --git a/client/src/app/auth/api/[...nextauth]/route.ts b/client/src/app/auth/api/[...nextauth]/route.ts new file mode 100644 index 00000000..6a32c4a6 --- /dev/null +++ b/client/src/app/auth/api/[...nextauth]/route.ts @@ -0,0 +1,7 @@ +import NextAuth from "next-auth"; + +import { config } from "./config"; + +const handler = NextAuth(config); + +export { handler as GET, handler as POST }; diff --git a/client/app/favicon.ico b/client/src/app/favicon.ico similarity index 100% rename from client/app/favicon.ico rename to client/src/app/favicon.ico diff --git a/client/app/fonts/GeistMonoVF.woff b/client/src/app/fonts/GeistMonoVF.woff similarity index 100% rename from client/app/fonts/GeistMonoVF.woff rename to client/src/app/fonts/GeistMonoVF.woff diff --git a/client/app/fonts/GeistVF.woff b/client/src/app/fonts/GeistVF.woff similarity index 100% rename from client/app/fonts/GeistVF.woff rename to client/src/app/fonts/GeistVF.woff diff --git a/client/app/globals.css b/client/src/app/globals.css similarity index 100% rename from client/app/globals.css rename to client/src/app/globals.css diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx new file mode 100644 index 00000000..ef90bd63 --- /dev/null +++ b/client/src/app/layout.tsx @@ -0,0 +1,34 @@ +import { Inter } from "next/font/google"; + +import type { Metadata } from "next"; +import "@/app/globals.css"; +import { getServerSession } from "next-auth"; + +import { config } from "@/app/auth/api/[...nextauth]/config"; + +import LayoutProviders from "./providers"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Blue Carbon Cost", + description: "[TBD]", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const session = await getServerSession(config); + + return ( + + + +
{children}
+ + +
+ ); +} diff --git a/client/app/page.tsx b/client/src/app/page.tsx similarity index 70% rename from client/app/page.tsx rename to client/src/app/page.tsx index b4e291ed..688cafd1 100644 --- a/client/app/page.tsx +++ b/client/src/app/page.tsx @@ -2,8 +2,8 @@ import Image from "next/image"; export default function Home() { return ( -
-
+
+
-
    + +
    1. Placeholder for{" "} - + Blue Carbon Cost Tool .
    -
-