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 (
-
-
+
+
-
+
+
-
Placeholder for{" "}
-
+
Blue Carbon Cost Tool
.
-
+
-