From 50f7cc5ce2b57047ac415af9a47fd465a729bd6e Mon Sep 17 00:00:00 2001
From: Josh Daniel <joshxfi.dev@gmail.com>
Date: Sun, 25 Aug 2024 16:44:30 +0800
Subject: [PATCH] feat: add shared package for reused pages

---
 apps/social/next.config.mjs                   |   2 +-
 apps/social/package.json                      |   1 +
 .../login/google/callback/route.ts            | 159 +----------
 .../(authentication)/login/google/route.ts    |  32 +--
 .../app/(authentication)/login/loading.tsx    |  29 +-
 .../src/app/(authentication)/login/page.tsx   |  50 +---
 .../app/(authentication)/register/loading.tsx |  45 +--
 .../app/(authentication)/register/page.tsx    |  50 +---
 packages/shared/.eslintrc.json                |   3 +
 packages/shared/.gitignore                    |  36 +++
 packages/shared/README.md                     |  36 +++
 packages/shared/actions.ts                    | 259 ++++++++++++++++++
 .../shared/app}/login/components/form.tsx     |   0
 .../app}/login/components/login-button.tsx    |   0
 packages/shared/app/login/loading.tsx         |  28 ++
 packages/shared/app/login/page.tsx            |  42 +++
 .../app}/register/components/button.tsx       |   0
 .../shared/app}/register/components/form.tsx  |   0
 packages/shared/app/register/loading.tsx      |  44 +++
 packages/shared/app/register/page.tsx         |  49 ++++
 packages/shared/lib/auth.ts                   |  84 ++++++
 packages/shared/next.config.mjs               |   4 +
 packages/shared/package.json                  |  47 ++++
 packages/shared/postcss.config.cjs            |   1 +
 .../shared/routes/google/callback/route.ts    | 158 +++++++++++
 packages/shared/routes/google/route.ts        |  31 +++
 packages/shared/tailwind.config.ts            |   1 +
 packages/shared/tsconfig.json                 |  26 ++
 packages/ui/postcss.config.js                 |   1 -
 pnpm-lock.yaml                                | 125 ++++++++-
 30 files changed, 979 insertions(+), 364 deletions(-)
 create mode 100644 packages/shared/.eslintrc.json
 create mode 100644 packages/shared/.gitignore
 create mode 100644 packages/shared/README.md
 create mode 100644 packages/shared/actions.ts
 rename {apps/social/src/app/(authentication) => packages/shared/app}/login/components/form.tsx (100%)
 rename {apps/social/src/app/(authentication) => packages/shared/app}/login/components/login-button.tsx (100%)
 create mode 100644 packages/shared/app/login/loading.tsx
 create mode 100644 packages/shared/app/login/page.tsx
 rename {apps/social/src/app/(authentication) => packages/shared/app}/register/components/button.tsx (100%)
 rename {apps/social/src/app/(authentication) => packages/shared/app}/register/components/form.tsx (100%)
 create mode 100644 packages/shared/app/register/loading.tsx
 create mode 100644 packages/shared/app/register/page.tsx
 create mode 100644 packages/shared/lib/auth.ts
 create mode 100644 packages/shared/next.config.mjs
 create mode 100644 packages/shared/package.json
 create mode 100644 packages/shared/postcss.config.cjs
 create mode 100644 packages/shared/routes/google/callback/route.ts
 create mode 100644 packages/shared/routes/google/route.ts
 create mode 100644 packages/shared/tailwind.config.ts
 create mode 100644 packages/shared/tsconfig.json

diff --git a/apps/social/next.config.mjs b/apps/social/next.config.mjs
index 32f7991c..27ba365c 100644
--- a/apps/social/next.config.mjs
+++ b/apps/social/next.config.mjs
@@ -8,7 +8,7 @@ const nextConfig = {
     removeConsole: process.env.NODE_ENV === "production",
   },
 
-  transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"],
+  transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql", "@umamin/shared"],
   images: {
     remotePatterns: [
       {
diff --git a/apps/social/package.json b/apps/social/package.json
index 95f58f2d..439c8b96 100644
--- a/apps/social/package.json
+++ b/apps/social/package.json
@@ -24,6 +24,7 @@
     "@umamin/db": "workspace:*",
     "@umamin/gql": "workspace:*",
     "@umamin/ui": "workspace:*",
+    "@umamin/shared": "workspace:*",
     "@urql/core": "^5.0.5",
     "@urql/exchange-graphcache": "^7.1.1",
     "@urql/exchange-persisted": "^4.3.0",
diff --git a/apps/social/src/app/(authentication)/login/google/callback/route.ts b/apps/social/src/app/(authentication)/login/google/callback/route.ts
index 261b8560..00f5c9ad 100644
--- a/apps/social/src/app/(authentication)/login/google/callback/route.ts
+++ b/apps/social/src/app/(authentication)/login/google/callback/route.ts
@@ -1,158 +1 @@
-import { nanoid } from "nanoid";
-import { generateId } from "lucia";
-import { cookies } from "next/headers";
-import { db, and, eq } from "@umamin/db";
-import { OAuth2RequestError } from "arctic";
-import {
-  user as userSchema,
-  account as accountSchema,
-} from "@umamin/db/schema/user";
-
-import { getSession, google, lucia } from "@/lib/auth";
-
-export async function GET(request: Request): Promise<Response> {
-  const url = new URL(request.url);
-  const code = url.searchParams.get("code");
-  const state = url.searchParams.get("state");
-
-  const storedState = cookies().get("google_oauth_state")?.value ?? null;
-  const storedCodeVerifier = cookies().get("code_verifier")?.value ?? null;
-
-  if (
-    !code ||
-    !state ||
-    !storedState ||
-    !storedCodeVerifier ||
-    state !== storedState
-  ) {
-    return new Response(null, {
-      status: 400,
-    });
-  }
-
-  try {
-    const tokens = await google.validateAuthorizationCode(
-      code,
-      storedCodeVerifier,
-    );
-
-    const googleUserResponse = await fetch(
-      "https://openidconnect.googleapis.com/v1/userinfo",
-      {
-        headers: {
-          Authorization: `Bearer ${tokens.accessToken}`,
-        },
-      },
-    );
-
-    const googleUser: GoogleUser = await googleUserResponse.json();
-
-    const { user } = await getSession();
-
-    const existingUser = await db.query.account.findFirst({
-      where: and(
-        eq(accountSchema.providerId, "google"),
-        eq(accountSchema.providerUserId, googleUser.sub),
-      ),
-    });
-
-    if (user && existingUser) {
-      return new Response(null, {
-        status: 302,
-        headers: {
-          Location: "/settings?error=already_linked",
-        },
-      });
-    } else if (user) {
-      await db
-        .update(userSchema)
-        .set({
-          imageUrl: googleUser.picture,
-        })
-        .where(eq(userSchema.id, user.id));
-
-      await db.insert(accountSchema).values({
-        providerId: "google",
-        providerUserId: googleUser.sub,
-        userId: user.id,
-        picture: googleUser.picture,
-        email: googleUser.email,
-      });
-
-      return new Response(null, {
-        status: 302,
-        headers: {
-          Location: "/settings",
-        },
-      });
-    }
-
-    if (existingUser) {
-      const session = await lucia.createSession(existingUser.userId, {});
-      const sessionCookie = lucia.createSessionCookie(session.id);
-
-      cookies().set(
-        sessionCookie.name,
-        sessionCookie.value,
-        sessionCookie.attributes,
-      );
-
-      return new Response(null, {
-        status: 302,
-        headers: {
-          Location: "/login",
-        },
-      });
-    }
-
-    const usernameId = generateId(5);
-    const userId = nanoid();
-
-    await db.insert(userSchema).values({
-      id: userId,
-      imageUrl: googleUser.picture,
-      username: `umamin_${usernameId}`,
-    });
-
-    await db.insert(accountSchema).values({
-      providerId: "google",
-      providerUserId: googleUser.sub,
-      userId,
-      picture: googleUser.picture,
-      email: googleUser.email,
-    });
-
-    const session = await lucia.createSession(userId, {});
-    const sessionCookie = lucia.createSessionCookie(session.id);
-
-    cookies().set(
-      sessionCookie.name,
-      sessionCookie.value,
-      sessionCookie.attributes,
-    );
-
-    return new Response(null, {
-      status: 302,
-      headers: {
-        Location: "/login",
-      },
-    });
-  } catch (err: any) {
-    console.log(err);
-    if (err instanceof OAuth2RequestError) {
-      return new Response(null, {
-        status: 400,
-      });
-    }
-
-    return new Response(null, {
-      status: 500,
-    });
-  }
-}
-
-interface GoogleUser {
-  sub: string;
-  picture: string;
-  email: string;
-}
+export { GET } from "@umamin/shared/routes/google/callback/route";
diff --git a/apps/social/src/app/(authentication)/login/google/route.ts b/apps/social/src/app/(authentication)/login/google/route.ts
index 88f65bc7..7c78acb6 100644
--- a/apps/social/src/app/(authentication)/login/google/route.ts
+++ b/apps/social/src/app/(authentication)/login/google/route.ts
@@ -1,31 +1 @@
-import { generateState, generateCodeVerifier } from "arctic";
-import { cookies } from "next/headers";
-import { google } from "@/lib/auth";
-
-export async function GET(): Promise<Response> {
-  const state = generateState();
-  const codeVerifier = generateCodeVerifier();
-  const url = await google.createAuthorizationURL(state, codeVerifier, {
-    scopes: ["profile", "email"],
-  });
-
-  url.searchParams.set("access_type", "offline");
-
-  cookies().set("google_oauth_state", state, {
-    path: "/",
-    secure: process.env.NODE_ENV === "production",
-    httpOnly: true,
-    maxAge: 60 * 10,
-    sameSite: "lax",
-  });
-
-  cookies().set("code_verifier", codeVerifier, {
-    path: "/",
-    secure: process.env.NODE_ENV === "production",
-    httpOnly: true,
-    maxAge: 60 * 10,
-    sameSite: "lax",
-  });
-
-  return Response.redirect(url);
-}
+export { GET } from "@umamin/shared/routes/google/route";
diff --git a/apps/social/src/app/(authentication)/login/loading.tsx b/apps/social/src/app/(authentication)/login/loading.tsx
index 5ba9d85f..2012bff6 100644
--- a/apps/social/src/app/(authentication)/login/loading.tsx
+++ b/apps/social/src/app/(authentication)/login/loading.tsx
@@ -1,28 +1 @@
-import { Skeleton } from "@umamin/ui/components/skeleton";
-
-export default function Loading() {
-  return (
-    <div className="max-w-lg md:max-w-md container mt-36 [&>div]:gap-3 [&>div]:flex [&>div]:flex-col flex gap-8 flex-col">
-      <div>
-        <Skeleton className="w-2/5 h-[25px] rounded-md" />
-        <Skeleton className="w-1/2 h-[10px] rounded-md" />
-      </div>
-
-      <div>
-        <Skeleton className="w-1/5 h-[10px] rounded-md" />
-        <Skeleton className="w-full h-[30px] rounded-md" />
-      </div>
-
-      <div>
-        <Skeleton className="w-1/5 h-[10px] rounded-md" />
-        <Skeleton className="w-full h-[30px] rounded-md" />
-      </div>
-
-      <div>
-        <Skeleton className="w-full h-[30px] rounded-md" />
-        <Skeleton className="w-full h-[30px] rounded-md" />
-        <Skeleton className="mx-auto w-1/2 h-[10px] rounded-md" />
-      </div>
-    </div>
-  );
-}
+export { default } from "@umamin/shared/app/login/loading";
diff --git a/apps/social/src/app/(authentication)/login/page.tsx b/apps/social/src/app/(authentication)/login/page.tsx
index a2e86b09..4842a9f4 100644
--- a/apps/social/src/app/(authentication)/login/page.tsx
+++ b/apps/social/src/app/(authentication)/login/page.tsx
@@ -1,14 +1,3 @@
-import Link from "next/link";
-import dynamic from "next/dynamic";
-import { getSession } from "@/lib/auth";
-import { redirect } from "next/navigation";
-import { LoginForm } from "./components/form";
-
-const BrowserWarning = dynamic(
-  () => import("@umamin/ui/components/browser-warning"),
-  { ssr: false }
-);
-
 export const metadata = {
   title: "Umamin Social — Login",
   description:
@@ -37,41 +26,4 @@ export const metadata = {
   },
 };
 
-export default async function Login() {
-  const { user } = await getSession();
-
-  if (user) {
-    redirect("/");
-  }
-
-  return (
-    <section className="container pt-10 flex flex-col items-center">
-      <BrowserWarning />
-      <div className="border-b-2 border-muted border-dashed pb-5 mb-10 sm:text-center inline-block">
-        <h1 className="font-bold md:text-6xl text-[10vw] leading-none dark:bg-gradient-to-b from-foreground dark:to-zinc-400 bg-clip-text bg-zinc-800 text-transparent tracking-tighter text-nowrap">
-          Umamin Social
-        </h1>
-        <p className="text-muted-foreground md:text-lg mt-2">
-          The <span className="text-foreground font-medium">Umamin v2.0</span>{" "}
-          Next generation open-source social platform
-        </p>
-      </div>
-
-      <div className="max-w-md w-full mx-auto">
-        <div className="mb-6">
-          <h2 className="text-2xl tracking-tight font-semibold">Account</h2>
-          <p className="text-sm text-muted-foreground">
-            Proceed with your Umamin v2.0 profile
-          </p>
-        </div>
-        <LoginForm />
-        <div className="mt-4 text-center text-sm w-full">
-          Don&apos;t have an account?{" "}
-          <Link href="/register" className="underline">
-            Sign up
-          </Link>
-        </div>
-      </div>
-    </section>
-  );
-}
+export { default } from "@umamin/shared/app/login/page";
diff --git a/apps/social/src/app/(authentication)/register/loading.tsx b/apps/social/src/app/(authentication)/register/loading.tsx
index d623ffdd..ff4b79de 100644
--- a/apps/social/src/app/(authentication)/register/loading.tsx
+++ b/apps/social/src/app/(authentication)/register/loading.tsx
@@ -1,44 +1 @@
-import { Skeleton } from "@umamin/ui/components/skeleton";
-
-export default function Loading() {
-  return (
-    <div className="container max-w-xl lg:mt-36 mt-28 mx-auto ">
-      <div className="flex flex-col gap-4">
-        <div className="flex gap-3">
-          <Skeleton className="size-16 md:size-20 rounded-full" />
-
-          <div className="flex flex-col gap-2">
-            <Skeleton className="h-[20px] w-[80px] rounded-md" />
-            <Skeleton className="h-[15px] w-[50px] rounded-md" />
-          </div>
-        </div>
-
-        <div className="flex flex-col gap-2">
-          <div className="flex gap-1 items-center">
-            <Skeleton className="size-[15px] rounded-full" />
-            <Skeleton className="h-[10px] w-[130px] rounded-md" />
-          </div>
-
-          <div className="flex gap-1 items-center">
-            <Skeleton className="size-[15px] rounded-full" />
-            <Skeleton className="h-[10px] w-[130px] rounded-md" />
-          </div>
-        </div>
-      </div>
-
-      <div className="space-y-5 mt-8">
-        <div>
-          <div className="flex justify-around">
-            <Skeleton className="h-[15px] w-[90px] rounded-md" />
-            <Skeleton className="h-[15px] w-[90px] rounded-md" />
-          </div>
-
-          <Skeleton className="w-full h-[2px] rounded-md mt-2" />
-        </div>
-
-        <Skeleton className="w-full h-[200px] rounded-md" />
-        <Skeleton className="w-full h-[200px] rounded-md" />
-      </div>
-    </div>
-  );
-}
+export { default } from "@umamin/shared/app/register/loading";
diff --git a/apps/social/src/app/(authentication)/register/page.tsx b/apps/social/src/app/(authentication)/register/page.tsx
index beda3f71..bf7f0719 100644
--- a/apps/social/src/app/(authentication)/register/page.tsx
+++ b/apps/social/src/app/(authentication)/register/page.tsx
@@ -1,14 +1,3 @@
-import Link from "next/link";
-import dynamic from "next/dynamic";
-import { getSession } from "@/lib/auth";
-import { redirect } from "next/navigation";
-import { RegisterForm } from "./components/form";
-
-const BrowserWarning = dynamic(
-  () => import("@umamin/ui/components/browser-warning"),
-  { ssr: false }
-);
-
 export const metadata = {
   title: "Umamin Social — Register",
   description:
@@ -37,41 +26,4 @@ export const metadata = {
   },
 };
 
-export default async function Register() {
-  const { user } = await getSession();
-
-  if (user) {
-    redirect("/");
-  }
-
-  return (
-    <section className="max-w-lg md:max-w-md container mt-24 min-h-screen">
-      <BrowserWarning />
-
-      <div className="mb-6">
-        <h2 className="text-2xl tracking-tight font-semibold">
-          Umamin Account
-        </h2>
-        <p className="text-sm text-muted-foreground">
-          By creating an account, you agree to our{" "}
-          <Link href="/privacy" className="text-zinc-200">
-            Privacy Policy
-          </Link>{" "}
-          and{" "}
-          <Link href="/terms" className="text-zinc-200">
-            Terms of Service
-          </Link>
-        </p>
-      </div>
-
-      <RegisterForm />
-
-      <div className="mt-4 text-center text-sm w-full">
-        Already have an account?{" "}
-        <Link href="/login" className="underline">
-          Login
-        </Link>
-      </div>
-    </section>
-  );
-}
+export { default } from "@umamin/shared/app/register/page";
diff --git a/packages/shared/.eslintrc.json b/packages/shared/.eslintrc.json
new file mode 100644
index 00000000..bffb357a
--- /dev/null
+++ b/packages/shared/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+  "extends": "next/core-web-vitals"
+}
diff --git a/packages/shared/.gitignore b/packages/shared/.gitignore
new file mode 100644
index 00000000..fd3dbb57
--- /dev/null
+++ b/packages/shared/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/packages/shared/README.md b/packages/shared/README.md
new file mode 100644
index 00000000..c4033664
--- /dev/null
+++ b/packages/shared/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/packages/shared/actions.ts b/packages/shared/actions.ts
new file mode 100644
index 00000000..ee86c9bd
--- /dev/null
+++ b/packages/shared/actions.ts
@@ -0,0 +1,259 @@
+"use server";
+
+import { nanoid } from "nanoid";
+import { db, eq } from "@umamin/db";
+import { cookies } from "next/headers";
+import { redirect } from "next/navigation";
+import { hash, verify } from "@node-rs/argon2";
+import {
+  user as userSchema,
+  account as accountSchema,
+} from "@umamin/db/schema/user";
+import { note as noteSchema } from "@umamin/db/schema/note";
+import { message as messageSchema } from "@umamin/db/schema/message";
+
+import { getSession, lucia } from "./lib/auth";
+import { z } from "zod";
+
+export async function logout(): Promise<ActionResult> {
+  const { session } = await getSession();
+
+  if (!session) {
+    throw new Error("Unauthorized");
+  }
+
+  await lucia.invalidateSession(session.id);
+
+  const sessionCookie = lucia.createBlankSessionCookie();
+  cookies().set(
+    sessionCookie.name,
+    sessionCookie.value,
+    sessionCookie.attributes,
+  );
+
+  return redirect("/login");
+}
+
+const signupSchema = z
+  .object({
+    username: z
+      .string()
+      .min(5, {
+        message: "Username must be at least 5 characters",
+      })
+      .max(20, {
+        message: "Username must not exceed 20 characters",
+      })
+      .refine((url) => /^[a-zA-Z0-9_-]+$/.test(url), {
+        message: "Username must be alphanumeric with no spaces",
+      }),
+    password: z
+      .string()
+      .min(5, {
+        message: "Password must be at least 5 characters",
+      })
+      .max(255, {
+        message: "Password must not exceed 255 characters",
+      }),
+    confirmPassword: z.string(),
+  })
+  .refine(
+    (values) => {
+      return values.password === values.confirmPassword;
+    },
+    {
+      message: "Password does not match",
+      path: ["confirmPassword"],
+    },
+  );
+
+export async function signup(_: any, formData: FormData) {
+  const validatedFields = signupSchema.safeParse({
+    username: formData.get("username"),
+    password: formData.get("password"),
+    confirmPassword: formData.get("confirmPassword"),
+  });
+
+  if (!validatedFields.success) {
+    return {
+      errors: validatedFields.error.flatten().fieldErrors,
+    };
+  }
+
+  const passwordHash = await hash(validatedFields.data.password, {
+    memoryCost: 19456,
+    timeCost: 2,
+    outputLen: 32,
+    parallelism: 1,
+  });
+
+  const userId = nanoid();
+
+  try {
+    await db.insert(userSchema).values({
+      id: userId,
+      username: validatedFields.data.username.toLowerCase(),
+      passwordHash,
+    });
+  } catch (err: any) {
+    if (err.code === "SQLITE_CONSTRAINT") {
+      if (err.message.includes("user.username")) {
+        return {
+          errors: {
+            username: ["Username already taken"],
+          },
+        };
+      }
+    }
+
+    throw new Error("Something went wrong");
+  }
+
+  const session = await lucia.createSession(userId, {});
+  const sessionCookie = lucia.createSessionCookie(session.id);
+
+  cookies().set(
+    sessionCookie.name,
+    sessionCookie.value,
+    sessionCookie.attributes,
+  );
+
+  return redirect("/inbox");
+}
+
+export async function login(_: any, formData: FormData): Promise<ActionResult> {
+  const username = formData.get("username");
+
+  if (
+    typeof username !== "string" ||
+    username.length < 5 ||
+    username.length > 20 ||
+    !/^[a-zA-Z0-9_-]+$/.test(username)
+  ) {
+    return {
+      error: "Incorrect username or password",
+    };
+  }
+
+  const password = formData.get("password");
+
+  if (
+    typeof password !== "string" ||
+    password.length < 5 ||
+    password.length > 255
+  ) {
+    return {
+      error: "Incorrect username or password",
+    };
+  }
+
+  const existingUser = await db.query.user.findFirst({
+    where: eq(userSchema.username, username.toLowerCase()),
+  });
+
+  if (!existingUser || !existingUser.passwordHash) {
+    return {
+      error: "Incorrect username or password",
+    };
+  }
+
+  const validPassword = await verify(existingUser.passwordHash, password, {
+    memoryCost: 19456,
+    timeCost: 2,
+    outputLen: 32,
+    parallelism: 1,
+  });
+
+  if (!validPassword) {
+    return {
+      error: "Incorrect username or password",
+    };
+  }
+
+  const session = await lucia.createSession(existingUser.id, {});
+  const sessionCookie = lucia.createSessionCookie(session.id);
+  cookies().set(
+    sessionCookie.name,
+    sessionCookie.value,
+    sessionCookie.attributes,
+  );
+
+  return redirect("/inbox");
+}
+
+export async function updatePassword({
+  currentPassword,
+  password,
+}: {
+  currentPassword?: string;
+  password: string;
+}): Promise<ActionResult> {
+  const { user } = await getSession();
+
+  if (!user) {
+    throw new Error("Unauthorized");
+  }
+
+  if (currentPassword && user.passwordHash) {
+    const validPassword = await verify(user.passwordHash, currentPassword, {
+      memoryCost: 19456,
+      timeCost: 2,
+      outputLen: 32,
+      parallelism: 1,
+    });
+
+    if (!validPassword) {
+      return {
+        error: "Incorrect password",
+      };
+    }
+  }
+
+  const passwordHash = await hash(password, {
+    memoryCost: 19456,
+    timeCost: 2,
+    outputLen: 32,
+    parallelism: 1,
+  });
+
+  await db
+    .update(userSchema)
+    .set({ passwordHash })
+    .where(eq(userSchema.id, user.id));
+
+  return redirect("/settings");
+}
+
+export async function deleteAccount() {
+  const { user } = await getSession();
+
+  if (!user) {
+    throw new Error("Unauthorized");
+  }
+
+  try {
+    await db.batch([
+      db.delete(messageSchema).where(eq(messageSchema.receiverId, user.id)),
+      db.delete(accountSchema).where(eq(accountSchema.userId, user.id)),
+      db.delete(noteSchema).where(eq(noteSchema.userId, user.id)),
+      db.delete(userSchema).where(eq(userSchema.id, user.id)),
+    ]);
+
+    await lucia.invalidateSession(user.id);
+
+    const sessionCookie = lucia.createBlankSessionCookie();
+    cookies().set(
+      sessionCookie.name,
+      sessionCookie.value,
+      sessionCookie.attributes,
+    );
+  } catch (err) {
+    throw new Error("Failed to delete account");
+  }
+
+  return redirect("/login");
+}
+
+interface ActionResult {
+  error: string | null;
+}
diff --git a/apps/social/src/app/(authentication)/login/components/form.tsx b/packages/shared/app/login/components/form.tsx
similarity index 100%
rename from apps/social/src/app/(authentication)/login/components/form.tsx
rename to packages/shared/app/login/components/form.tsx
diff --git a/apps/social/src/app/(authentication)/login/components/login-button.tsx b/packages/shared/app/login/components/login-button.tsx
similarity index 100%
rename from apps/social/src/app/(authentication)/login/components/login-button.tsx
rename to packages/shared/app/login/components/login-button.tsx
diff --git a/packages/shared/app/login/loading.tsx b/packages/shared/app/login/loading.tsx
new file mode 100644
index 00000000..5ba9d85f
--- /dev/null
+++ b/packages/shared/app/login/loading.tsx
@@ -0,0 +1,28 @@
+import { Skeleton } from "@umamin/ui/components/skeleton";
+
+export default function Loading() {
+  return (
+    <div className="max-w-lg md:max-w-md container mt-36 [&>div]:gap-3 [&>div]:flex [&>div]:flex-col flex gap-8 flex-col">
+      <div>
+        <Skeleton className="w-2/5 h-[25px] rounded-md" />
+        <Skeleton className="w-1/2 h-[10px] rounded-md" />
+      </div>
+
+      <div>
+        <Skeleton className="w-1/5 h-[10px] rounded-md" />
+        <Skeleton className="w-full h-[30px] rounded-md" />
+      </div>
+
+      <div>
+        <Skeleton className="w-1/5 h-[10px] rounded-md" />
+        <Skeleton className="w-full h-[30px] rounded-md" />
+      </div>
+
+      <div>
+        <Skeleton className="w-full h-[30px] rounded-md" />
+        <Skeleton className="w-full h-[30px] rounded-md" />
+        <Skeleton className="mx-auto w-1/2 h-[10px] rounded-md" />
+      </div>
+    </div>
+  );
+}
diff --git a/packages/shared/app/login/page.tsx b/packages/shared/app/login/page.tsx
new file mode 100644
index 00000000..537cd831
--- /dev/null
+++ b/packages/shared/app/login/page.tsx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+import dynamic from "next/dynamic";
+import { getSession } from "@/lib/auth";
+import { redirect } from "next/navigation";
+import { LoginForm } from "./components/form";
+
+const BrowserWarning = dynamic(
+  () => import("@umamin/ui/components/browser-warning"),
+  { ssr: false }
+);
+
+export default async function Login() {
+  const { user } = await getSession();
+
+  if (user) {
+    redirect("/inbox");
+  }
+
+  return (
+    <section className="max-w-lg md:max-w-md container mt-36 min-h-screen">
+      <BrowserWarning />
+
+      <div className="mb-6">
+        <h2 className="text-2xl tracking-tight font-semibold">
+          Umamin Account
+        </h2>
+        <p className="text-sm text-muted-foreground">
+          Proceed with your Umamin v2.0 profile
+        </p>
+      </div>
+
+      <LoginForm />
+
+      <div className="mt-4 text-center text-sm w-full">
+        Don&apos;t have an account?{" "}
+        <Link href="/register" className="underline">
+          Sign up
+        </Link>
+      </div>
+    </section>
+  );
+}
diff --git a/apps/social/src/app/(authentication)/register/components/button.tsx b/packages/shared/app/register/components/button.tsx
similarity index 100%
rename from apps/social/src/app/(authentication)/register/components/button.tsx
rename to packages/shared/app/register/components/button.tsx
diff --git a/apps/social/src/app/(authentication)/register/components/form.tsx b/packages/shared/app/register/components/form.tsx
similarity index 100%
rename from apps/social/src/app/(authentication)/register/components/form.tsx
rename to packages/shared/app/register/components/form.tsx
diff --git a/packages/shared/app/register/loading.tsx b/packages/shared/app/register/loading.tsx
new file mode 100644
index 00000000..d623ffdd
--- /dev/null
+++ b/packages/shared/app/register/loading.tsx
@@ -0,0 +1,44 @@
+import { Skeleton } from "@umamin/ui/components/skeleton";
+
+export default function Loading() {
+  return (
+    <div className="container max-w-xl lg:mt-36 mt-28 mx-auto ">
+      <div className="flex flex-col gap-4">
+        <div className="flex gap-3">
+          <Skeleton className="size-16 md:size-20 rounded-full" />
+
+          <div className="flex flex-col gap-2">
+            <Skeleton className="h-[20px] w-[80px] rounded-md" />
+            <Skeleton className="h-[15px] w-[50px] rounded-md" />
+          </div>
+        </div>
+
+        <div className="flex flex-col gap-2">
+          <div className="flex gap-1 items-center">
+            <Skeleton className="size-[15px] rounded-full" />
+            <Skeleton className="h-[10px] w-[130px] rounded-md" />
+          </div>
+
+          <div className="flex gap-1 items-center">
+            <Skeleton className="size-[15px] rounded-full" />
+            <Skeleton className="h-[10px] w-[130px] rounded-md" />
+          </div>
+        </div>
+      </div>
+
+      <div className="space-y-5 mt-8">
+        <div>
+          <div className="flex justify-around">
+            <Skeleton className="h-[15px] w-[90px] rounded-md" />
+            <Skeleton className="h-[15px] w-[90px] rounded-md" />
+          </div>
+
+          <Skeleton className="w-full h-[2px] rounded-md mt-2" />
+        </div>
+
+        <Skeleton className="w-full h-[200px] rounded-md" />
+        <Skeleton className="w-full h-[200px] rounded-md" />
+      </div>
+    </div>
+  );
+}
diff --git a/packages/shared/app/register/page.tsx b/packages/shared/app/register/page.tsx
new file mode 100644
index 00000000..1313bb40
--- /dev/null
+++ b/packages/shared/app/register/page.tsx
@@ -0,0 +1,49 @@
+import Link from "next/link";
+import dynamic from "next/dynamic";
+import { getSession } from "@/lib/auth";
+import { redirect } from "next/navigation";
+import { RegisterForm } from "./components/form";
+
+const BrowserWarning = dynamic(
+  () => import("@umamin/ui/components/browser-warning"),
+  { ssr: false }
+);
+
+export default async function Register() {
+  const { user } = await getSession();
+
+  if (user) {
+    redirect("/inbox");
+  }
+
+  return (
+    <section className="max-w-lg md:max-w-md container mt-36 min-h-screen">
+      <BrowserWarning />
+
+      <div className="mb-6">
+        <h2 className="text-2xl tracking-tight font-semibold">
+          Umamin Account
+        </h2>
+        <p className="text-sm text-muted-foreground">
+          By creating an account, you agree to our{" "}
+          <Link href="/privacy" className="text-zinc-200">
+            Privacy Policy
+          </Link>{" "}
+          and{" "}
+          <Link href="/terms" className="text-zinc-200">
+            Terms of Service
+          </Link>
+        </p>
+      </div>
+
+      <RegisterForm />
+
+      <div className="mt-4 text-center text-sm w-full">
+        Already have an account?{" "}
+        <Link href="/login" className="underline">
+          Login
+        </Link>
+      </div>
+    </section>
+  );
+}
diff --git a/packages/shared/lib/auth.ts b/packages/shared/lib/auth.ts
new file mode 100644
index 00000000..05f347e7
--- /dev/null
+++ b/packages/shared/lib/auth.ts
@@ -0,0 +1,84 @@
+import { cache } from "react";
+import { Google } from "arctic";
+import { cookies } from "next/headers";
+import { Lucia, Session, User } from "lucia";
+import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
+
+import { db } from "@umamin/db";
+import {
+  SelectUser,
+  user as userSchema,
+  session as sessionSchema,
+} from "@umamin/db/schema/user";
+
+export const google = new Google(
+  process.env.GOOGLE_CLIENT_ID!,
+  process.env.GOOGLE_CLIENT_SECRET!,
+  process.env.GOOGLE_REDIRECT_URI!,
+);
+
+const adapter = new DrizzleSQLiteAdapter(db, sessionSchema, userSchema);
+
+export const lucia = new Lucia(adapter, {
+  sessionCookie: {
+    expires: false,
+    attributes: {
+      secure: process.env.NODE_ENV === "production",
+    },
+  },
+  getUserAttributes: (attributes) => {
+    return {
+      ...attributes,
+    };
+  },
+});
+
+export const getSession = cache(
+  async (): Promise<
+    { user: User; session: Session } | { user: null; session: null }
+  > => {
+    const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
+
+    if (!sessionId) {
+      return {
+        user: null,
+        session: null,
+      };
+    }
+
+    const result = await lucia.validateSession(sessionId);
+
+    try {
+      if (result.session && result.session.fresh) {
+        const sessionCookie = lucia.createSessionCookie(result.session.id);
+        cookies().set(
+          sessionCookie.name,
+          sessionCookie.value,
+          sessionCookie.attributes,
+        );
+      }
+
+      if (!result.session) {
+        const sessionCookie = lucia.createBlankSessionCookie();
+        cookies().set(
+          sessionCookie.name,
+          sessionCookie.value,
+          sessionCookie.attributes,
+        );
+      }
+    } catch (err) {
+      console.log(err);
+    }
+    return result;
+  },
+);
+
+declare module "lucia" {
+  // eslint-disable-next-line no-unused-vars
+  interface Register {
+    Lucia: typeof lucia;
+    DatabaseUserAttributes: DatabaseUserAttributes;
+  }
+}
+
+interface DatabaseUserAttributes extends SelectUser {}
diff --git a/packages/shared/next.config.mjs b/packages/shared/next.config.mjs
new file mode 100644
index 00000000..4678774e
--- /dev/null
+++ b/packages/shared/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/packages/shared/package.json b/packages/shared/package.json
new file mode 100644
index 00000000..90d6a4be
--- /dev/null
+++ b/packages/shared/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "@umamin/shared",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "lint": "next lint"
+  },
+  "exports": {
+    "./app/*": "./app/*.tsx",
+    "./routes/*": "./routes/*.ts",
+    "./components": "./components/index.ts",
+    "./lib": "./utils/lib/*.ts",
+    "./actions": "./actions.ts"
+  },
+  "peerDependencies": {
+    "react": "^18",
+    "react-dom": "^18",
+    "next": "14.2.5",
+    "gql.tada": "^1.8.5",
+    "graphql": "^16.9.0",
+    "@urql/core": "^5.0.5",
+    "@urql/next": "^1.1.1",
+    "urql": "^4.1.0"
+  },
+  "dependencies": {
+    "@umamin/db": "workspace:*",
+    "@umamin/gql": "workspace:*",
+    "@umamin/ui": "workspace:*",
+    "@lucia-auth/adapter-drizzle": "^1.0.7",
+    "nanoid": "^5.0.7",
+    "zod": "^3.22.4",
+    "lucia": "^3.2.0",
+    "arctic": "^1.9.2",
+    "lucide-react": "^0.407.0",
+    "@node-rs/argon2": "^1.8.3"
+  },
+  "devDependencies": {
+    "typescript": "^5",
+    "@types/node": "^20",
+    "@types/react": "^18",
+    "@types/react-dom": "^18",
+    "postcss": "^8",
+    "tailwindcss": "^3.4.1",
+    "eslint": "^8",
+    "eslint-config-next": "14.2.6"
+  }
+}
diff --git a/packages/shared/postcss.config.cjs b/packages/shared/postcss.config.cjs
new file mode 100644
index 00000000..a4917b21
--- /dev/null
+++ b/packages/shared/postcss.config.cjs
@@ -0,0 +1 @@
+module.exports = require("@umamin/ui/postcss.config");
diff --git a/packages/shared/routes/google/callback/route.ts b/packages/shared/routes/google/callback/route.ts
new file mode 100644
index 00000000..5b346169
--- /dev/null
+++ b/packages/shared/routes/google/callback/route.ts
@@ -0,0 +1,158 @@
+import { nanoid } from "nanoid";
+import { generateId } from "lucia";
+import { cookies } from "next/headers";
+import { db, and, eq } from "@umamin/db";
+import { OAuth2RequestError } from "arctic";
+import {
+  user as userSchema,
+  account as accountSchema,
+} from "@umamin/db/schema/user";
+
+import { getSession, google, lucia } from "@/lib/auth";
+
+export async function GET(request: Request): Promise<Response> {
+  const url = new URL(request.url);
+  const code = url.searchParams.get("code");
+  const state = url.searchParams.get("state");
+
+  const storedState = cookies().get("google_oauth_state")?.value ?? null;
+  const storedCodeVerifier = cookies().get("code_verifier")?.value ?? null;
+
+  if (
+    !code ||
+    !state ||
+    !storedState ||
+    !storedCodeVerifier ||
+    state !== storedState
+  ) {
+    return new Response(null, {
+      status: 400,
+    });
+  }
+
+  try {
+    const tokens = await google.validateAuthorizationCode(
+      code,
+      storedCodeVerifier
+    );
+
+    const googleUserResponse = await fetch(
+      "https://openidconnect.googleapis.com/v1/userinfo",
+      {
+        headers: {
+          Authorization: `Bearer ${tokens.accessToken}`,
+        },
+      }
+    );
+
+    const googleUser: GoogleUser = await googleUserResponse.json();
+
+    const { user } = await getSession();
+
+    const existingUser = await db.query.account.findFirst({
+      where: and(
+        eq(accountSchema.providerId, "google"),
+        eq(accountSchema.providerUserId, googleUser.sub)
+      ),
+    });
+
+    if (user && existingUser) {
+      return new Response(null, {
+        status: 302,
+        headers: {
+          Location: "/settings?error=already_linked",
+        },
+      });
+    } else if (user) {
+      await db
+        .update(userSchema)
+        .set({
+          imageUrl: googleUser.picture,
+        })
+        .where(eq(userSchema.id, user.id));
+
+      await db.insert(accountSchema).values({
+        providerId: "google",
+        providerUserId: googleUser.sub,
+        userId: user.id,
+        picture: googleUser.picture,
+        email: googleUser.email,
+      });
+
+      return new Response(null, {
+        status: 302,
+        headers: {
+          Location: "/settings",
+        },
+      });
+    }
+
+    if (existingUser) {
+      const session = await lucia.createSession(existingUser.userId, {});
+      const sessionCookie = lucia.createSessionCookie(session.id);
+
+      cookies().set(
+        sessionCookie.name,
+        sessionCookie.value,
+        sessionCookie.attributes
+      );
+
+      return new Response(null, {
+        status: 302,
+        headers: {
+          Location: "/login",
+        },
+      });
+    }
+
+    const usernameId = generateId(5);
+    const userId = nanoid();
+
+    await db.insert(userSchema).values({
+      id: userId,
+      imageUrl: googleUser.picture,
+      username: `umamin_${usernameId}`,
+    });
+
+    await db.insert(accountSchema).values({
+      providerId: "google",
+      providerUserId: googleUser.sub,
+      userId,
+      picture: googleUser.picture,
+      email: googleUser.email,
+    });
+
+    const session = await lucia.createSession(userId, {});
+    const sessionCookie = lucia.createSessionCookie(session.id);
+
+    cookies().set(
+      sessionCookie.name,
+      sessionCookie.value,
+      sessionCookie.attributes
+    );
+
+    return new Response(null, {
+      status: 302,
+      headers: {
+        Location: "/login",
+      },
+    });
+  } catch (err: any) {
+    console.log(err);
+    if (err instanceof OAuth2RequestError) {
+      return new Response(null, {
+        status: 400,
+      });
+    }
+
+    return new Response(null, {
+      status: 500,
+    });
+  }
+}
+
+interface GoogleUser {
+  sub: string;
+  picture: string;
+  email: string;
+}
diff --git a/packages/shared/routes/google/route.ts b/packages/shared/routes/google/route.ts
new file mode 100644
index 00000000..88f65bc7
--- /dev/null
+++ b/packages/shared/routes/google/route.ts
@@ -0,0 +1,31 @@
+import { generateState, generateCodeVerifier } from "arctic";
+import { cookies } from "next/headers";
+import { google } from "@/lib/auth";
+
+export async function GET(): Promise<Response> {
+  const state = generateState();
+  const codeVerifier = generateCodeVerifier();
+  const url = await google.createAuthorizationURL(state, codeVerifier, {
+    scopes: ["profile", "email"],
+  });
+
+  url.searchParams.set("access_type", "offline");
+
+  cookies().set("google_oauth_state", state, {
+    path: "/",
+    secure: process.env.NODE_ENV === "production",
+    httpOnly: true,
+    maxAge: 60 * 10,
+    sameSite: "lax",
+  });
+
+  cookies().set("code_verifier", codeVerifier, {
+    path: "/",
+    secure: process.env.NODE_ENV === "production",
+    httpOnly: true,
+    maxAge: 60 * 10,
+    sameSite: "lax",
+  });
+
+  return Response.redirect(url);
+}
diff --git a/packages/shared/tailwind.config.ts b/packages/shared/tailwind.config.ts
new file mode 100644
index 00000000..544d0407
--- /dev/null
+++ b/packages/shared/tailwind.config.ts
@@ -0,0 +1 @@
+export * from "@umamin/ui/tailwind.config";
diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json
new file mode 100644
index 00000000..e7ff90fd
--- /dev/null
+++ b/packages/shared/tsconfig.json
@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "paths": {
+      "@/*": ["./*"]
+    }
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}
diff --git a/packages/ui/postcss.config.js b/packages/ui/postcss.config.js
index c21c0763..12a703d9 100644
--- a/packages/ui/postcss.config.js
+++ b/packages/ui/postcss.config.js
@@ -4,4 +4,3 @@ module.exports = {
     autoprefixer: {},
   },
 };
-
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a04796e..4e84e4e3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -189,6 +189,9 @@ importers:
       '@umamin/gql':
         specifier: workspace:*
         version: link:../../packages/gql
+      '@umamin/shared':
+        specifier: workspace:*
+        version: link:../../packages/shared
       '@umamin/ui':
         specifier: workspace:*
         version: link:../../packages/ui
@@ -551,7 +554,7 @@ importers:
         version: 7.18.0(eslint@8.57.0)(typescript@5.5.4)
       '@vercel/style-guide':
         specifier: ^5.2.0
-        version: 5.2.0(@next/eslint-plugin-next@14.2.5)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)
+        version: 5.2.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)
       eslint-config-prettier:
         specifier: ^9.1.0
         version: 9.1.0(eslint@8.57.0)
@@ -617,6 +620,88 @@ importers:
         specifier: ^5.5.4
         version: 5.5.4
 
+  packages/shared:
+    dependencies:
+      '@lucia-auth/adapter-drizzle':
+        specifier: ^1.0.7
+        version: 1.0.7(lucia@3.2.0)
+      '@node-rs/argon2':
+        specifier: ^1.8.3
+        version: 1.8.3
+      '@umamin/db':
+        specifier: workspace:*
+        version: link:../db
+      '@umamin/gql':
+        specifier: workspace:*
+        version: link:../gql
+      '@umamin/ui':
+        specifier: workspace:*
+        version: link:../ui
+      '@urql/core':
+        specifier: ^5.0.5
+        version: 5.0.5(graphql@16.9.0)
+      '@urql/next':
+        specifier: ^1.1.1
+        version: 1.1.1(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1))
+      arctic:
+        specifier: ^1.9.2
+        version: 1.9.2
+      gql.tada:
+        specifier: ^1.8.5
+        version: 1.8.5(graphql@16.9.0)(typescript@5.5.4)
+      graphql:
+        specifier: ^16.9.0
+        version: 16.9.0
+      lucia:
+        specifier: ^3.2.0
+        version: 3.2.0
+      lucide-react:
+        specifier: ^0.407.0
+        version: 0.407.0(react@18.3.1)
+      nanoid:
+        specifier: ^5.0.7
+        version: 5.0.7
+      next:
+        specifier: 14.2.5
+        version: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      react:
+        specifier: ^18
+        version: 18.3.1
+      react-dom:
+        specifier: ^18
+        version: 18.3.1(react@18.3.1)
+      urql:
+        specifier: ^4.1.0
+        version: 4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1)
+      zod:
+        specifier: ^3.22.4
+        version: 3.23.8
+    devDependencies:
+      '@types/node':
+        specifier: ^20
+        version: 20.14.13
+      '@types/react':
+        specifier: ^18
+        version: 18.3.3
+      '@types/react-dom':
+        specifier: ^18
+        version: 18.3.0
+      eslint:
+        specifier: ^8
+        version: 8.57.0
+      eslint-config-next:
+        specifier: 14.2.6
+        version: 14.2.6(eslint@8.57.0)(typescript@5.5.4)
+      postcss:
+        specifier: ^8
+        version: 8.4.40
+      tailwindcss:
+        specifier: ^3.4.1
+        version: 3.4.7
+      typescript:
+        specifier: ^5
+        version: 5.5.4
+
   packages/tsconfig: {}
 
   packages/ui:
@@ -1599,6 +1684,9 @@ packages:
   '@next/eslint-plugin-next@14.2.5':
     resolution: {integrity: sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==}
 
+  '@next/eslint-plugin-next@14.2.6':
+    resolution: {integrity: sha512-d3+p4AjIYmhqzYHhhmkRYYN6ZU35TwZAKX08xKRfnHkz72KhWL2kxMFsDptpZs5e8bBGdepn7vn1+9DaF8iX+A==}
+
   '@next/mdx@14.2.5':
     resolution: {integrity: sha512-AROhSdXQg0/jt55iqxVSJqp9oaCyXwRe44/I17c77gDshZ6ex7VKBZDH0GljaxZ0Y4mScYUbFJJEh42Xw4X4Dg==}
     peerDependencies:
@@ -3705,6 +3793,15 @@ packages:
       typescript:
         optional: true
 
+  eslint-config-next@14.2.6:
+    resolution: {integrity: sha512-z0URA5LO6y8lS/YLN0EDW/C4LEkDODjJzA37dvLVdzCPzuewjzTe1os5g3XclZAZrQ8X8hPaSMQ2JuVWwMmrTA==}
+    peerDependencies:
+      eslint: ^7.23.0 || ^8.0.0
+      typescript: '>=3.3.1'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   eslint-config-prettier@9.1.0:
     resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
     hasBin: true
@@ -6863,6 +6960,10 @@ snapshots:
     dependencies:
       glob: 10.3.10
 
+  '@next/eslint-plugin-next@14.2.6':
+    dependencies:
+      glob: 10.3.10
+
   '@next/mdx@14.2.5(@mdx-js/loader@3.0.1(webpack@5.93.0))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1))':
     dependencies:
       source-map: 0.7.4
@@ -8389,7 +8490,7 @@ snapshots:
       react: 18.3.1
       urql: 4.1.0(@urql/core@5.0.5(graphql@16.9.0))(react@18.3.1)
 
-  '@vercel/style-guide@5.2.0(@next/eslint-plugin-next@14.2.5)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)':
+  '@vercel/style-guide@5.2.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)':
     dependencies:
       '@babel/core': 7.25.2
       '@babel/eslint-parser': 7.25.1(@babel/core@7.25.2)(eslint@8.57.0)
@@ -8411,7 +8512,7 @@ snapshots:
       eslint-plugin-unicorn: 48.0.1(eslint@8.57.0)
       prettier-plugin-packagejson: 2.5.1(prettier@3.3.3)
     optionalDependencies:
-      '@next/eslint-plugin-next': 14.2.5
+      '@next/eslint-plugin-next': 14.2.6
       eslint: 8.57.0
       prettier: 3.3.3
       typescript: 5.5.4
@@ -9165,6 +9266,24 @@ snapshots:
       - eslint-import-resolver-webpack
       - supports-color
 
+  eslint-config-next@14.2.6(eslint@8.57.0)(typescript@5.5.4):
+    dependencies:
+      '@next/eslint-plugin-next': 14.2.6
+      '@rushstack/eslint-patch': 1.10.4
+      '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4)
+      eslint: 8.57.0
+      eslint-import-resolver-node: 0.3.9
+      eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
+      eslint-plugin-react: 7.35.0(eslint@8.57.0)
+      eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
+    optionalDependencies:
+      typescript: 5.5.4
+    transitivePeerDependencies:
+      - eslint-import-resolver-webpack
+      - supports-color
+
   eslint-config-prettier@9.1.0(eslint@8.57.0):
     dependencies:
       eslint: 8.57.0